Skip to content

feat(channels): add Plivo SMS channel adapter#20

Open
sarveshpatil-plivo wants to merge 5 commits into
framerslab:masterfrom
sarveshpatil-plivo:plivo-sms-channel
Open

feat(channels): add Plivo SMS channel adapter#20
sarveshpatil-plivo wants to merge 5 commits into
framerslab:masterfrom
sarveshpatil-plivo:plivo-sms-channel

Conversation

@sarveshpatil-plivo

@sarveshpatil-plivo sarveshpatil-plivo commented Jul 14, 2026

Copy link
Copy Markdown

Summary

  • Teams already on Plivo can wire SMS into agent workflows without routing
    through another provider.
  • Adds a Plivo SMS channel adapter so agents can send and receive SMS through
    Plivo, registered as its own plivo channel platform alongside the existing
    adapters.
  • This complements the Plivo voice provider already in the telephony
    subsystem, filling the SMS gap without touching voice.
  • Inbound webhooks are verified with the Plivo V3 signature and fail closed,
    so unsigned or invalid requests are dropped.

Checklist

  • Tests added/updated
  • Docs updated (README or typedoc)
  • Conventional commit title/body

Summary by Sourcery

Add a Plivo-backed SMS messaging channel adapter and wire it into the channels platform registry.

New Features:

  • Introduce a PlivoSmsChannelAdapter to send and receive SMS via Plivo as a dedicated plivo messaging platform.
  • Expose a helper to compute Plivo V3 webhook signatures for integration and validation use cases.

Documentation:

  • Document the Plivo SMS channel, its required environment variables, initialization example, and inbound webhook wiring in CHANNELS.md.

Tests:

  • Add unit tests covering Plivo SMS outbound sending, inbound webhook handling, and Plivo V3 signature computation against a golden fixture.

Summary by CodeRabbit

  • New Features
    • Added Plivo as a new SMS messaging channel.
    • Enabled outbound SMS delivery through Plivo.
    • Added inbound webhook handling for Plivo, including optional fail-closed signature verification.
    • Exposed Plivo channel adapter configuration and signature utility for integration.
  • Documentation
    • Added a Plivo (SMS) setup guide with required environment variables and webhook/Express example.
  • Tests
    • Added unit tests for outbound request formatting, inbound webhook signature verification, and fail/verification edge cases.

sarveshpatil-plivo and others added 3 commits July 14, 2026 13:18
Fill the `sms` channel slot with a Plivo-backed adapter using Plivo's
Messaging API. Outbound sends via POST /v1/Account/{authId}/Message/;
inbound messages arrive on a webhook the host forwards to
handleIncomingWebhook, which verifies the X-Plivo-Signature-V3 signature
and fails closed before emitting.

Register the adapter in adapters/index.ts and document setup in
docs/features/CHANNELS.md. Unit tests cover outbound send, inbound
emit/drop, and a golden-fixture signature check against the Plivo SDK.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Signed-off-by: sarveshpatil-plivo <sarvesh.patil@plivo.com>
Register the adapter under a dedicated 'plivo' ChannelPlatform instead of
taking over the shared 'sms' slot, and restore the original Twilio entry in
the channels doc (Plivo is added as a separate row/section, not a swap).

Signed-off-by: sarveshpatil-plivo <sarvesh.patil@plivo.com>
- fail fast on 401/403 account-verification (auth is known-bad), while still
  tolerating transient/network errors at connect
- log when an inbound webhook is dropped because the adapter is not connected

Signed-off-by: sarveshpatil-plivo <sarvesh.patil@plivo.com>
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.

@sourcery-ai

sourcery-ai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds a new Plivo-backed SMS messaging channel adapter, wires it into the existing channel registry and types, documents configuration and usage, and implements signature-verified inbound webhook handling with accompanying tests for outbound/inbound flows and Plivo V3 signature computation.

Sequence diagram for inbound Plivo SMS webhook handling

sequenceDiagram
  participant Plivo
  participant HostApp
  participant PlivoSmsChannelAdapter
  participant AgentOSCore

  Plivo->>HostApp: POST /plivo/inbound (SMS webhook)
  HostApp->>PlivoSmsChannelAdapter: handleIncomingWebhook(body, meta)
  PlivoSmsChannelAdapter->>PlivoSmsChannelAdapter: isFromPlivo(body, meta)
  PlivoSmsChannelAdapter->>PlivoSmsChannelAdapter: computePlivoV3Signature(method, url, nonce, authToken, params)
  alt valid_signature
    PlivoSmsChannelAdapter->>AgentOSCore: emit({ type: message, platform: plivo, data: ChannelMessage })
  else invalid_or_missing_signature
    PlivoSmsChannelAdapter-->>AgentOSCore: [drop webhook]
  end
Loading

File-Level Changes

Change Details Files
Introduce Plivo SMS channel adapter implementation, including outbound send logic and inbound webhook handling with Plivo V3 signature verification.
  • Add PlivoSmsChannelAdapter class extending the base channel adapter with platform metadata and text-only capabilities.
  • Implement connection logic that initializes Plivo credentials, constructs a Basic auth header, and optionally verifies the account via a GET request.
  • Implement outbound SMS sending via Plivo’s Message API, collapsing text blocks and returning the message UUID or API id.
  • Implement inbound webhook handler that validates adapter state, optionally verifies X-Plivo-Signature-V3 using a timing-safe comparison, and emits normalized ChannelMessage events.
  • Add helper functions for computing Plivo V3 signatures, sorting params, and reading case-insensitive headers, plus an auth params interface for configuration.
src/io/channels/adapters/PlivoSmsChannelAdapter.ts
Register the Plivo SMS adapter and platform type in the public channel adapter API and channel platform union.
  • Export PlivoSmsChannelAdapter, computePlivoV3Signature, and associated auth params type from the adapters index for consumer use.
  • Extend the ChannelPlatform union to include the new plivo platform identifier.
src/io/channels/adapters/index.ts
src/io/channels/types.ts
Document Plivo SMS channel configuration, environment variables, initialization, and inbound webhook forwarding/verification in the channels feature docs.
  • Add plivo to the channels capabilities table with required environment variables.
  • Add a dedicated Plivo SMS section explaining how to configure Auth ID, Auth Token, sender number, and webhook URL.
  • Provide TypeScript examples for initializing the adapter and wiring an inbound Express route that passes method, URL, and headers for signature verification.
docs/features/CHANNELS.md
Add unit tests for Plivo SMS adapter behavior and signature computation.
  • Test computePlivoV3Signature against a Plivo SDK golden fixture and verify param-order independence.
  • Add outbound tests that validate POST request shape and returned message id, and that non-text content is rejected.
  • Add inbound tests that check event emission on valid signatures, support for array-valued headers, and dropping messages when signatures are missing or invalid, plus bypassing verification when disabled.
src/io/channels/adapters/__tests__/PlivoSmsChannelAdapter.test.ts

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@coderabbitai

coderabbitai Bot commented Jul 14, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: fe273ae1-f375-4a62-996e-b35c2e90a629

📥 Commits

Reviewing files that changed from the base of the PR and between 7687712 and 42e9bdb.

📒 Files selected for processing (1)
  • docs/features/CHANNELS.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • docs/features/CHANNELS.md

📝 Walkthrough

Walkthrough

Plivo SMS support adds a channel adapter for outbound messages and inbound webhooks, including Plivo V3 signature verification, public channel exports, platform typing, tests, and setup documentation.

Changes

Plivo SMS channel

Layer / File(s) Summary
Adapter contract and outbound messaging
src/io/channels/adapters/PlivoSmsChannelAdapter.ts, src/io/channels/types.ts, src/io/channels/adapters/index.ts, src/io/channels/index.ts, src/io/channels/adapters/__tests__/PlivoSmsChannelAdapter.test.ts
Adds the Plivo platform type, adapter configuration, connection lifecycle, account verification, outbound SMS requests, message ID handling, public exports, and outbound tests.
Inbound webhook and signature verification
src/io/channels/adapters/PlivoSmsChannelAdapter.ts, src/io/channels/adapters/__tests__/PlivoSmsChannelAdapter.test.ts
Adds Plivo V3 signature computation and validation, inbound payload parsing, AgentOS message events, and tests for valid, invalid, missing, array-valued, and disabled signature checks.
Channel setup documentation
docs/features/CHANNELS.md
Documents Plivo environment variables, adapter registration, webhook configuration, and Express webhook handling.

Estimated code review effort: 4 (Complex) | ~45 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Plivo
  participant ExpressRoute
  participant PlivoSmsChannelAdapter
  participant AgentOS
  Plivo->>ExpressRoute: POST signed SMS webhook
  ExpressRoute->>PlivoSmsChannelAdapter: handleIncomingWebhook(body, method, URL, headers)
  PlivoSmsChannelAdapter->>PlivoSmsChannelAdapter: Validate V3 signature
  PlivoSmsChannelAdapter->>AgentOS: Emit normalized message event
Loading

Suggested reviewers: jddunn, victor-evogor

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main change: adding a Plivo SMS channel adapter.
Description check ✅ Passed The description covers the summary, rationale, checklist, and testing/docs status; only the related issue link is missing.
Docstring Coverage ✅ Passed Docstring coverage is 100.00% which is sufficient. The required threshold is 80.00%.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@qodo-free-for-open-source-projects

Copy link
Copy Markdown

PR Summary by Qodo

Add Plivo SMS channel adapter with signed inbound webhook verification

✨ Enhancement 🧪 Tests 📝 Documentation 🕐 40+ Minutes

Grey Divider

AI Description

• Add a new plivo messaging channel adapter for outbound and inbound SMS.
• Verify inbound webhooks using Plivo V3 signatures and drop invalid/unsigned requests.
• Export the adapter publicly and document setup; add unit tests for send/inbound/signature.
Diagram

graph TD
  agent["Agent workflow"] --> router["Channel router"] --> adapter["Plivo SMS adapter"] -->|"POST /Message"| plivoApi{{"Plivo API"}}
  plivoWebhook{{"Plivo inbound"}} --> hostRoute["Host webhook route"] -->|"forward body+meta"| adapter -->|"verify V3 + emit"| router
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Use official Plivo SDK for Messaging + signature verification
  • ➕ Less custom HTTP/request-shaping code to maintain
  • ➕ Signature verification stays aligned with upstream changes
  • ➖ Adds a runtime dependency and potentially larger bundle surface
  • ➖ Harder to inject/mocks in tests than a small fetch wrapper (depending on SDK design)
2. Move signature verification into a shared webhook-security utility
  • ➕ Reusable pattern for future webhook-based adapters (Twilio/others)
  • ➕ Centralizes timing-safe compare and canonicalization behaviors
  • ➖ Premature abstraction if no other adapters need the same V3 scheme
  • ➖ May complicate adapter code paths with indirection

Recommendation: The current approach (small fetch-based adapter + explicit V3 signature implementation with a golden fixture test) is a good fit for a library adapter: it avoids extra dependencies and keeps the security behavior transparent and testable. If additional providers with similar webhook signing are added, consider extracting common verification helpers into a shared utility at that time.

Files changed (5) +652 / -0

Enhancement (3) +392 / -0
PlivoSmsChannelAdapter.tsImplement PlivoSmsChannelAdapter with outbound send + inbound V3 verification +388/-0

Implement PlivoSmsChannelAdapter with outbound send + inbound V3 verification

• Introduces a new Plivo SMS adapter that connects using Auth ID/Auth Token, sends SMS via Plivo’s Messaging API, and emits inbound messages only after validating 'X-Plivo-Signature-V3' (fail-closed by default). Includes a standalone V3 signature computation helper (HMAC-SHA256 + canonicalization) exported for testing and integration.

src/io/channels/adapters/PlivoSmsChannelAdapter.ts

index.tsExport Plivo SMS adapter and auth params from adapters entrypoint +3/-0

Export Plivo SMS adapter and auth params from adapters entrypoint

• Registers public exports for 'PlivoSmsChannelAdapter', 'computePlivoV3Signature', and 'PlivoSmsAuthParams' so consumers can import the adapter from the adapters index.

src/io/channels/adapters/index.ts

types.tsAdd 'plivo' to ChannelPlatform union type +1/-0

Add 'plivo' to ChannelPlatform union type

• Extends the 'ChannelPlatform' type to include 'plivo', enabling type-safe initialization and event routing for the new adapter.

src/io/channels/types.ts

Tests (1) +216 / -0
PlivoSmsChannelAdapter.test.tsAdd unit tests for Plivo signature, outbound send, and inbound drop/emit +216/-0

Add unit tests for Plivo signature, outbound send, and inbound drop/emit

• Adds a golden-fixture test to ensure signature computation matches Plivo’s reference implementation. Verifies outbound request shape and message id parsing, and checks inbound behavior for valid/invalid/missing signatures and the 'verifySignature=false' escape hatch.

src/io/channels/adapters/tests/PlivoSmsChannelAdapter.test.ts

Documentation (1) +44 / -0
CHANNELS.mdDocument Plivo SMS channel setup and webhook forwarding +44/-0

Document Plivo SMS channel setup and webhook forwarding

• Adds 'plivo' as a supported Messaging channel alongside existing adapters and documents required environment variables. Provides initialization and host webhook-forwarding examples, including guidance on passing URL/method/headers for V3 signature verification.

docs/features/CHANNELS.md

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 3 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="src/io/channels/adapters/PlivoSmsChannelAdapter.ts" line_range="277-280" />
<code_context>
+    const headers = meta?.headers ?? {};
+    const signature = headerValue(headers, 'x-plivo-signature-v3');
+    const nonce = headerValue(headers, 'x-plivo-signature-v3-nonce');
+    const url = meta?.url ?? this.webhookUrl;
+    const method = (meta?.method ?? 'POST').toUpperCase();
+
+    if (!signature || !nonce || !url || !this.authToken) return false;
+
+    let expected: string;
</code_context>
<issue_to_address>
**suggestion:** Dropping inbound webhooks when `verifySignatureEnabled` is true but no URL is available may lead to silent misconfiguration.

Because `meta.url` may be missing and `this.webhookUrl` may be unset, all inbound messages are marked untrusted (`return false`) and then dropped, with no clear indication of misconfiguration. Consider either enforcing that `webhookUrl` is set in `doConnect` when `verifySignatureEnabled` is true, or logging a clear error when `url` is missing so operators understand why webhooks are being rejected.

Suggested implementation:

```typescript
    const url = meta?.url ?? this.webhookUrl;
    const method = (meta?.method ?? 'POST').toUpperCase();

    if (!signature || !nonce || !this.authToken) return false;

    if (!url) {
      this.logger?.error(
        'Plivo webhook signature verification failed: no request URL available. ' +
          'Ensure webhookUrl is configured or meta.url is provided when verifySignatureEnabled is true.',
      );
      return false;
    }

```

1. Ensure the adapter class has a `logger` instance (or similar) available; if not, introduce one consistent with the existing logging conventions in this project and pass it into `PlivoSmsChannelAdapter`.
2. Optionally, enforce that `webhookUrl` is set in your `doConnect` (or equivalent initialization) method when `verifySignatureEnabled` is true by validating configuration there and logging/throwing a configuration error early, so misconfiguration is caught before handling webhooks.
</issue_to_address>

### Comment 2
<location path="docs/features/CHANNELS.md" line_range="297" />
<code_context>
+```bash
+export PLIVO_AUTH_ID=your-auth-id
+export PLIVO_AUTH_TOKEN=your-auth-token
+export PLIVO_PHONE=+14150000000
+```
+
</code_context>
<issue_to_address>
**suggestion:** Consider clarifying the expected format of `PLIVO_PHONE` in the example.

Add a brief note indicating that `PLIVO_PHONE` should be in E.164 format (for example, `+14150000000`), or explicitly state the format Plivo requires to reduce the chance of misconfiguration.
</issue_to_address>

### Comment 3
<location path="src/io/channels/adapters/PlivoSmsChannelAdapter.ts" line_range="376" />
<code_context>
+// Plivo SMS Auth Params
+// ============================================================================
+
+/** Platform-specific parameters for a Plivo SMS connection. */
+export interface PlivoSmsAuthParams extends Record<string, string | undefined> {
+  /** Plivo Auth ID (account identifier). Required. */
</code_context>
<issue_to_address>
**issue (complexity):** Consider simplifying the adapter’s configuration and verification paths by tightening types, specializing POST-only webhook handling, and reducing generic helpers to the concrete Plivo SMS shapes you actually use.

You can trim some of the over‑generalization without losing any current behavior by tightening types and specializing the hot paths.

### 1. Use a boolean flag for signature verification

The current string protocol (`verifySignature !== 'false'`) is surprising. A boolean is simpler and self‑documenting:

```ts
// interface
export interface PlivoSmsAuthParams {
  // ...
  /** When false, inbound signature verification is disabled. Defaults to true. */
  verifySignature?: boolean;
}

// doConnect
this.verifySignatureEnabled = params.verifySignature ?? true;
```

This keeps the default behavior (enabled) but removes the “anything but 'false'” quirk.

### 2. Make `PlivoSmsAuthParams` a closed interface

`extends Record<string, string | undefined>` suggests arbitrary keys, which aren’t used and adds surface area. A closed interface is easier to reason about:

```ts
export interface PlivoSmsAuthParams {
  /** Plivo Auth ID (account identifier). Required. */
  authId?: string;
  /** Plivo Auth Token. If omitted, the `credential` field is used. */
  authToken?: string;
  /** Plivo sender number / short code / sender id used as `src`. Required. */
  phoneNumber?: string;
  /** Externally-visible message URL Plivo signs; used to verify inbound webhooks. */
  webhookUrl?: string;
  /** When false, inbound signature verification is disabled. Defaults to true. */
  verifySignature?: boolean;
}
```

If you really need arbitrary extra keys, you can explicitly model them:

```ts
  /** Extra provider-specific options. */
  extraParams?: Record<string, string | undefined>;
```

### 3. Specialize the inbound verification path to POST callbacks

The adapter only ever verifies POST callbacks. You can simplify `isFromPlivo` by hardcoding POST instead of threading a generic `method`, and by requiring `url` when verification is on:

```ts
handleIncomingWebhook(
  body: Record<string, unknown>,
  meta?: {
    /** The exact externally-visible URL Plivo POSTed to (must byte-match). */
    url: string;
    headers: Record<string, string | string[] | undefined>;
  },
): void {
  if (this.status !== 'connected') {
    console.warn('[Plivo SMS] Dropping inbound webhook — adapter not connected.');
    return;
  }

  if (this.verifySignatureEnabled && (!meta || !this.isFromPlivo(body, meta))) {
    console.warn('[Plivo SMS] Dropping inbound webhook — signature missing or invalid.');
    return;
  }

  // ...
}

private isFromPlivo(
  body: Record<string, unknown>,
  meta: { url: string; headers: Record<string, string | string[] | undefined> },
): boolean {
  const signature = headerValue(meta.headers, 'x-plivo-signature-v3');
  const nonce = headerValue(meta.headers, 'x-plivo-signature-v3-nonce');

  if (!signature || !nonce || !this.authToken) return false;

  let expected: string;
  try {
    expected = computePlivoV3Signature({
      method: 'POST',           // only POST used by this adapter
      url: meta.url,
      nonce,
      authToken: this.authToken,
      params: body,
    });
  } catch {
    return false;
  }

  // ...
}
```

This removes the “optional method + fallback URL” branching from the adapter surface, while still allowing `computePlivoV3Signature` to support GET in case you need it elsewhere.

### 4. Narrow `sortedParamsString` to the flat body shape you actually see

Plivo’s inbound SMS payload is a flat form. You can simplify the serializer to a flat map of primitives and drop recursion/array sorting (keeping the function exported and name intact):

```ts
export function computePlivoV3Signature(input: {
  method: string;
  url: string;
  nonce: string;
  authToken: string;
  params: Record<string, string | number | boolean | undefined>;
}): string {
  // ...
  if (isPost) {
    signed += sortedParamsString(input.params) + '.' + nonce;
  }
  // ...
}

function sortedParamsString(params: Record<string, string | number | boolean | undefined>): string {
  let out = '';
  for (const key of Object.keys(params).sort()) {
    const value = params[key];
    if (value === undefined) continue;
    out += key + String(value);
  }
  return out;
}
```

Call sites that currently pass `Record<string, unknown>` can be constrained at the boundary (e.g., by coercing to `Record<string, string | number | boolean | undefined>`), but the adapter’s concrete usage (flat inbound payload) remains unchanged and the implementation is much easier to follow.

### 5. Avoid generic header scanning in a tight loop

Since `isFromPlivo` only cares about two headers, normalizing header keys once can be clearer than a generic search per lookup:

```ts
private isFromPlivo(
  body: Record<string, unknown>,
  meta: { url: string; headers: Record<string, string | string[] | undefined> },
): boolean {
  const headersLower = new Map<string, string>();

  for (const [k, v] of Object.entries(meta.headers)) {
    const value = Array.isArray(v) ? v[0] : v;
    if (typeof value === 'string') {
      headersLower.set(k.toLowerCase(), value);
    }
  }

  const signature = headersLower.get('x-plivo-signature-v3');
  const nonce = headersLower.get('x-plivo-signature-v3-nonce');

  // ...
}
```

With this in place, you can either inline `headerValue` usage here or drop the helper entirely; either way, you avoid iterating over all headers for each lookup.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/io/channels/adapters/PlivoSmsChannelAdapter.ts Outdated
Comment thread docs/features/CHANNELS.md Outdated
// Plivo SMS Auth Params
// ============================================================================

/** Platform-specific parameters for a Plivo SMS connection. */

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider simplifying the adapter’s configuration and verification paths by tightening types, specializing POST-only webhook handling, and reducing generic helpers to the concrete Plivo SMS shapes you actually use.

You can trim some of the over‑generalization without losing any current behavior by tightening types and specializing the hot paths.

1. Use a boolean flag for signature verification

The current string protocol (verifySignature !== 'false') is surprising. A boolean is simpler and self‑documenting:

// interface
export interface PlivoSmsAuthParams {
  // ...
  /** When false, inbound signature verification is disabled. Defaults to true. */
  verifySignature?: boolean;
}

// doConnect
this.verifySignatureEnabled = params.verifySignature ?? true;

This keeps the default behavior (enabled) but removes the “anything but 'false'” quirk.

2. Make PlivoSmsAuthParams a closed interface

extends Record<string, string | undefined> suggests arbitrary keys, which aren’t used and adds surface area. A closed interface is easier to reason about:

export interface PlivoSmsAuthParams {
  /** Plivo Auth ID (account identifier). Required. */
  authId?: string;
  /** Plivo Auth Token. If omitted, the `credential` field is used. */
  authToken?: string;
  /** Plivo sender number / short code / sender id used as `src`. Required. */
  phoneNumber?: string;
  /** Externally-visible message URL Plivo signs; used to verify inbound webhooks. */
  webhookUrl?: string;
  /** When false, inbound signature verification is disabled. Defaults to true. */
  verifySignature?: boolean;
}

If you really need arbitrary extra keys, you can explicitly model them:

  /** Extra provider-specific options. */
  extraParams?: Record<string, string | undefined>;

3. Specialize the inbound verification path to POST callbacks

The adapter only ever verifies POST callbacks. You can simplify isFromPlivo by hardcoding POST instead of threading a generic method, and by requiring url when verification is on:

handleIncomingWebhook(
  body: Record<string, unknown>,
  meta?: {
    /** The exact externally-visible URL Plivo POSTed to (must byte-match). */
    url: string;
    headers: Record<string, string | string[] | undefined>;
  },
): void {
  if (this.status !== 'connected') {
    console.warn('[Plivo SMS] Dropping inbound webhook — adapter not connected.');
    return;
  }

  if (this.verifySignatureEnabled && (!meta || !this.isFromPlivo(body, meta))) {
    console.warn('[Plivo SMS] Dropping inbound webhook — signature missing or invalid.');
    return;
  }

  // ...
}

private isFromPlivo(
  body: Record<string, unknown>,
  meta: { url: string; headers: Record<string, string | string[] | undefined> },
): boolean {
  const signature = headerValue(meta.headers, 'x-plivo-signature-v3');
  const nonce = headerValue(meta.headers, 'x-plivo-signature-v3-nonce');

  if (!signature || !nonce || !this.authToken) return false;

  let expected: string;
  try {
    expected = computePlivoV3Signature({
      method: 'POST',           // only POST used by this adapter
      url: meta.url,
      nonce,
      authToken: this.authToken,
      params: body,
    });
  } catch {
    return false;
  }

  // ...
}

This removes the “optional method + fallback URL” branching from the adapter surface, while still allowing computePlivoV3Signature to support GET in case you need it elsewhere.

4. Narrow sortedParamsString to the flat body shape you actually see

Plivo’s inbound SMS payload is a flat form. You can simplify the serializer to a flat map of primitives and drop recursion/array sorting (keeping the function exported and name intact):

export function computePlivoV3Signature(input: {
  method: string;
  url: string;
  nonce: string;
  authToken: string;
  params: Record<string, string | number | boolean | undefined>;
}): string {
  // ...
  if (isPost) {
    signed += sortedParamsString(input.params) + '.' + nonce;
  }
  // ...
}

function sortedParamsString(params: Record<string, string | number | boolean | undefined>): string {
  let out = '';
  for (const key of Object.keys(params).sort()) {
    const value = params[key];
    if (value === undefined) continue;
    out += key + String(value);
  }
  return out;
}

Call sites that currently pass Record<string, unknown> can be constrained at the boundary (e.g., by coercing to Record<string, string | number | boolean | undefined>), but the adapter’s concrete usage (flat inbound payload) remains unchanged and the implementation is much easier to follow.

5. Avoid generic header scanning in a tight loop

Since isFromPlivo only cares about two headers, normalizing header keys once can be clearer than a generic search per lookup:

private isFromPlivo(
  body: Record<string, unknown>,
  meta: { url: string; headers: Record<string, string | string[] | undefined> },
): boolean {
  const headersLower = new Map<string, string>();

  for (const [k, v] of Object.entries(meta.headers)) {
    const value = Array.isArray(v) ? v[0] : v;
    if (typeof value === 'string') {
      headersLower.set(k.toLowerCase(), value);
    }
  }

  const signature = headersLower.get('x-plivo-signature-v3');
  const nonce = headersLower.get('x-plivo-signature-v3-nonce');

  // ...
}

With this in place, you can either inline headerValue usage here or drop the helper entirely; either way, you avoid iterating over all headers for each lookup.

@qodo-free-for-open-source-projects

qodo-free-for-open-source-projects Bot commented Jul 14, 2026

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (1) 📘 Rule violations (0) 📎 Requirement gaps (0) 🎨 UX issues (0) 🔗 Cross-repo conflicts (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Adapter not publicly exported ✓ Resolved 🐞 Bug ≡ Correctness
Description
The docs instruct import { PlivoSmsChannelAdapter } from '@framers/agentos', but
@framers/agentos re-exports src/io/channels/index.ts, which does not export
PlivoSmsChannelAdapter, and the package exports map does not expose a deep import path for
io/channels/adapters. Consumers following the docs will hit an import/compile failure and be
unable to use the new channel adapter.
Code

docs/features/CHANNELS.md[R300-306]

+```typescript
+import { PlivoSmsChannelAdapter } from '@framers/agentos'; // src/io/channels/adapters
+
+const sms = new PlivoSmsChannelAdapter();
+await sms.initialize({
+  platform: 'plivo',
+  credential: process.env.PLIVO_AUTH_TOKEN!, // Auth Token
Evidence
The docs show a root-package import for Plivo, but the root export chain only re-exports
./io/channels, and that barrel does not include Plivo. The package exports map also doesn’t allow
a supported deep import to reach io/channels/adapters, so the documented import cannot work for
published consumers.

docs/features/CHANNELS.md[300-316]
src/io/channels/adapters/index.ts[27-34]
src/index.ts[82-85]
src/io/channels/index.ts[14-44]
package.json[258-272]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The new `PlivoSmsChannelAdapter` is exported only from `src/io/channels/adapters/index.ts`, but the package’s public entrypoint (`@framers/agentos` → `src/index.ts` → `src/io/channels/index.ts`) does not re-export it. The docs now recommend importing it from `@framers/agentos`, which will fail for consumers.
## Issue Context
- `src/index.ts` re-exports `./io/channels`.
- `src/io/channels/index.ts` explicitly enumerates which adapters are exported, and currently omits Plivo.
- `package.json` exports map exposes `./io/channels` but not `./io/channels/adapters`, so consumers can’t rely on deep imports as a workaround.
## Fix Focus Areas
- Add exports in the channels barrel so `@framers/agentos` consumers can import the adapter:
- src/io/channels/index.ts[14-44]
- Ensure docs import matches the supported public API:
- docs/features/CHANNELS.md[300-316]
- (Optional) If you intend to support `@framers/agentos/io/channels/adapters` deep imports, add an exports-map entry:
- package.json[258-272]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Plivo V3 signature inconsistency 🐞 Bug ⛨ Security
Description
PlivoSmsChannelAdapter computes/validates X-Plivo-Signature-V3 using a
URL+query+sorted-body-params+nonce signing string, while the existing PlivoVoiceProvider in this
repo documents and implements Plivo V3 verification as HMAC(url + nonce) with no body involvement.
With two incompatible V3 algorithms in-tree for the same header names, at least one Plivo webhook
surface (voice or SMS) is likely being verified with the wrong contract unless Plivo explicitly uses
different schemes per product.
Code

src/io/channels/adapters/PlivoSmsChannelAdapter.ts[R270-340]

+  private isFromPlivo(
+    body: Record<string, unknown>,
+    meta?: { method?: string; url?: string; headers?: Record<string, string | string[] | undefined> },
+  ): boolean {
+    const headers = meta?.headers ?? {};
+    const signature = headerValue(headers, 'x-plivo-signature-v3');
+    const nonce = headerValue(headers, 'x-plivo-signature-v3-nonce');
+    const url = meta?.url ?? this.webhookUrl;
+    const method = (meta?.method ?? 'POST').toUpperCase();
+
+    if (!signature || !nonce || !url || !this.authToken) return false;
+
+    let expected: string;
+    try {
+      expected = computePlivoV3Signature({ method, url, nonce, authToken: this.authToken, params: body });
+    } catch {
+      return false; // malformed URL/input is untrusted, not a crash
+    }
+
+    const expectedBuf = Buffer.from(expected);
+    return signature.split(',').some((candidate) => {
+      const candBuf = Buffer.from(candidate.trim());
+      return candBuf.length === expectedBuf.length && timingSafeEqual(candBuf, expectedBuf);
+    });
+  }
+}
+
+// ============================================================================
+// Signature helpers (exported for testing against the Plivo SDK golden fixture)
+// ============================================================================
+
+/**
+ * Compute Plivo's X-Plivo-Signature-V3 for a callback, matching the algorithm
+ * in `plivo-python`'s `signature_v3.py`.
+ *
+ * For a POST callback the signed string is:
+ *   `{scheme}://{host}{path}?` + (query as sorted `k=v&…` + `.` if present)
+ *   + sorted, separator-less `key`+`value` body params + `.` + nonce
+ * then `base64(HMAC_SHA256(authToken, signedString))`.
+ */
+export function computePlivoV3Signature(input: {
+  method: string;
+  url: string;
+  nonce: string;
+  authToken: string;
+  params: Record<string, unknown>;
+}): string {
+  const { method, url, nonce, authToken, params } = input;
+  const parsed = new URL(url);
+  const base = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
+  const isPost = method.toUpperCase() === 'POST';
+
+  let signed = base + '?';
+
+  // If the URL carried a query string, append it as sorted k=v&k=v.
+  if (parsed.search && parsed.search.length > 1) {
+    signed += [...parsed.searchParams.entries()]
+      .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
+      .map(([k, v]) => `${k}=${v}`)
+      .join('&');
+    if (isPost) signed += '.'; // separator between query and the POST params
+  }
+
+  // POST callbacks append sorted key+value body params, then '.' + nonce.
+  // GET callbacks are signed over the query string alone (params ARE the query).
+  if (isPost) {
+    signed += sortedParamsString(params) + '.' + nonce;
+  }
+
+  return createHmac('sha256', authToken).update(signed).digest('base64');
+}
Evidence
The SMS adapter verifies signatures using computePlivoV3Signature(...) which builds a signing
string that includes sorted POST body parameters and a . separator before the nonce. The Plivo
voice provider in the same repo explicitly documents and implements the V3 scheme as signing only
{url}{nonce} and even states the POST body is not included; its unit test fixture also encodes
that assumption. These two algorithms cannot both be correct for the same webhook contract and
header names.

src/io/channels/adapters/PlivoSmsChannelAdapter.ts[264-340]
src/io/channels/telephony/providers/plivo.ts[23-36]
src/io/channels/telephony/providers/plivo.ts[151-183]
src/io/channels/telephony/tests/PlivoVoiceProvider.test.ts[38-66]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Two different “Plivo V3” signature algorithms exist in the repo:
- SMS adapter: signs URL (with normalized query) + sorted body params + `.` + nonce.
- Voice provider: signs `{fullRequestURL}{nonce}` only (explicitly states body is not included).
This inconsistency makes signature verification behavior ambiguous and increases the risk of rejecting valid webhooks (or documenting the wrong security behavior).
## Issue Context
- The SMS adapter is new and defaults to `verifySignatureEnabled = true`, so if its algorithm does not match Plivo’s SMS webhook contract, inbound SMS will be dropped.
- The voice provider is already implemented/tested with the URL+nonce scheme.
## Fix Focus Areas
- Decide and document the authoritative signing contract(s) and make implementations consistent (either shared helper or clearly named product-specific helpers):
- src/io/channels/adapters/PlivoSmsChannelAdapter.ts[264-340]
- src/io/channels/telephony/providers/plivo.ts[23-36]
- src/io/channels/telephony/providers/plivo.ts[151-183]
- Add/adjust tests to prevent future drift between Plivo webhook verifiers:
- src/io/channels/adapters/__tests__/PlivoSmsChannelAdapter.test.ts[18-55]
- src/io/channels/telephony/__tests__/PlivoVoiceProvider.test.ts[38-66]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment thread docs/features/CHANNELS.md
Comment on lines +270 to +340
private isFromPlivo(
body: Record<string, unknown>,
meta?: { method?: string; url?: string; headers?: Record<string, string | string[] | undefined> },
): boolean {
const headers = meta?.headers ?? {};
const signature = headerValue(headers, 'x-plivo-signature-v3');
const nonce = headerValue(headers, 'x-plivo-signature-v3-nonce');
const url = meta?.url ?? this.webhookUrl;
const method = (meta?.method ?? 'POST').toUpperCase();

if (!signature || !nonce || !url || !this.authToken) return false;

let expected: string;
try {
expected = computePlivoV3Signature({ method, url, nonce, authToken: this.authToken, params: body });
} catch {
return false; // malformed URL/input is untrusted, not a crash
}

const expectedBuf = Buffer.from(expected);
return signature.split(',').some((candidate) => {
const candBuf = Buffer.from(candidate.trim());
return candBuf.length === expectedBuf.length && timingSafeEqual(candBuf, expectedBuf);
});
}
}

// ============================================================================
// Signature helpers (exported for testing against the Plivo SDK golden fixture)
// ============================================================================

/**
* Compute Plivo's X-Plivo-Signature-V3 for a callback, matching the algorithm
* in `plivo-python`'s `signature_v3.py`.
*
* For a POST callback the signed string is:
* `{scheme}://{host}{path}?` + (query as sorted `k=v&…` + `.` if present)
* + sorted, separator-less `key`+`value` body params + `.` + nonce
* then `base64(HMAC_SHA256(authToken, signedString))`.
*/
export function computePlivoV3Signature(input: {
method: string;
url: string;
nonce: string;
authToken: string;
params: Record<string, unknown>;
}): string {
const { method, url, nonce, authToken, params } = input;
const parsed = new URL(url);
const base = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
const isPost = method.toUpperCase() === 'POST';

let signed = base + '?';

// If the URL carried a query string, append it as sorted k=v&k=v.
if (parsed.search && parsed.search.length > 1) {
signed += [...parsed.searchParams.entries()]
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
.map(([k, v]) => `${k}=${v}`)
.join('&');
if (isPost) signed += '.'; // separator between query and the POST params
}

// POST callbacks append sorted key+value body params, then '.' + nonce.
// GET callbacks are signed over the query string alone (params ARE the query).
if (isPost) {
signed += sortedParamsString(params) + '.' + nonce;
}

return createHmac('sha256', authToken).update(signed).digest('base64');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Action required

2. Plivo v3 signature inconsistency 🐞 Bug ⛨ Security

PlivoSmsChannelAdapter computes/validates X-Plivo-Signature-V3 using a
URL+query+sorted-body-params+nonce signing string, while the existing PlivoVoiceProvider in this
repo documents and implements Plivo V3 verification as HMAC(url + nonce) with no body involvement.
With two incompatible V3 algorithms in-tree for the same header names, at least one Plivo webhook
surface (voice or SMS) is likely being verified with the wrong contract unless Plivo explicitly uses
different schemes per product.
Agent Prompt
## Issue description
Two different “Plivo V3” signature algorithms exist in the repo:
- SMS adapter: signs URL (with normalized query) + sorted body params + `.` + nonce.
- Voice provider: signs `{fullRequestURL}{nonce}` only (explicitly states body is not included).

This inconsistency makes signature verification behavior ambiguous and increases the risk of rejecting valid webhooks (or documenting the wrong security behavior).

## Issue Context
- The SMS adapter is new and defaults to `verifySignatureEnabled = true`, so if its algorithm does not match Plivo’s SMS webhook contract, inbound SMS will be dropped.
- The voice provider is already implemented/tested with the URL+nonce scheme.

## Fix Focus Areas
- Decide and document the authoritative signing contract(s) and make implementations consistent (either shared helper or clearly named product-specific helpers):
  - src/io/channels/adapters/PlivoSmsChannelAdapter.ts[264-340]
  - src/io/channels/telephony/providers/plivo.ts[23-36]
  - src/io/channels/telephony/providers/plivo.ts[151-183]
- Add/adjust tests to prevent future drift between Plivo webhook verifiers:
  - src/io/channels/adapters/__tests__/PlivoSmsChannelAdapter.test.ts[18-55]
  - src/io/channels/telephony/__tests__/PlivoVoiceProvider.test.ts[38-66]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (2)
src/io/channels/adapters/__tests__/PlivoSmsChannelAdapter.test.ts (1)

95-216: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Missing coverage for connection-hardening behaviors called out in this PR.

The commit summary highlights two hardening behaviors — failing fast on invalid account credentials (401/403) and logging inbound webhooks dropped while disconnected — but neither has a dedicated test here. Worth adding:

  • A doConnect/initialize test where makeFetch returns status: 401 for the account GET, asserting initialize rejects.
  • A handleIncomingWebhook test called before connect() (or after shutdown()), asserting no event is emitted.

Want me to draft these two test cases?

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/io/channels/adapters/__tests__/PlivoSmsChannelAdapter.test.ts` around
lines 95 - 216, The Plivo adapter tests lack coverage for credential failures
during initialization and webhook handling while disconnected. Add a connection
test using makeFetch to return 401 for the account GET and assert initialize
rejects, then add a handleIncomingWebhook test before connect or after shutdown
that asserts no message event is emitted and preserves the disconnected-drop
behavior.
src/io/channels/adapters/PlivoSmsChannelAdapter.ts (1)

118-143: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Fragile fail-fast detection via Error.message string matching.

The 401/403 fail-fast (Line 135) is re-detected in the catch block via err.message.includes('Authentication failed') (Line 139). Editing either string independently (e.g., i18n, wording tweak) silently turns a hard-auth failure into a tolerated warning, defeating the fail-fast goal. Prefer a typed/sentinel error (custom error class or a property flag) instead of substring matching.

♻️ Suggested refactor
+class PlivoAuthError extends Error {}
+
       if (resp.status === 401 || resp.status === 403) {
-        throw new Error(`[Plivo SMS] Authentication failed (HTTP ${resp.status}) — check authId/authToken.`);
+        throw new PlivoAuthError(`[Plivo SMS] Authentication failed (HTTP ${resp.status}) — check authId/authToken.`);
       }
       console.warn(`[Plivo SMS] Account verification returned HTTP ${resp.status}.`);
     } catch (err) {
-      if (err instanceof Error && err.message.includes('Authentication failed')) {
+      if (err instanceof PlivoAuthError) {
         throw err;
       }
       console.warn(`[Plivo SMS] Account verification failed: ${err}`);
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/io/channels/adapters/PlivoSmsChannelAdapter.ts` around lines 118 - 143,
Replace the authentication-failure message substring check in the verification
method with a typed or sentinel error shared by the 401/403 branch and the catch
block. Update the branch that throws on unauthorized responses and the catch
handling so this error is rethrown reliably regardless of message wording, while
other failures remain warnings.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@docs/features/CHANNELS.md`:
- Line 48: Use PLIVO_PHONE_NUMBER consistently in both documented locations:
update the Plivo required-variable table at docs/features/CHANNELS.md lines
48-48, and update the setup example’s export and read references at
docs/features/CHANNELS.md lines 295-309. No other documentation changes are
needed.

In `@src/io/channels/adapters/PlivoSmsChannelAdapter.ts`:
- Around line 119-122: Add request timeouts to both outbound Plivo calls in the
PlivoSmsChannelAdapter: the account-verification fetch and the outbound Message/
POST. Pass an appropriate AbortSignal.timeout value in each fetch request while
preserving the existing headers, authentication, and response handling.
- Around line 270-340: Update isFromPlivo to validate Plivo SMS webhooks using
the X-Plivo-Signature-V2 header and its corresponding V2 verification algorithm,
instead of requiring X-Plivo-Signature-V3 and nonce. Preserve the existing
fail-closed behavior when the signature, URL, or auth token is missing, and use
the appropriate V2 signature helper or implementation.

---

Nitpick comments:
In `@src/io/channels/adapters/__tests__/PlivoSmsChannelAdapter.test.ts`:
- Around line 95-216: The Plivo adapter tests lack coverage for credential
failures during initialization and webhook handling while disconnected. Add a
connection test using makeFetch to return 401 for the account GET and assert
initialize rejects, then add a handleIncomingWebhook test before connect or
after shutdown that asserts no message event is emitted and preserves the
disconnected-drop behavior.

In `@src/io/channels/adapters/PlivoSmsChannelAdapter.ts`:
- Around line 118-143: Replace the authentication-failure message substring
check in the verification method with a typed or sentinel error shared by the
401/403 branch and the catch block. Update the branch that throws on
unauthorized responses and the catch handling so this error is rethrown reliably
regardless of message wording, while other failures remain warnings.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: 62bf8496-20ba-4390-8d90-c73c8c91f4fc

📥 Commits

Reviewing files that changed from the base of the PR and between 6f8124c and 77ae9c9.

📒 Files selected for processing (5)
  • docs/features/CHANNELS.md
  • src/io/channels/adapters/PlivoSmsChannelAdapter.ts
  • src/io/channels/adapters/__tests__/PlivoSmsChannelAdapter.test.ts
  • src/io/channels/adapters/index.ts
  • src/io/channels/types.ts

Comment thread docs/features/CHANNELS.md Outdated
Comment on lines +119 to +122
const resp = await this.fetchImpl(
`https://api.plivo.com/v1/Account/${this.authId}/`,
{ headers: { Authorization: this.authHeader } },
);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

No timeout on outbound Plivo fetch calls.

Neither the account-verification GET (Lines 119-122) nor the outbound Message/ POST (Lines 167-182) sets a request timeout (e.g., AbortSignal.timeout(...)). A slow/hung Plivo API response would block the caller (connect or sendMessage) indefinitely.

🕐 Suggested fix
       const resp = await this.fetchImpl(
         `https://api.plivo.com/v1/Account/${this.authId}/Message/`,
         {
           method: 'POST',
           headers: { ... },
           body: JSON.stringify({ ... }),
+          signal: AbortSignal.timeout(10_000),
         },
       );

Also applies to: 167-182

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/io/channels/adapters/PlivoSmsChannelAdapter.ts` around lines 119 - 122,
Add request timeouts to both outbound Plivo calls in the PlivoSmsChannelAdapter:
the account-verification fetch and the outbound Message/ POST. Pass an
appropriate AbortSignal.timeout value in each fetch request while preserving the
existing headers, authentication, and response handling.

Comment on lines +270 to +340
private isFromPlivo(
body: Record<string, unknown>,
meta?: { method?: string; url?: string; headers?: Record<string, string | string[] | undefined> },
): boolean {
const headers = meta?.headers ?? {};
const signature = headerValue(headers, 'x-plivo-signature-v3');
const nonce = headerValue(headers, 'x-plivo-signature-v3-nonce');
const url = meta?.url ?? this.webhookUrl;
const method = (meta?.method ?? 'POST').toUpperCase();

if (!signature || !nonce || !url || !this.authToken) return false;

let expected: string;
try {
expected = computePlivoV3Signature({ method, url, nonce, authToken: this.authToken, params: body });
} catch {
return false; // malformed URL/input is untrusted, not a crash
}

const expectedBuf = Buffer.from(expected);
return signature.split(',').some((candidate) => {
const candBuf = Buffer.from(candidate.trim());
return candBuf.length === expectedBuf.length && timingSafeEqual(candBuf, expectedBuf);
});
}
}

// ============================================================================
// Signature helpers (exported for testing against the Plivo SDK golden fixture)
// ============================================================================

/**
* Compute Plivo's X-Plivo-Signature-V3 for a callback, matching the algorithm
* in `plivo-python`'s `signature_v3.py`.
*
* For a POST callback the signed string is:
* `{scheme}://{host}{path}?` + (query as sorted `k=v&…` + `.` if present)
* + sorted, separator-less `key`+`value` body params + `.` + nonce
* then `base64(HMAC_SHA256(authToken, signedString))`.
*/
export function computePlivoV3Signature(input: {
method: string;
url: string;
nonce: string;
authToken: string;
params: Record<string, unknown>;
}): string {
const { method, url, nonce, authToken, params } = input;
const parsed = new URL(url);
const base = `${parsed.protocol}//${parsed.host}${parsed.pathname}`;
const isPost = method.toUpperCase() === 'POST';

let signed = base + '?';

// If the URL carried a query string, append it as sorted k=v&k=v.
if (parsed.search && parsed.search.length > 1) {
signed += [...parsed.searchParams.entries()]
.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
.map(([k, v]) => `${k}=${v}`)
.join('&');
if (isPost) signed += '.'; // separator between query and the POST params
}

// POST callbacks append sorted key+value body params, then '.' + nonce.
// GET callbacks are signed over the query string alone (params ARE the query).
if (isPost) {
signed += sortedParamsString(params) + '.' + nonce;
}

return createHmac('sha256', authToken).update(signed).digest('base64');
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🔒 Security & Privacy | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Does Plivo currently support X-Plivo-Signature-V3 for SMS/messaging webhooks, or only X-Plivo-Signature-V2?

💡 Result:

Plivo currently uses X-Plivo-Signature-V2 for SMS/messaging webhooks [1][2][3][4]. While X-Plivo-Signature-V3 is supported by Plivo, it is specifically used for voice callbacks, which incorporate a nonce for enhanced security (replay protection) [5][6][2][7]. SMS callbacks and messaging-related webhooks continue to rely on the X-Plivo-Signature-V2 header for signature validation [1][2][3].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/io/channels/adapters/PlivoSmsChannelAdapter.ts"
wc -l "$FILE"
sed -n '80,360p' "$FILE" | cat -n

Repository: framerslab/agentos

Length of output: 12059


🏁 Script executed:

#!/bin/bash
set -euo pipefail

FILE="src/io/channels/adapters/PlivoSmsChannelAdapter.ts"

# Show the configuration fields and the webhook verification method.
sed -n '1,120p' "$FILE" | cat -n
printf '\n---\n'
sed -n '250,345p' "$FILE" | cat -n

Repository: framerslab/agentos

Length of output: 9657


Use Plivo SMS signature V2

  • isFromPlivo() only accepts X-Plivo-Signature-V3/-Nonce, but Plivo SMS webhooks use X-Plivo-Signature-V2; with signature checking enabled by default, valid inbound SMS will be dropped.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/io/channels/adapters/PlivoSmsChannelAdapter.ts` around lines 270 - 340,
Update isFromPlivo to validate Plivo SMS webhooks using the X-Plivo-Signature-V2
header and its corresponding V2 verification algorithm, instead of requiring
X-Plivo-Signature-V3 and nonce. Preserve the existing fail-closed behavior when
the signature, URL, or auth token is missing, and use the appropriate V2
signature helper or implementation.

- Re-export PlivoSmsChannelAdapter and PlivoSmsAuthParams from the channels
  barrel so the documented import from '@framers/agentos' resolves.
- Use PLIVO_PHONE_NUMBER in the docs to match the configured secret name, and
  note the sender must be E.164.
- Log a clear warning when an inbound webhook cannot be verified because no
  request URL is available, instead of a silent failure that looks like a bad
  signature.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/io/channels/index.ts`:
- Line 14: Update the Phase 4 comment in the adapter barrel to accurately state
“base class + 12 platform adapters,” unless the intended behavior is to export
14, in which case add the two missing adapter exports. Keep the comment and
exported adapter count consistent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro Plus

Run ID: ee1ab8ec-0808-4601-a81b-f46e531a2e3b

📥 Commits

Reviewing files that changed from the base of the PR and between 77ae9c9 and 7687712.

📒 Files selected for processing (3)
  • docs/features/CHANNELS.md
  • src/io/channels/adapters/PlivoSmsChannelAdapter.ts
  • src/io/channels/index.ts
🚧 Files skipped from review as they are similar to previous changes (2)
  • docs/features/CHANNELS.md
  • src/io/channels/adapters/PlivoSmsChannelAdapter.ts

Comment thread src/io/channels/index.ts
export type { GroupPolicyInput, GroupPolicyResult, GroupPolicyReason } from './group-policy.js';

// Phase 4: Adapter implementations — base class + 13 platform adapters
// Phase 4: Adapter implementations — base class + 14 platform adapters

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win

Correct the adapter count in the phase comment.

This barrel currently exports 12 platform adapters, not 14. Update the comment to base class + 12 platform adapters, or add the two missing adapter exports if 14 is the intended total.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/io/channels/index.ts` at line 14, Update the Phase 4 comment in the
adapter barrel to accurately state “base class + 12 platform adapters,” unless
the intended behavior is to export 14, in which case add the two missing adapter
exports. Keep the comment and exported adapter count consistent.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant