Skip to content

SF-30 add runtime shared packages#84

Merged
graphite-app[bot] merged 1 commit intodevfrom
SF-30/runtime-shared-foundations
Apr 13, 2026
Merged

SF-30 add runtime shared packages#84
graphite-app[bot] merged 1 commit intodevfrom
SF-30/runtime-shared-foundations

Conversation

@thyaravind
Copy link
Copy Markdown
Member

@thyaravind thyaravind commented Apr 4, 2026

Summary

  • add the missing shared runtime packages to origin/dev
  • include config, envelope, errors, metrics, queue, and webhooks foundations
  • keep the PR limited to the shared package layer used by multiple downstream functions

Validation

  • inspected the copied package set in a clean worktree created from origin/dev
  • did not run a reliable monorepo validation pass in this worktree

Notes

  • no .conduct changes included
  • opened directly against origin/dev with GH_CONFIG_DIR=~/.config/gh-other

Summary by Sourcery

Introduce shared runtime utility packages for envelopes, errors, config, metrics, queues, and webhooks to be reused across Superfunctions services.

New Features:

  • Add a canonical response envelope package with helpers for constructing, normalizing, and converting success/error envelopes.
  • Add a shared typed error package providing an error registry and strongly-typed SuperfunctionError constructor.
  • Add a shared config package exposing typed environment variable readers for ints, strings, and booleans.
  • Add a shared metrics package with pluggable and namespaced metrics emitters.
  • Add a shared queue package with an in-memory adapter and a FlowFn-backed queue adapter interface.
  • Add a shared webhooks package for HMAC signing/verification and retrying webhook delivery.

Build:

  • Configure TypeScript build settings and package metadata for the new config, envelope, errors, metrics, queue, and webhooks packages.

Summary by cubic

Adds shared runtime packages for config, envelope, errors, metrics, queue, and webhooks. Registers them for release and hardens envelope normalization and webhook signing/delivery edge cases (SF-30).

  • New Features
    • @superfunctions/config: int/string/boolean readers; safe-integer checks; min/max; validates defaults; empty strings off by default (allowEmpty opt‑in); booleans accept true/false, 1/0, yes/no; throws ENV_VALUE_INVALID/ENV_VALUE_OUT_OF_RANGE.
    • @superfunctions/envelope: canonical ok/err with timestamp; normalize legacy {data}/{result}; fill missing status/retryable; guard malformed legacy and ok:false+data; err() requires numeric status and boolean retryable or returns INVALID_ENVELOPE_ERROR_SHAPE.
    • @superfunctions/errors: registry; SuperfunctionError; createTypedError; unknown codes throw ERROR_CODE_UNREGISTERED.
    • @superfunctions/metrics: no‑op emitter; namespaced emitter with strict validation (METRIC_NAMESPACE_INVALID).
    • @superfunctions/queue: MemoryQueueAdapter with name normalization/validation (FLOWFN_QUEUE_NAME_INVALID); FlowFnQueueAdapter with Bearer auth, per‑request timeouts (AbortController), invalid timeout → FLOWFN_QUEUE_TIMEOUT_INVALID, trailing‑slash normalization; 404→null on dequeue; preserves explicit undefined; peek/size/dequeueMatching unsupported.
    • @superfunctions/webhooks: HMAC sign/verify (hex/base64) with strict decoding (accepts missing padding; rejects malformed) and optional prefix; delivery with retries/backoff, per‑attempt timeout, and validated timing opts (WEBHOOK_MAX_RETRIES_INVALID/WEBHOOK_INITIAL_DELAY_INVALID/WEBHOOK_MAX_DELAY_INVALID/WEBHOOK_PER_ATTEMPT_TIMEOUT_INVALID); rejects empty secrets (WEBHOOK_SECRET_INVALID) and blank signature header names; no retries on non‑transient 4xx; canonicalizes headers (case‑insensitive de‑dup, prefers latest, single Content-Type and signature header, force application/json for non‑JSON); validates JSON‑serializable payload; returns structured attempts with final responseStatus.

Written for commit 8ff8f78. Summary will update on new commits.

Summary by CodeRabbit

  • New Features

    • Configuration helpers for reading/validating env values (ints, strings, booleans).
    • Canonical envelope format with converters for legacy shapes.
    • Error registry and typed error construction utilities.
    • Metrics emitter with optional namespacing.
    • Queue abstraction with in-memory and remote adapters.
    • Webhook signing, verification, and delivery with retries/backoff and timeouts.
  • Tests

    • Added comprehensive test suites covering the new packages and behaviors.

@linear
Copy link
Copy Markdown

linear bot commented Apr 4, 2026

SF-30 Shared packages

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 4, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds six new packages—config, envelope, errors, metrics, queue, and webhooks—each with package manifests, TypeScript configs, source code, and tests; they introduce utilities for env-reading, envelope normalization, typed errors/registry, metrics emitters, queue adapters, and webhook signing/delivery.

Changes

Cohort / File(s) Summary
Config package
packages/config/package.json, packages/config/tsconfig.json, packages/config/src/index.ts, packages/config/src/__tests__/config.test.ts
New @superfunctions/config: ConfigError and env helpers readIntEnv, readIntEnvOrDefault, readStringEnv, readBooleanEnv with validation/bounds; TS config, build/test scripts, and tests.
Envelope package
packages/envelope/package.json, packages/envelope/tsconfig.json, packages/envelope/src/index.ts, packages/envelope/src/__tests__/envelope.test.ts
New @superfunctions/envelope: canonical Envelope types, ok()/err() constructors, normalizeLegacyEnvelope, toLegacyDataEnvelope, toLegacyResultEnvelope, timestamp/meta handling, plus tests.
Errors package
packages/errors/package.json, packages/errors/tsconfig.json, packages/errors/src/index.ts, packages/errors/src/__tests__/errors.test.ts
New @superfunctions/errors: ErrorDefinition, SuperfunctionError, ErrorRegistry via createErrorRegistry, createTypedError, registry resolution/fallback behavior; TS config and tests.
Metrics package
packages/metrics/package.json, packages/metrics/tsconfig.json, packages/metrics/src/index.ts, packages/metrics/src/__tests__/metrics.test.ts
New @superfunctions/metrics: MetricsEmitter interface, createMetricsEmitter, createNamespacedEmitter with namespace validation and prefixing; TS config and tests.
Queue package
packages/queue/package.json, packages/queue/tsconfig.json, packages/queue/src/index.ts, packages/queue/src/__tests__/queue.test.ts
New @superfunctions/queue: QueueAdapter interface, MemoryQueueAdapter (in-memory FIFO, name normalization, dequeueMatching), FlowFnQueueAdapter (HTTP enqueue/dequeue, bearer auth, timeout/abort handling, unsupported ops raise), helpers and tests.
Webhooks package
packages/webhooks/package.json, packages/webhooks/tsconfig.json, packages/webhooks/src/index.ts, packages/webhooks/src/__tests__/webhooks.test.ts
New @superfunctions/webhooks: signWebhookPayload, verifyWebhookSignature (hex/base64, prefix handling, timing-safe compare), deliverWebhook with JSON serialization, signature header, per-attempt timeouts, retry/backoff and attempt recording; TS config and tests.
Monorepo release config
release-packages.json
Added entries for the six new packages to release manifest.

Sequence Diagram(s)

sequenceDiagram
  participant Caller as Caller
  participant Deliver as deliverWebhook
  participant FetchAPI as fetch
  participant Remote as RemoteServer

  Caller->>Deliver: deliverWebhook(input, options)
  Deliver->>Deliver: serialize payload, build headers (incl. signature)
  Deliver->>FetchAPI: fetch(request, { signal })
  FetchAPI->>Remote: HTTP request
  Remote-->>FetchAPI: HTTP response / error / timeout
  FetchAPI-->>Deliver: response or error
  alt response.ok (2xx)
    Deliver->>Caller: return { ok: true, attempts: [...] }
  else retryable (429 or 5xx or transient error)
    Deliver->>Deliver: wait exponential backoff, retry up to maxRetries
    Deliver->>Caller: return aggregated failure attempts after retries
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~75 minutes

Possibly related PRs

Suggested labels

Review effort 4/5

Poem

🐰 I nibble through commits with cheer,
Six packages sprout from root right here.
Envelopes, queues, webhooks hum,
Errors, metrics, configs—code and drum.
A carrot patch for CI to hear.

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'SF-30 add runtime shared packages' clearly and concisely describes the main change: adding a set of new shared runtime packages to the monorepo.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch SF-30/runtime-shared-foundations

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 and usage tips.

@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Apr 4, 2026

Reviewer's Guide

Introduces a set of shared runtime packages (config, envelope, errors, metrics, queue, webhooks) into the monorepo, each as its own TypeScript package with basic implementation, build/test configuration, and package metadata, intended for reuse by downstream functions.

Sequence diagram for webhook delivery with signing and retries

sequenceDiagram
  actor Service
  participant WebhooksModule
  participant Fetch as FetchAPI
  participant Endpoint as WebhookEndpoint

  Service->>WebhooksModule: deliverWebhook(input, options)
  WebhooksModule->>WebhooksModule: JSON.stringify(input.payload)
  alt secret provided
    WebhooksModule->>WebhooksModule: signWebhookPayload(payloadString, secret, options)
    WebhooksModule->>Fetch: fetch(url, signedRequest)
  else no secret
    WebhooksModule->>Fetch: fetch(url, unsignedRequest)
  end

  loop up to maxRetries
    Fetch-->>WebhooksModule: Response
    alt response.ok
      WebhooksModule-->>Service: WebhookDeliveryResult(ok=true, attempts, responseStatus)
    else non ok or error
      WebhooksModule->>WebhooksModule: record failed attempt
      alt more retries remaining
        WebhooksModule->>WebhooksModule: sleep(backoffDelay)
        WebhooksModule->>Fetch: fetch(url, request)
      else no retries left
        WebhooksModule-->>Service: WebhookDeliveryResult(ok=false, attempts, responseStatus)
      end
    end
  end

  Fetch->>Endpoint: HTTP request with JSON body
Loading

Class diagram for shared envelope package

classDiagram
  class EnvelopeMeta {
    +string timestamp
  }

  class CanonicalEnvelopeError {
    +string code
    +string message
    +number status
    +boolean retryable
    +Record~string, unknown~ details
  }

  class SuccessEnvelope~T~ {
    +true ok
    +T data
    +EnvelopeMeta meta
  }

  class ErrorEnvelope {
    +false ok
    +CanonicalEnvelopeError error
    +EnvelopeMeta meta
  }

  class Envelope~T~ {
  }

  class LegacySuccessEnvelope~T~ {
  }

  class LegacyErrorEnvelope {
  }

  class EnvelopeModule {
    +ok~T~(data T, options timestampOptions) SuccessEnvelope~T~
    +err(input CanonicalLikeErrorInput, options timestampOptions) ErrorEnvelope
    +normalizeLegacyEnvelope~T~(envelope LegacySuccessEnvelope~T~ | LegacyErrorEnvelope, options normalizeOptions) Envelope~T~
    +toLegacyDataEnvelope~T~(envelope Envelope~T~) LegacyDataEnvelope~T~
    +toLegacyResultEnvelope~T~(envelope Envelope~T~) LegacyResultEnvelope~T~
    +isValidErrorShape(input CanonicalLikeErrorInput) boolean
  }

  class CanonicalLikeErrorInput {
    +string code
    +string message
    +number status
    +boolean retryable
    +Record~string, unknown~ details
  }

  class timestampOptions {
    +string timestamp
  }

  class normalizeOptions {
    +string timestamp
    +number defaultStatus
    +boolean defaultRetryable
  }

  class LegacyDataEnvelope~T~ {
    +boolean ok
    +T data
  }

  class LegacyResultEnvelope~T~ {
    +boolean ok
    +T result
  }

  Envelope~T~ <|-- SuccessEnvelope~T~
  Envelope~T~ <|-- ErrorEnvelope
  ErrorEnvelope --> CanonicalEnvelopeError
  SuccessEnvelope~T~ --> EnvelopeMeta
  ErrorEnvelope --> EnvelopeMeta
  EnvelopeModule --> EnvelopeMeta
  EnvelopeModule --> CanonicalEnvelopeError
  EnvelopeModule --> SuccessEnvelope~T~
  EnvelopeModule --> ErrorEnvelope
  EnvelopeModule --> LegacySuccessEnvelope~T~
  EnvelopeModule --> LegacyErrorEnvelope
  EnvelopeModule --> LegacyDataEnvelope~T~
  EnvelopeModule --> LegacyResultEnvelope~T~
  EnvelopeModule --> CanonicalLikeErrorInput
  EnvelopeModule --> normalizeOptions
  EnvelopeModule --> timestampOptions
Loading

Class diagram for shared errors package

classDiagram
  class ErrorDefinition {
    +string code
    +number httpStatus
    +boolean retryable
    +string defaultMessage
  }

  class SuperfunctionError {
    +string code
    +number status
    +boolean retryable
    +Record~string, unknown~ details
    +constructor(input SuperfunctionErrorInput)
  }

  class SuperfunctionErrorInput {
    +string code
    +string message
    +number status
    +boolean retryable
    +Record~string, unknown~ details
  }

  class ErrorRegistry {
    <<interface>>
    +register(definition ErrorDefinition) void
    +registerMany(definitions ErrorDefinition[]) void
    +has(code string) boolean
    +resolve(code string) ErrorDefinition
    +resolveOrDefault(code string, fallback ErrorDefinitionWithoutCode) ErrorDefinition
    +list() ErrorDefinition[]
  }

  class ErrorRegistryImpl {
    -Map~string, ErrorDefinition~ definitions
    +register(definition ErrorDefinition) void
    +registerMany(definitions ErrorDefinition[]) void
    +has(code string) boolean
    +resolve(code string) ErrorDefinition
    +resolveOrDefault(code string, fallback ErrorDefinitionWithoutCode) ErrorDefinition
    +list() ErrorDefinition[]
  }

  class ErrorDefinitionWithoutCode {
    +number httpStatus
    +boolean retryable
    +string defaultMessage
  }

  class ErrorModule {
    +createErrorRegistry(initialDefinitions ErrorDefinition[]) ErrorRegistry
    +createTypedError(input TypedErrorInput) SuperfunctionError
  }

  class TypedErrorInput {
    +string code
    +string message
    +Record~string, unknown~ details
    +ErrorRegistry registry
  }

  ErrorRegistry <|.. ErrorRegistryImpl
  ErrorRegistryImpl --> ErrorDefinition
  ErrorRegistryImpl --> SuperfunctionError
  SuperfunctionError --> SuperfunctionErrorInput
  ErrorModule --> ErrorRegistry
  ErrorModule --> ErrorRegistryImpl
  ErrorModule --> SuperfunctionError
  ErrorModule --> ErrorDefinition
  ErrorModule --> TypedErrorInput
Loading

Class diagram for shared metrics package

classDiagram
  class MetricsEmitter {
    <<interface>>
    +track(event string, properties Record~string, unknown~) void
  }

  class MetricsEmitterImpl {
    -trackFn(event string, properties Record~string, unknown~) void
    +track(event string, properties Record~string, unknown~) void
  }

  class NoopMetricsEmitter {
    +track(event string, properties Record~string, unknown~) void
  }

  class MetricsModule {
    +createMetricsEmitter(trackFn function) MetricsEmitter
    +createNamespacedEmitter(namespace string, emitter MetricsEmitter) MetricsEmitter
  }

  class NamespacedEmitter {
    -string namespace
    -MetricsEmitter innerEmitter
    +track(event string, properties Record~string, unknown~) void
  }

  MetricsEmitter <|.. MetricsEmitterImpl
  MetricsEmitter <|.. NoopMetricsEmitter
  MetricsEmitter <|.. NamespacedEmitter
  MetricsModule --> MetricsEmitter
  MetricsModule --> MetricsEmitterImpl
  MetricsModule --> NoopMetricsEmitter
  MetricsModule --> NamespacedEmitter
Loading

Class diagram for shared config package

classDiagram
  class ConfigError {
    +"ENV_VALUE_INVALID" | "ENV_VALUE_OUT_OF_RANGE" code
    +constructor(code "ENV_VALUE_INVALID" | "ENV_VALUE_OUT_OF_RANGE", message string)
  }

  class ConfigModule {
    +readIntEnv(key string, options IntEnvOptions) number
    +readIntEnvOrDefault(key string, options IntEnvOptions) number
    +readStringEnv(key string, options StringEnvOptions) string
    +readBooleanEnv(key string, options BooleanEnvOptions) boolean
  }

  class IntEnvOptions {
    +number defaultValue
    +number min
    +number max
    +NodeJS.ProcessEnv env
  }

  class StringEnvOptions {
    +string defaultValue
    +boolean allowEmpty
    +NodeJS.ProcessEnv env
  }

  class BooleanEnvOptions {
    +boolean defaultValue
    +NodeJS.ProcessEnv env
  }

  ConfigModule --> ConfigError
  ConfigModule --> IntEnvOptions
  ConfigModule --> StringEnvOptions
  ConfigModule --> BooleanEnvOptions
Loading

Class diagram for shared queue package

classDiagram
  class QueueAdapter~TPayload~ {
    <<interface>>
    +enqueue(queueName string, payload TPayload) Promise~void~
    +dequeue(queueName string) Promise~TPayload | null~
    +dequeueMatching(queueName string, matcher function) Promise~TPayload | null~
    +peek(queueName string) TPayload[]
    +size(queueName string) number
  }

  class MemoryQueueAdapter~TPayload~ {
    -Map~string, TPayload[]~ queues
    +enqueue(queueName string, payload TPayload) Promise~void~
    +dequeue(queueName string) Promise~TPayload | null~
    +dequeueMatching(queueName string, matcher function) Promise~TPayload | null~
    +peek(queueName string) TPayload[]
    +size(queueName string) number
  }

  class FlowFnQueueAdapterConfig {
    +string baseUrl
    +string apiKey
    +fetch function
  }

  class FlowFnQueueAdapter~TPayload~ {
    -FlowFnQueueAdapterConfig config
    -fetcher function
    +constructor(config FlowFnQueueAdapterConfig)
    +enqueue(queueName string, payload TPayload) Promise~void~
    +dequeue(queueName string) Promise~TPayload | null~
    +dequeueMatching(queueName string, matcher function) Promise~TPayload | null~
    +peek(queueName string) TPayload[]
    +size(queueName string) number
  }

  QueueAdapter~TPayload~ <|.. MemoryQueueAdapter~TPayload~
  QueueAdapter~TPayload~ <|.. FlowFnQueueAdapter~TPayload~
  FlowFnQueueAdapter~TPayload~ --> FlowFnQueueAdapterConfig
Loading

Class diagram for shared webhooks package

classDiagram
  class WebhookSignatureOptions {
    +"sha256" | "sha1" | "sha512" algorithm
    +"hex" | "base64" encoding
    +string prefix
  }

  class WebhookDeliveryInput {
    +string url
    +unknown payload
    +string secret
    +string signatureHeader
    +Record~string, string~ headers
    +"POST" | "PUT" method
  }

  class WebhookDeliveryAttempt {
    +number attempt
    +boolean ok
    +number status
    +string error
  }

  class WebhookDeliveryResult {
    +boolean ok
    +WebhookDeliveryAttempt[] attempts
    +number responseStatus
  }

  class WebhookDeliveryOptions {
    +fetch function
    +number maxRetries
    +number initialDelayMs
    +number maxDelayMs
  }

  class WebhooksModule {
    +signWebhookPayload(payload string, secret string, options WebhookSignatureOptions) string
    +verifyWebhookSignature(payload string, providedSignature string, secret string, options WebhookSignatureOptions) boolean
    +deliverWebhook(input WebhookDeliveryInput, options WebhookDeliveryOptions) Promise~WebhookDeliveryResult~
  }

  WebhooksModule --> WebhookSignatureOptions
  WebhooksModule --> WebhookDeliveryInput
  WebhooksModule --> WebhookDeliveryResult
  WebhookDeliveryResult --> WebhookDeliveryAttempt
  WebhooksModule --> WebhookDeliveryOptions
Loading

File-Level Changes

Change Details Files
Add shared response envelope utilities with legacy compatibility helpers.
  • Define canonical success/error envelope shapes with metadata and canonical error structure.
  • Provide ok/err constructors that enforce presence of status and retryable, falling back to a canonical INVALID_ENVELOPE_ERROR_SHAPE error when invalid.
  • Add helpers to normalize legacy envelope formats into the canonical shape and convert canonical envelopes back to legacy data/result forms.
  • Implement internal runtime validation of error shapes.
packages/envelope/src/index.ts
packages/envelope/package.json
packages/envelope/tsconfig.json
packages/envelope/src/__tests__/envelope.test.ts
Introduce a shared typed error system with registry and error class.
  • Define ErrorDefinition type describing error code, HTTP status, retryability, and optional default message.
  • Implement SuperfunctionError class carrying code, status, retryable, and details.
  • Implement createErrorRegistry for registering and resolving error definitions with list/resolve/resolveOrDefault APIs, throwing a typed error when resolving unknown codes.
  • Add createTypedError helper that uses the registry to construct SuperfunctionError instances with default messages.
  • Configure package metadata and TypeScript build settings for the errors package.
packages/errors/src/index.ts
packages/errors/package.json
packages/errors/tsconfig.json
packages/errors/src/__tests__/errors.test.ts
Add shared configuration helpers for reading typed environment variables.
  • Introduce ConfigError with typed error codes for invalid or out-of-range env values.
  • Provide readIntEnv/readIntEnvOrDefault for bounded integer env parsing with defaulting and range validation.
  • Provide readStringEnv for non-empty string envs with optional empty-allowance and overridable env source.
  • Provide readBooleanEnv for boolean-like env parsing with explicit allowed string forms and error on invalid values.
  • Configure package metadata and TypeScript build settings for the config package.
packages/config/src/index.ts
packages/config/package.json
packages/config/tsconfig.json
packages/config/src/__tests__/config.test.ts
Add shared metrics emitter abstraction with namespacing support.
  • Define MetricsEmitter interface with a simple track(event, properties) contract.
  • Implement createMetricsEmitter that returns a no-op emitter when no tracking function is provided, otherwise delegates to the provided track function.
  • Implement createNamespacedEmitter that validates a metric namespace and prefixes event names with the namespace, avoiding double-prefixing.
  • Configure package metadata and TypeScript build settings for the metrics package.
packages/metrics/src/index.ts
packages/metrics/package.json
packages/metrics/tsconfig.json
packages/metrics/src/__tests__/metrics.test.ts
Introduce shared queue adapter abstractions including an in-memory and FlowFn-backed implementation.
  • Define QueueAdapter interface for enqueue, dequeue, dequeueMatching, peek, and size operations.
  • Implement MemoryQueueAdapter with in-memory storage using a Map of queue names to arrays, including dequeueMatching by predicate.
  • Introduce FlowFnQueueAdapterConfig and FlowFnQueueAdapter that call external HTTP endpoints for enqueue/dequeue using fetch with bearer auth.
  • Stub out peek and size in FlowFnQueueAdapter as no-op/empty implementations, and implement a dequeueMatching that attempts to claim matching messages via dequeue.
  • Configure package metadata and TypeScript build settings for the queue package.
packages/queue/src/index.ts
packages/queue/package.json
packages/queue/tsconfig.json
packages/queue/src/__tests__/queue.test.ts
Add shared webhook signing, verification, and delivery utilities.
  • Implement HMAC-based signWebhookPayload with configurable algorithm, encoding, and optional prefix.
  • Implement verifyWebhookSignature that validates optional prefix, decodes signatures safely, and compares digests using timingSafeEqual to avoid timing leaks.
  • Define webhook delivery input/attempt/result types and WebhookDeliveryOptions for pluggable fetch and retry configuration.
  • Implement deliverWebhook with JSON payload encoding, optional signature header, bounded retry logic with linear backoff capped at a max delay, and attempt tracking.
  • Implement internal signature decoding and sleep helpers, and configure package metadata and TypeScript build settings for the webhooks package.
packages/webhooks/src/index.ts
packages/webhooks/package.json
packages/webhooks/tsconfig.json
packages/webhooks/src/__tests__/webhooks.test.ts

Possibly linked issues

  • #unknown: The PR implements the shared runtime packages described by the shared packages issue, adding foundations used downstream.

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

@qodo-code-review
Copy link
Copy Markdown

Review Summary by Qodo

Add runtime shared packages (config, envelope, errors, metrics, queue, webhooks)

✨ Enhancement

Grey Divider

Walkthroughs

Description
• Add six shared runtime packages to monorepo foundation layer
• Implement config package with typed environment variable readers
• Implement envelope package with canonical response structures
• Implement errors package with typed error registry system
• Implement metrics package with emitter abstraction
• Implement queue package with memory and FlowFn adapters
• Implement webhooks package with signing, verification, and delivery
Diagram
flowchart LR
  A["Shared Packages"] --> B["config"]
  A --> C["envelope"]
  A --> D["errors"]
  A --> E["metrics"]
  A --> F["queue"]
  A --> G["webhooks"]
  B --> B1["Env Readers"]
  C --> C1["Response Envelopes"]
  D --> D1["Error Registry"]
  E --> E1["Metrics Emitter"]
  F --> F1["Memory & FlowFn Adapters"]
  G --> G1["HMAC Signing & Delivery"]
Loading

Grey Divider

File Changes

1. packages/config/src/index.ts ✨ Enhancement +84/-0

Typed environment variable reader functions

packages/config/src/index.ts


2. packages/config/src/__tests__/config.test.ts 🧪 Tests +39/-0

Config package unit tests

packages/config/src/tests/config.test.ts


3. packages/config/package.json ⚙️ Configuration changes +37/-0

Config package metadata and dependencies

packages/config/package.json


View more (21)
4. packages/config/tsconfig.json ⚙️ Configuration changes +27/-0

Config package TypeScript configuration

packages/config/tsconfig.json


5. packages/envelope/src/index.ts ✨ Enhancement +174/-0

Canonical envelope constructors and converters

packages/envelope/src/index.ts


6. packages/envelope/src/__tests__/envelope.test.ts 🧪 Tests +41/-0

Envelope package unit tests

packages/envelope/src/tests/envelope.test.ts


7. packages/envelope/package.json ⚙️ Configuration changes +45/-0

Envelope package metadata and dependencies

packages/envelope/package.json


8. packages/envelope/tsconfig.json ⚙️ Configuration changes +27/-0

Envelope package TypeScript configuration

packages/envelope/tsconfig.json


9. packages/errors/src/index.ts ✨ Enhancement +110/-0

Error registry and typed error constructor

packages/errors/src/index.ts


10. packages/errors/src/__tests__/errors.test.ts 🧪 Tests +41/-0

Errors package unit tests

packages/errors/src/tests/errors.test.ts


11. packages/errors/package.json ⚙️ Configuration changes +45/-0

Errors package metadata and dependencies

packages/errors/package.json


12. packages/errors/tsconfig.json ⚙️ Configuration changes +27/-0

Errors package TypeScript configuration

packages/errors/tsconfig.json


13. packages/metrics/src/index.ts ✨ Enhancement +33/-0

Metrics emitter factory and namespace wrapper

packages/metrics/src/index.ts


14. packages/metrics/src/__tests__/metrics.test.ts 🧪 Tests +18/-0

Metrics package unit tests

packages/metrics/src/tests/metrics.test.ts


15. packages/metrics/package.json ⚙️ Configuration changes +37/-0

Metrics package metadata and dependencies

packages/metrics/package.json


16. packages/metrics/tsconfig.json ⚙️ Configuration changes +27/-0

Metrics package TypeScript configuration

packages/metrics/tsconfig.json


17. packages/queue/src/index.ts ✨ Enhancement +120/-0

Memory and FlowFn queue adapter implementations

packages/queue/src/index.ts


18. packages/queue/src/__tests__/queue.test.ts 🧪 Tests +60/-0

Queue adapters unit tests

packages/queue/src/tests/queue.test.ts


19. packages/queue/package.json ⚙️ Configuration changes +45/-0

Queue package metadata and dependencies

packages/queue/package.json


20. packages/queue/tsconfig.json ⚙️ Configuration changes +27/-0

Queue package TypeScript configuration

packages/queue/tsconfig.json


21. packages/webhooks/src/index.ts ✨ Enhancement +172/-0

Webhook signing, verification, and delivery logic

packages/webhooks/src/index.ts


22. packages/webhooks/src/__tests__/webhooks.test.ts 🧪 Tests +55/-0

Webhooks package unit tests

packages/webhooks/src/tests/webhooks.test.ts


23. packages/webhooks/package.json ⚙️ Configuration changes +45/-0

Webhooks package metadata and dependencies

packages/webhooks/package.json


24. packages/webhooks/tsconfig.json ⚙️ Configuration changes +27/-0

Webhooks package TypeScript configuration

packages/webhooks/tsconfig.json


Grey Divider

Qodo Logo

@qodo-code-review
Copy link
Copy Markdown

qodo-code-review bot commented Apr 4, 2026

Code Review by Qodo

🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0) 🖥 UI issues (0) 🎨 UX Issues (0)

Grey Divider


Action required

1. Lax int env parsing🐞
Description
readIntEnv accepts non-integer strings (e.g. "123abc", "1.5") because it only checks
parseInt + isFinite, causing silent misconfiguration instead of throwing ConfigError. This
differs from the repo’s existing env parsing which verifies the entire string is an integer.
Code

packages/config/src/index.ts[R21-24]

+  const parsed = Number.parseInt(raw, 10);
+  if (!Number.isFinite(parsed)) {
+    throw new ConfigError('ENV_VALUE_INVALID', `${key} must be an integer`);
+  }
Evidence
In the new package, parseInt results like 123 from "123abc" pass Number.isFinite, so the
function returns a value instead of throwing. The existing env helper in clifn/core demonstrates
the intended stricter behavior by requiring Number.isInteger(parsed) and `String(parsed) ===
raw.trim()` before accepting the value.

packages/config/src/index.ts[21-24]
clifn/core/src/env.ts[68-73]

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

## Issue description
`packages/config`'s `readIntEnv` currently accepts partially-numeric strings because it only checks `Number.isFinite(parseInt(...))`. This can silently accept invalid env values.
## Issue Context
There is an existing stricter implementation in `clifn/core/src/env.ts` that rejects values unless the entire trimmed string matches an integer.
## Fix Focus Areas
- packages/config/src/index.ts[17-35]
- packages/config/src/__tests__/config.test.ts[1-39]
- clifn/core/src/env.ts[59-74]

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


2. Defaults ignore bounds🐞
Description
readIntEnv returns defaultValue immediately when the env var is missing/empty and never applies
min/max checks to that default. This allows out-of-range defaults to bypass the bounds contract.
Code

packages/config/src/index.ts[R17-19]

+  if (raw === undefined || raw === '') {
+    return options.defaultValue;
+  }
Evidence
The early return on missing/empty env skips all bounds checks (which only run after parsing raw).
The existing clifn/core implementation validates defaults via validateInt, which also enforces
min/max.

packages/config/src/index.ts[17-32]
clifn/core/src/env.ts[59-74]
clifn/core/src/env.ts[96-110]

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

## Issue description
When an env var is missing/empty, `readIntEnv` returns `defaultValue` without validating it against `min`/`max` (or integer-ness). This breaks the bounds contract.
## Issue Context
`clifn/core/src/env.ts` shows a pattern that validates defaults and parsed values through a shared validator.
## Fix Focus Areas
- packages/config/src/index.ts[11-35]
- packages/config/src/__tests__/config.test.ts[1-39]
- clifn/core/src/env.ts[59-74]

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


3. dequeueMatching always null🐞
Description
FlowFnQueueAdapter.dequeueMatching iterates over this.peek(queueName), but
FlowFnQueueAdapter.peek always returns [], so dequeueMatching never calls the remote dequeue
API and always returns null. This makes the interface method non-functional for the FlowFn
adapter.
Code

packages/queue/src/index.ts[R99-115]

+  async dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null> {
+    const queued = this.peek(queueName);
+    for (const payload of queued) {
+      if (matcher(payload)) {
+        const claimed = await this.dequeue(queueName);
+        if (claimed) {
+          return claimed;
+        }
+      }
+    }
+
+    return null;
+  }
+
+  peek(_queueName: string): TPayload[] {
+    return [];
+  }
Evidence
dequeueMatching depends on peek for candidate payloads, but peek is hard-coded to return an
empty array in the FlowFn adapter implementation, so the loop never executes and the method always
returns null.

packages/queue/src/index.ts[99-115]

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

## Issue description
`FlowFnQueueAdapter.dequeueMatching` cannot work because `peek()` is a stub returning `[]`. Callers will silently fail to match/dequeue jobs.
## Issue Context
If FlowFn does not support peek/match semantics, this method should not pretend to work.
## Fix Focus Areas
- packages/queue/src/index.ts[99-120]
- packages/queue/src/__tests__/queue.test.ts[1-60]

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


View more (2)
4. Queue name path injection🐞
Description
FlowFnQueueAdapter interpolates queueName directly into request URLs without encoding, so names
containing / or .. can alter the request path and hit unintended endpoints. This is a
path-injection risk if queueName can be influenced by untrusted input.
Code

packages/queue/src/index.ts[R64-85]

+  async enqueue(queueName: string, payload: TPayload): Promise<void> {
+    const response = await this.fetcher(`${this.config.baseUrl}/queues/${queueName}/enqueue`, {
+      method: 'POST',
+      headers: {
+        'Content-Type': 'application/json',
+        Authorization: `Bearer ${this.config.apiKey}`,
+      },
+      body: JSON.stringify(payload),
+    });
+
+    if (!response.ok) {
+      throw new Error(`FlowFn enqueue failed: ${response.status} ${response.statusText}`);
+    }
+  }
+
+  async dequeue(queueName: string): Promise<TPayload | null> {
+    const response = await this.fetcher(`${this.config.baseUrl}/queues/${queueName}/dequeue`, {
+      method: 'POST',
+      headers: {
+        Authorization: `Bearer ${this.config.apiKey}`,
+      },
+    });
Evidence
Both enqueue and dequeue construct URLs via string interpolation with queueName embedded in the
path, and there is no encodeURIComponent or URL builder usage to prevent path manipulation.

packages/queue/src/index.ts[64-85]

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

## Issue description
`queueName` is inserted into the URL path without encoding, allowing path manipulation (e.g., slashes / dot segments) and potentially reaching unintended endpoints.
## Issue Context
This affects both enqueue and dequeue URL construction.
## Fix Focus Areas
- packages/queue/src/index.ts[64-85]

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


5. Webhook stringify crash🐞
Description
deliverWebhook calls JSON.stringify(input.payload) outside the retry loop’s try/catch, so
non-serializable payloads throw immediately and the function does not return a structured `{ ok,
attempts }` result. This breaks the function’s contract and bypasses retry/attempt reporting.
Code

packages/webhooks/src/index.ts[R87-100]

+  const fetchFn = options.fetch ?? globalThis.fetch;
+  const maxRetries = Math.max(1, options.maxRetries ?? 3);
+  const initialDelayMs = Math.max(0, options.initialDelayMs ?? 1000);
+  const maxDelayMs = Math.max(initialDelayMs, options.maxDelayMs ?? 30_000);
+
+  const payloadString = JSON.stringify(input.payload);
+  const headers: Record<string, string> = {
+    'Content-Type': 'application/json',
+    ...(input.headers ?? {}),
+  };
+
+  if (input.secret) {
+    headers[input.signatureHeader ?? 'X-Webhook-Signature'] = signWebhookPayload(payloadString, input.secret);
+  }
Evidence
payloadString is computed before the loop, while the error handling only wraps the fetch call
inside the loop. If JSON.stringify throws (e.g., circular references), no attempt is recorded and
the promise rejects instead of returning a WebhookDeliveryResult.

packages/webhooks/src/index.ts[87-100]
packages/webhooks/src/index.ts[104-138]

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

## Issue description
`deliverWebhook` can throw before any retries/attempts are recorded because `JSON.stringify` happens outside the guarded section.
## Issue Context
Callers expect a `WebhookDeliveryResult` with `attempts` even on failures.
## Fix Focus Areas
- packages/webhooks/src/index.ts[83-151]
- packages/webhooks/src/__tests__/webhooks.test.ts[1-55]

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



Remediation recommended

6. Normalization drops meta🐞
Description
normalizeLegacyEnvelope accepts canonical envelopes (SuccessEnvelope/ErrorEnvelope) but
re-wraps them via ok()/err(), overwriting any existing meta.timestamp. This makes
normalization non-idempotent and can change timestamps unexpectedly.
Code

packages/envelope/src/index.ts[R102-112]

+export function normalizeLegacyEnvelope<T>(
+  envelope: LegacySuccessEnvelope<T> | LegacyErrorEnvelope,
+  options?: { timestamp?: string; defaultStatus?: number; defaultRetryable?: boolean }
+): Envelope<T> {
+  if (envelope.ok) {
+    if ('data' in envelope) {
+      return ok(envelope.data, options);
+    }
+
+    return ok(envelope.result, options);
+  }
Evidence
The type of LegacySuccessEnvelope explicitly includes SuccessEnvelope, so callers can pass
canonical envelopes. The implementation always calls ok(...), and ok() always sets
meta.timestamp to options?.timestamp ?? new Date().toISOString(), discarding any existing
timestamp on a canonical input envelope.

packages/envelope/src/index.ts[27-31]
packages/envelope/src/index.ts[45-52]
packages/envelope/src/index.ts[102-112]

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

## Issue description
`normalizeLegacyEnvelope` is not idempotent for canonical envelopes because it reconstructs envelopes and regenerates `meta.timestamp`.
## Issue Context
`LegacySuccessEnvelope` / `LegacyErrorEnvelope` unions include canonical envelope types, so this function should either (a) return canonical envelopes unchanged, or (b) preserve `meta.timestamp` when `options.timestamp` is not provided.
## Fix Focus Areas
- packages/envelope/src/index.ts[27-31]
- packages/envelope/src/index.ts[45-52]
- packages/envelope/src/index.ts[102-124]
- packages/envelope/src/__tests__/envelope.test.ts[1-41]

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


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

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, and left some high level feedback:

  • In FlowFnQueueAdapter, peek and size are currently hard-coded to return empty/zero, which makes dequeueMatching effectively non-functional; consider either implementing these against the FlowFn API or explicitly throwing Unsupported errors so callers don’t assume matching dequeue semantics that aren’t actually provided.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `FlowFnQueueAdapter`, `peek` and `size` are currently hard-coded to return empty/zero, which makes `dequeueMatching` effectively non-functional; consider either implementing these against the FlowFn API or explicitly throwing `Unsupported` errors so callers don’t assume matching dequeue semantics that aren’t actually provided.

## Individual Comments

### Comment 1
<location path="packages/queue/src/index.ts" line_range="27-21" />
<code_context>
+    return queue.shift() ?? null;
+  }
+
+  async dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null> {
+    const queue = this.queues.get(queueName);
+    if (!queue || queue.length === 0) {
+      return null;
+    }
+
</code_context>
<issue_to_address>
**issue (bug_risk):** FlowFnQueueAdapter.dequeueMatching never matches because peek() always returns an empty array.

Since this behavior effectively disables `dequeueMatching`, consider either:
- Throwing an explicit "not supported" error from `dequeueMatching` for this adapter, or
- Implementing a minimal supported version (e.g., a single `dequeue` followed by applying the matcher) instead of iterating over an empty list.

This makes the limitation/behavior clear to callers and avoids a silent no-op.
</issue_to_address>

### Comment 2
<location path="packages/queue/src/index.ts" line_range="113-114" />
<code_context>
+    return null;
+  }
+
+  peek(_queueName: string): TPayload[] {
+    return [];
+  }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** FlowFnQueueAdapter.peek/size are effectively unimplemented and may surprise callers expecting the QueueAdapter contract.

For this adapter, having `peek` always return `[]` (and `size` always `0`) is likely to cause subtle bugs if callers rely on these methods for control flow or monitoring, assuming they reflect remote state.

If these operations aren’t truly supported by FlowFn, consider either throwing an explicit `Unsupported operation for FlowFnQueueAdapter` error from `peek`/`size`, or updating the adapter contract so these methods are optional and clearly documented as such.

Suggested implementation:

```typescript
  peek(_queueName: string): TPayload[] {
    throw new Error('Unsupported operation for FlowFnQueueAdapter: peek');
  }

```

```typescript
  size(_queueName: string): number {
    throw new Error('Unsupported operation for FlowFnQueueAdapter: size');
  }

```
</issue_to_address>

### Comment 3
<location path="packages/config/src/index.ts" line_range="21-30" />
<code_context>
+    return options.defaultValue;
+  }
+
+  const parsed = Number.parseInt(raw, 10);
+  if (!Number.isFinite(parsed)) {
+    throw new ConfigError('ENV_VALUE_INVALID', `${key} must be an integer`);
+  }
+
+  if (options.min !== undefined && parsed < options.min) {
+    throw new ConfigError('ENV_VALUE_OUT_OF_RANGE', `${key} must be >= ${options.min}`);
+  }
+
+  if (options.max !== undefined && parsed > options.max) {
+    throw new ConfigError('ENV_VALUE_OUT_OF_RANGE', `${key} must be <= ${options.max}`);
+  }
+
+  return parsed;
+}
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Using parseInt allows partially numeric strings (e.g. '10px') to be treated as valid integers.

Because `readIntEnv` uses `Number.parseInt(raw, 10)` and only checks `Number.isFinite(parsed)`, inputs like `'10px'` or `'5.5'` are accepted as `10` and `5`. For stricter behavior, validate the full string (e.g. `/^-?\d+$/`) before parsing, or use `Number(raw)` and then `Number.isInteger(value)`, so non-integer or partially numeric values correctly trigger `ENV_VALUE_INVALID` instead of being coerced.

Suggested implementation:

```typescript
  if (raw === undefined || raw === '') {
    return options.defaultValue;
  }

  const parsed = Number(raw);
  if (!Number.isInteger(parsed)) {
    throw new ConfigError('ENV_VALUE_INVALID', `${key} must be an integer`);
  }

  if (options.min !== undefined && parsed < options.min) {
    throw new ConfigError('ENV_VALUE_OUT_OF_RANGE', `${key} must be >= ${options.min}`);
  }

  if (options.max !== undefined && parsed > options.max) {
    throw new ConfigError('ENV_VALUE_OUT_OF_RANGE', `${key} must be <= ${options.max}`);
  }

  return parsed;
}

  constructor(code: 'ENV_VALUE_INVALID' | 'ENV_VALUE_OUT_OF_RANGE', message: string) {

```

I inferred the surrounding context from the snippet. If the original implementation of `readIntEnv` appears elsewhere in the file (e.g. a previous version using `Number.parseInt`), you should also replace that occurrence in the same way: use `const parsed = Number(raw);` followed by `if (!Number.isInteger(parsed))` and keep the existing range checks. Ensure there is only a single `readIntEnv` definition exported from this file.
</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 packages/queue/src/index.ts
Comment thread packages/queue/src/index.ts Outdated
Comment thread packages/config/src/index.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4981acee16

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/queue/src/index.ts Outdated
Comment on lines +113 to +114
peek(_queueName: string): TPayload[] {
return [];
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Make dequeueMatching non-silent for FlowFn adapter

dequeueMatching is effectively non-functional in FlowFnQueueAdapter because it relies on peek, and this implementation hardcodes peek to return an empty array, so dequeueMatching always returns null even when matching jobs exist remotely. Callers that depend on predicate-based dequeue will silently skip work; this should either be implemented against the backend API or explicitly throw unsupported instead of returning null.

Useful? React with 👍 / 👎.

Comment thread packages/config/src/index.ts Outdated
Comment on lines +21 to +22
const parsed = Number.parseInt(raw, 10);
if (!Number.isFinite(parsed)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate full integer strings in readIntEnv

Using Number.parseInt here accepts malformed values with trailing text (for example, "2000ms" parses as 2000), so invalid environment values can be treated as valid and alter runtime behavior instead of throwing ENV_VALUE_INVALID. Since this helper claims to read integer env values, it should reject non-integer strings before parsing.

Useful? React with 👍 / 👎.

Comment thread packages/webhooks/src/index.ts Outdated
Comment on lines +163 to +164
const decoded = Buffer.from(value, 'base64');
return decoded.length > 0 ? decoded : null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject non-canonical base64 signatures before compare

In the base64 branch, Buffer.from(value, 'base64') is permissive and ignores invalid trailing characters, so a tampered signature like <validDigest>!! can decode to the same bytes and still pass timingSafeEqual when encoding: 'base64' is used. This weakens webhook signature validation in that mode; enforce strict base64 format (including padding) prior to decoding.

Useful? React with 👍 / 👎.

Comment on lines +99 to +115
async dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null> {
const queued = this.peek(queueName);
for (const payload of queued) {
if (matcher(payload)) {
const claimed = await this.dequeue(queueName);
if (claimed) {
return claimed;
}
}
}

return null;
}

peek(_queueName: string): TPayload[] {
return [];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The dequeueMatching method in FlowFnQueueAdapter will never work because it calls this.peek(queueName) which always returns an empty array [] (line 113-115). This means the for loop will never execute and the method will always return null, even if matching items exist in the queue.

This is a critical bug that breaks the functionality of dequeueMatching for the FlowFn adapter. Either:

  1. Implement peek to make an API call to retrieve queued items, or
  2. Document that dequeueMatching is not supported for FlowFn and throw an error:
async dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null> {
  throw new Error('dequeueMatching is not supported for FlowFnQueueAdapter');
}
Suggested change
async dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null> {
const queued = this.peek(queueName);
for (const payload of queued) {
if (matcher(payload)) {
const claimed = await this.dequeue(queueName);
if (claimed) {
return claimed;
}
}
}
return null;
}
peek(_queueName: string): TPayload[] {
return [];
}
async dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null> {
throw new Error('dequeueMatching is not supported for FlowFnQueueAdapter');
}
peek(_queueName: string): TPayload[] {
return [];
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Comment thread packages/config/src/index.ts Outdated
Comment thread packages/config/src/index.ts
Comment thread packages/queue/src/index.ts
Comment thread packages/queue/src/index.ts
Comment thread packages/webhooks/src/index.ts
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

7 issues found across 24 files

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/webhooks/src/index.ts">

<violation number="1" location="packages/webhooks/src/index.ts:92">
P2: Handle `JSON.stringify` failures before signing or sending the webhook body. As written, some valid `unknown` inputs make `deliverWebhook` throw before it can return a `WebhookDeliveryResult`.</violation>

<violation number="2" location="packages/webhooks/src/index.ts:163">
P2: `Buffer.from(value, 'base64')` silently strips non-base64 characters before decoding, so a tampered signature like `<validDigest>!!` decodes to the same bytes as the clean digest and passes `timingSafeEqual`. When `encoding: 'base64'` is used, validate that the input is strictly well-formed base64 (e.g. `/^[A-Za-z0-9+/]*={0,2}$/`) before decoding to prevent signature bypass.</violation>
</file>

<file name="packages/envelope/src/index.ts">

<violation number="1" location="packages/envelope/src/index.ts:39">
P1: `err()` advertises `status` and `retryable` as optional, but omitting either one replaces the caller's error with `INVALID_ENVELOPE_ERROR_SHAPE`. Make the parameters required here, or default them before validation, so valid calls don't silently change error semantics.</violation>

<violation number="2" location="packages/envelope/src/index.ts:108">
P2: Preserve `meta.timestamp` when normalizing an already-canonical envelope. Rebuilding it through `ok()`/`err()` restamps the envelope and changes the original event time on pass-through inputs.</violation>
</file>

<file name="packages/queue/src/index.ts">

<violation number="1" location="packages/queue/src/index.ts:114">
P1: `peek()` always returns an empty array, so `dequeueMatching()` can never match any queued item in the FlowFn adapter.</violation>
</file>

<file name="packages/metrics/src/index.ts">

<violation number="1" location="packages/metrics/src/index.ts:23">
P2: Reject namespaces that start or end with `.`. The current guard still allows malformed names like `recfn..event` through.</violation>
</file>

<file name="packages/config/src/index.ts">

<violation number="1" location="packages/config/src/index.ts:21">
P1: `parseInt()` accepts malformed values like `"1.5"` or `"123abc"`, so this can silently use the wrong configuration instead of rejecting a non-integer env value.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/envelope/src/index.ts
Comment thread packages/queue/src/index.ts Outdated
Comment thread packages/config/src/index.ts Outdated
Comment thread packages/webhooks/src/index.ts Outdated
Comment thread packages/envelope/src/index.ts
Comment thread packages/webhooks/src/index.ts Outdated
Comment thread packages/metrics/src/index.ts Outdated
@greptile-apps
Copy link
Copy Markdown

greptile-apps bot commented Apr 4, 2026

Greptile Summary

This PR introduces 6 new shared runtime packages (config, envelope, errors, metrics, queue, webhooks) to the monorepo's shared package layer. All packages follow a consistent structure — strict tsconfigs, ESM exports, and vitest test suites — and are well-suited to their role as foundational utilities consumed by downstream functions. The errors and config packages are solid with no blocking issues.

Key findings:

  • packages/queue/src/index.tsFlowFnQueueAdapter.dequeueMatching is completely non-functional. It calls this.peek(), which is a stub that always returns [], so the matcher loop body is never entered and the method silently returns null for every call on the remote adapter. No test covers this code path.
  • packages/metrics/src/index.tscreateNamespacedEmitter namespace validation only rejects strings containing .., but a trailing dot (e.g. "foo.") passes the guard and later produces "foo..bar" event names when events are tracked.
  • packages/envelope/src/index.tserr() declares status and retryable as optional in its TypeScript signature, but silently substitutes the caller's error with INVALID_ENVELOPE_ERROR_SHAPE when either is missing — a sharp edge for downstream callers who rely on the error code being preserved.
  • packages/webhooks/src/index.ts — The retry delay formula uses initialDelayMs * attempt (linear backoff) rather than the conventional initialDelayMs * 2 ** (attempt - 1) (exponential backoff).

Confidence Score: 2/5

Not safe to merge — FlowFnQueueAdapter.dequeueMatching is completely non-functional and the metrics namespace validation has a silent correctness gap

Two confirmed logic bugs that cause silent incorrect runtime behavior bring the score down significantly: (1) the queue adapter's dequeueMatching always returning null due to a peek() stub, and (2) the metrics namespace validation allowing trailing/leading dots that produce malformed double-dot event names. The envelope err() API surprise (optional-typed fields that silently replace the caller's error code) adds further risk. The config and errors packages are solid and would score 5/5 independently.

packages/queue/src/index.ts and packages/metrics/src/index.ts require fixes before merge; packages/envelope/src/index.ts needs API clarification or signature hardening

Important Files Changed

Filename Overview
packages/queue/src/index.ts FlowFnQueueAdapter.dequeueMatching is completely non-functional — peek() is a stub returning [] so the matcher loop is never entered; MemoryQueueAdapter is correct
packages/queue/src/tests/queue.test.ts Good MemoryQueueAdapter coverage; FlowFnQueueAdapter.dequeueMatching (the broken method) is not tested
packages/metrics/src/index.ts Namespace validation does not reject trailing/leading dots, allowing malformed double-dot event names to be emitted silently
packages/metrics/src/tests/metrics.test.ts Covers happy path only; missing edge-case tests for trailing/leading-dot namespaces
packages/envelope/src/index.ts err() silently substitutes caller's error with INVALID_ENVELOPE_ERROR_SHAPE when optional-typed fields status/retryable are omitted; legacy conversion helpers are well-designed
packages/envelope/src/tests/envelope.test.ts Good coverage; test at line 21 confirms the silent INVALID_ENVELOPE_ERROR_SHAPE substitution behavior
packages/webhooks/src/index.ts HMAC signing/verification is correct and timing-safe; retry delay uses linear backoff (attempt * delay) instead of exponential
packages/webhooks/src/tests/webhooks.test.ts Good HMAC and retry coverage; 1ms test delays prevent linear vs. exponential backoff distinction from surfacing
packages/errors/src/index.ts Clean error registry factory with SuperfunctionError, createErrorRegistry, and createTypedError; no issues found
packages/errors/src/tests/errors.test.ts Thorough tests covering register, resolve, createTypedError, and ERROR_CODE_UNREGISTERED path
packages/config/src/index.ts Solid env-reading helpers; readIntEnvOrDefault silently swallows all ConfigErrors including ENV_VALUE_INVALID, not just out-of-range
packages/config/src/tests/config.test.ts Well-structured tests covering bounded integer, out-of-range fallback, string, and boolean env reading

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[Downstream Functions] --> B[@superfunctions/config]
    A --> C[@superfunctions/envelope]
    A --> D[@superfunctions/errors]
    A --> E[@superfunctions/metrics]
    A --> F[@superfunctions/queue]
    A --> G[@superfunctions/webhooks]

    D -->|createTypedError uses| C

    F -->|MemoryQueueAdapter| H[In-Process Memory]
    F -->|FlowFnQueueAdapter| I[FlowFn Remote API]
    I -.->|dequeueMatching broken: peek always returns empty| X[❌ Always returns null]

    E -->|createNamespacedEmitter| Y[Event Name Builder]
    Y -.->|trailing dot namespace: foo. + event → foo..event| Z[⚠️ Malformed event names]

    G -->|signWebhookPayload / verifyWebhookSignature| J[node:crypto HMAC]
    G -->|deliverWebhook| K[HTTP POST + Linear Retry]
Loading
Prompt To Fix All With AI
This is a comment left during a code review.
Path: packages/queue/src/index.ts
Line: 99-111

Comment:
**`dequeueMatching` always returns `null` for `FlowFnQueueAdapter`**

`this.peek(queueName)` is called on line 100, but `FlowFnQueueAdapter.peek()` is a stub that unconditionally returns `[]` (lines 113–115). This means the `for` loop body is never entered and `dequeueMatching` silently returns `null` for every call on the remote adapter — regardless of what is actually in the remote queue. There is no test covering this code path.

To confirm: `MemoryQueueAdapter.peek()` correctly returns a copy of the internal queue, so only the remote adapter is broken.

Options to fix:
- **Best-effort approach:** repeatedly call `dequeue`, re-enqueue non-matching items, and return the first match (though not atomic).
- **Server-side filter endpoint:** expose a `/queues/{name}/dequeue/matching` endpoint on FlowFn that keeps the match-and-claim operation atomic.

As-is, any consumer of `QueueAdapter.dequeueMatching` that receives a `FlowFnQueueAdapter` will silently get `null` with no error.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/metrics/src/index.ts
Line: 22-24

Comment:
**Namespace validation misses trailing and leading dots**

The guard `normalized.includes('..')` only rejects strings that already contain a consecutive double-dot, but a namespace with a **trailing dot** (e.g. `"foo."`) passes the check. When that namespace is used to build an event name, the template `` `${normalized}.${event}` `` produces `"foo..bar"` — the exact double-dot form the validation is meant to prevent.

The same applies to a **leading dot**: `".foo"` produces event names like `".foo.bar"`.

Suggested fix:
```suggestion
  const normalized = namespace.trim();
  if (!normalized || normalized.includes('..') || normalized.startsWith('.') || normalized.endsWith('.')) {
    throw new Error('METRIC_NAMESPACE_INVALID');
  }
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/envelope/src/index.ts
Line: 55-85

Comment:
**`err()` silently substitutes the caller's error when `status`/`retryable` are omitted**

The TypeScript signature marks `status` and `retryable` as *optional*:
```ts
input: { code: string; message: string; status?: number; retryable?: boolean; ... }
```
However, if either field is absent, `isValidErrorShape` returns `false` and the function silently discards the caller's error, returning a *different* envelope with `code: 'INVALID_ENVELOPE_ERROR_SHAPE'`. A caller who writes:
```ts
err({ code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests' })
```
gets back an envelope whose `error.code` is `'INVALID_ENVELOPE_ERROR_SHAPE'`, not `'RATE_LIMIT_EXCEEDED'`. This silent substitution is confirmed by the test at `envelope.test.ts:21-26` and is very easy to miss in production.

Recommended options:
- Make `status` and `retryable` **required** fields (compile-time safety, minimal change).
- Apply safe defaults (e.g. `status ?? 500`, `retryable ?? false`) similar to what `normalizeLegacyEnvelope` already does on lines 118–119.
- Throw explicitly so failures are never silent.

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/webhooks/src/index.ts
Line: 141

Comment:
**Linear backoff instead of exponential backoff**

`initialDelayMs * attempt` produces **linear** growth (1 s → 2 s → 3 s…). Standard exponential backoff (`initialDelayMs * 2 ** (attempt - 1)`) grows 1 s → 2 s → 4 s…, providing much stronger back-pressure against a struggling upstream. The existence of `maxDelayMs` as a cap strongly implies exponential growth was the intent.

```suggestion
      const delay = Math.min(maxDelayMs, initialDelayMs * 2 ** (attempt - 1));
```

How can I resolve this? If you propose a fix, please make it concise.

Reviews (1): Last reviewed commit: "SF-30 add runtime shared packages" | Re-trigger Greptile

Comment on lines +99 to +111
async dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null> {
const queued = this.peek(queueName);
for (const payload of queued) {
if (matcher(payload)) {
const claimed = await this.dequeue(queueName);
if (claimed) {
return claimed;
}
}
}

return null;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P0 dequeueMatching always returns null for FlowFnQueueAdapter

this.peek(queueName) is called on line 100, but FlowFnQueueAdapter.peek() is a stub that unconditionally returns [] (lines 113–115). This means the for loop body is never entered and dequeueMatching silently returns null for every call on the remote adapter — regardless of what is actually in the remote queue. There is no test covering this code path.

To confirm: MemoryQueueAdapter.peek() correctly returns a copy of the internal queue, so only the remote adapter is broken.

Options to fix:

  • Best-effort approach: repeatedly call dequeue, re-enqueue non-matching items, and return the first match (though not atomic).
  • Server-side filter endpoint: expose a /queues/{name}/dequeue/matching endpoint on FlowFn that keeps the match-and-claim operation atomic.

As-is, any consumer of QueueAdapter.dequeueMatching that receives a FlowFnQueueAdapter will silently get null with no error.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/queue/src/index.ts
Line: 99-111

Comment:
**`dequeueMatching` always returns `null` for `FlowFnQueueAdapter`**

`this.peek(queueName)` is called on line 100, but `FlowFnQueueAdapter.peek()` is a stub that unconditionally returns `[]` (lines 113–115). This means the `for` loop body is never entered and `dequeueMatching` silently returns `null` for every call on the remote adapter — regardless of what is actually in the remote queue. There is no test covering this code path.

To confirm: `MemoryQueueAdapter.peek()` correctly returns a copy of the internal queue, so only the remote adapter is broken.

Options to fix:
- **Best-effort approach:** repeatedly call `dequeue`, re-enqueue non-matching items, and return the first match (though not atomic).
- **Server-side filter endpoint:** expose a `/queues/{name}/dequeue/matching` endpoint on FlowFn that keeps the match-and-claim operation atomic.

As-is, any consumer of `QueueAdapter.dequeueMatching` that receives a `FlowFnQueueAdapter` will silently get `null` with no error.

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +22 to +24
const normalized = namespace.trim();
if (!normalized || normalized.includes('..')) {
throw new Error('METRIC_NAMESPACE_INVALID');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Namespace validation misses trailing and leading dots

The guard normalized.includes('..') only rejects strings that already contain a consecutive double-dot, but a namespace with a trailing dot (e.g. "foo.") passes the check. When that namespace is used to build an event name, the template `${normalized}.${event}` produces "foo..bar" — the exact double-dot form the validation is meant to prevent.

The same applies to a leading dot: ".foo" produces event names like ".foo.bar".

Suggested fix:

Suggested change
const normalized = namespace.trim();
if (!normalized || normalized.includes('..')) {
throw new Error('METRIC_NAMESPACE_INVALID');
const normalized = namespace.trim();
if (!normalized || normalized.includes('..') || normalized.startsWith('.') || normalized.endsWith('.')) {
throw new Error('METRIC_NAMESPACE_INVALID');
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/metrics/src/index.ts
Line: 22-24

Comment:
**Namespace validation misses trailing and leading dots**

The guard `normalized.includes('..')` only rejects strings that already contain a consecutive double-dot, but a namespace with a **trailing dot** (e.g. `"foo."`) passes the check. When that namespace is used to build an event name, the template `` `${normalized}.${event}` `` produces `"foo..bar"` — the exact double-dot form the validation is meant to prevent.

The same applies to a **leading dot**: `".foo"` produces event names like `".foo.bar"`.

Suggested fix:
```suggestion
  const normalized = namespace.trim();
  if (!normalized || normalized.includes('..') || normalized.startsWith('.') || normalized.endsWith('.')) {
    throw new Error('METRIC_NAMESPACE_INVALID');
  }
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +55 to +85
export function err(
input: {
code: string;
message: string;
status?: number;
retryable?: boolean;
details?: Record<string, unknown>;
},
options?: { timestamp?: string }
): ErrorEnvelope {
if (!isValidErrorShape(input)) {
return {
ok: false,
error: {
code: 'INVALID_ENVELOPE_ERROR_SHAPE',
message: 'Envelope error shape is invalid; status and retryable are required.',
status: 500,
retryable: false,
details: {
received: {
code: input.code,
hasStatus: typeof input.status === 'number',
hasRetryable: typeof input.retryable === 'boolean',
},
},
},
meta: {
timestamp: options?.timestamp ?? new Date().toISOString(),
},
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 err() silently substitutes the caller's error when status/retryable are omitted

The TypeScript signature marks status and retryable as optional:

input: { code: string; message: string; status?: number; retryable?: boolean; ... }

However, if either field is absent, isValidErrorShape returns false and the function silently discards the caller's error, returning a different envelope with code: 'INVALID_ENVELOPE_ERROR_SHAPE'. A caller who writes:

err({ code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests' })

gets back an envelope whose error.code is 'INVALID_ENVELOPE_ERROR_SHAPE', not 'RATE_LIMIT_EXCEEDED'. This silent substitution is confirmed by the test at envelope.test.ts:21-26 and is very easy to miss in production.

Recommended options:

  • Make status and retryable required fields (compile-time safety, minimal change).
  • Apply safe defaults (e.g. status ?? 500, retryable ?? false) similar to what normalizeLegacyEnvelope already does on lines 118–119.
  • Throw explicitly so failures are never silent.
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/envelope/src/index.ts
Line: 55-85

Comment:
**`err()` silently substitutes the caller's error when `status`/`retryable` are omitted**

The TypeScript signature marks `status` and `retryable` as *optional*:
```ts
input: { code: string; message: string; status?: number; retryable?: boolean; ... }
```
However, if either field is absent, `isValidErrorShape` returns `false` and the function silently discards the caller's error, returning a *different* envelope with `code: 'INVALID_ENVELOPE_ERROR_SHAPE'`. A caller who writes:
```ts
err({ code: 'RATE_LIMIT_EXCEEDED', message: 'Too many requests' })
```
gets back an envelope whose `error.code` is `'INVALID_ENVELOPE_ERROR_SHAPE'`, not `'RATE_LIMIT_EXCEEDED'`. This silent substitution is confirmed by the test at `envelope.test.ts:21-26` and is very easy to miss in production.

Recommended options:
- Make `status` and `retryable` **required** fields (compile-time safety, minimal change).
- Apply safe defaults (e.g. `status ?? 500`, `retryable ?? false`) similar to what `normalizeLegacyEnvelope` already does on lines 118–119.
- Throw explicitly so failures are never silent.

How can I resolve this? If you propose a fix, please make it concise.

Comment thread packages/webhooks/src/index.ts Outdated
}

if (attempt < maxRetries) {
const delay = Math.min(maxDelayMs, initialDelayMs * attempt);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Linear backoff instead of exponential backoff

initialDelayMs * attempt produces linear growth (1 s → 2 s → 3 s…). Standard exponential backoff (initialDelayMs * 2 ** (attempt - 1)) grows 1 s → 2 s → 4 s…, providing much stronger back-pressure against a struggling upstream. The existence of maxDelayMs as a cap strongly implies exponential growth was the intent.

Suggested change
const delay = Math.min(maxDelayMs, initialDelayMs * attempt);
const delay = Math.min(maxDelayMs, initialDelayMs * 2 ** (attempt - 1));
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/webhooks/src/index.ts
Line: 141

Comment:
**Linear backoff instead of exponential backoff**

`initialDelayMs * attempt` produces **linear** growth (1 s → 2 s → 3 s…). Standard exponential backoff (`initialDelayMs * 2 ** (attempt - 1)`) grows 1 s → 2 s → 4 s…, providing much stronger back-pressure against a struggling upstream. The existence of `maxDelayMs` as a cap strongly implies exponential growth was the intent.

```suggestion
      const delay = Math.min(maxDelayMs, initialDelayMs * 2 ** (attempt - 1));
```

How can I resolve this? If you propose a fix, please make it concise.

@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 4, 2026

Warning

Review the following alerts detected in dependencies.

According to your organization's Security Policy, it is recommended to resolve "Warn" alerts. Learn more about Socket for GitHub.

Action Severity Alert  (click "▶" to expand/collapse)
Warn High
Obfuscated code: npm svelte is 91.0% likely obfuscated

Confidence: 0.91

Location: Package overview

From: datafn/svelte/package.jsonnpm/svelte@4.2.20

ℹ Read more on: This package | This alert | What is obfuscated code?

Next steps: Take a moment to review the security alert above. Review the linked package source code to understand the potential risk. Ensure the package is not malicious before proceeding. If you're unsure how to proceed, reach out to your security team or ask the Socket team for help at support@socket.dev.

Suggestion: Packages should not obfuscate their code. Consider not using packages with obfuscated code.

Mark the package as acceptable risk. To ignore this alert only in this pull request, reply with the comment @SocketSecurity ignore npm/svelte@4.2.20. You can also ignore all packages with @SocketSecurity ignore-all. To ignore an alert for all future pull requests, use Socket's Dashboard to change the triage state of this alert.

View full report

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

4 issues found across 5 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/envelope/src/index.ts">

<violation number="1" location="packages/envelope/src/index.ts:59">
P2: This type-only change removes `err()`'s runtime invalid-shape handling. JS or other untyped callers can now emit canonical error envelopes with `status`/`retryable` left undefined instead of getting `INVALID_ENVELOPE_ERROR_SHAPE`.</violation>
</file>

<file name="packages/queue/src/index.ts">

<violation number="1" location="packages/queue/src/index.ts:112">
P2: FlowFnQueueAdapter still implements `QueueAdapter`, but these methods now always throw. That breaks the shared adapter contract and turns generic `QueueAdapter` calls into runtime failures.</violation>
</file>

<file name="packages/webhooks/src/index.ts">

<violation number="1" location="packages/webhooks/src/index.ts:94">
P2: Handle `JSON.stringify` returning `undefined`; those payloads bypass this new failure path and can still be sent as a non-JSON body.</violation>

<violation number="2" location="packages/webhooks/src/index.ts:178">
P2: This canonicalization rejects valid byte-equivalent base64 signatures, so verification can now fail for callers that omit padding.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/envelope/src/index.ts Outdated
async dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null> {
void matcher;
encodeQueueName(queueName);
throw new Error('FLOWFN_QUEUE_MATCHING_UNSUPPORTED');
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot Apr 4, 2026

Choose a reason for hiding this comment

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

P2: FlowFnQueueAdapter still implements QueueAdapter, but these methods now always throw. That breaks the shared adapter contract and turns generic QueueAdapter calls into runtime failures.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/queue/src/index.ts, line 112:

<comment>FlowFnQueueAdapter still implements `QueueAdapter`, but these methods now always throw. That breaks the shared adapter contract and turns generic `QueueAdapter` calls into runtime failures.</comment>

<file context>
@@ -97,24 +107,18 @@ export class FlowFnQueueAdapter<TPayload> implements QueueAdapter<TPayload> {
-    return null;
+    void matcher;
+    encodeQueueName(queueName);
+    throw new Error('FLOWFN_QUEUE_MATCHING_UNSUPPORTED');
   }
 
</file context>
Fix with Cubic

Comment thread packages/webhooks/src/index.ts Outdated
Comment thread packages/webhooks/src/index.ts
Comment on lines +60 to +75
export function readStringEnv(
key: string,
options: { defaultValue: string; allowEmpty?: boolean; env?: NodeJS.ProcessEnv }
): string {
const env = options.env ?? process.env;
const raw = env[key];
if (raw === undefined) {
return options.defaultValue;
}

if (!options.allowEmpty && raw.trim().length === 0) {
throw new ConfigError('ENV_VALUE_INVALID', `${key} must not be empty`);
}

return raw;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Inconsistent behavior: readStringEnv doesn't treat empty string the same as undefined, while readIntEnv and readBooleanEnv both return the default value when raw === ''. This inconsistency will cause unexpected errors in production.

For example:

  • readIntEnv('KEY', { env: { KEY: '' }, defaultValue: 100 }) returns 100
  • readBooleanEnv('KEY', { env: { KEY: '' }, defaultValue: true }) returns true
  • readStringEnv('KEY', { env: { KEY: '' }, defaultValue: 'x' }) throws ConfigError

Fix: Change line 66 to match the other functions:

if (raw === undefined || raw === '') {
  return options.defaultValue;
}
Suggested change
export function readStringEnv(
key: string,
options: { defaultValue: string; allowEmpty?: boolean; env?: NodeJS.ProcessEnv }
): string {
const env = options.env ?? process.env;
const raw = env[key];
if (raw === undefined) {
return options.defaultValue;
}
if (!options.allowEmpty && raw.trim().length === 0) {
throw new ConfigError('ENV_VALUE_INVALID', `${key} must not be empty`);
}
return raw;
}
export function readStringEnv(
key: string,
options: { defaultValue: string; allowEmpty?: boolean; env?: NodeJS.ProcessEnv }
): string {
const env = options.env ?? process.env;
const raw = env[key];
if (raw === undefined || raw === '') {
return options.defaultValue;
}
if (!options.allowEmpty && raw.trim().length === 0) {
throw new ConfigError('ENV_VALUE_INVALID', `${key} must not be empty`);
}
return raw;
}

Spotted by Graphite

Fix in Graphite


Is this helpful? React 👍 or 👎 to let us know.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 6

🧹 Nitpick comments (3)
packages/config/src/__tests__/config.test.ts (1)

5-33: Add max-bound coverage for integer env validation.

Current tests only exercise min; please add max (and optionally min+max together) so upper-bound regressions are caught.

💡 Suggested test additions
 describe('config', () => {
@@
   it('can fallback deterministically using readIntEnvOrDefault', () => {
@@
   });
+
+  it('throws ENV_VALUE_OUT_OF_RANGE for value above max', () => {
+    expect(() =>
+      readIntEnv('RECFN_RATE_LIMIT_WINDOW_MS', {
+        env: { RECFN_RATE_LIMIT_WINDOW_MS: '6000' },
+        defaultValue: 1000,
+        min: 100,
+        max: 5000,
+      })
+    ).toThrowError(ConfigError);
+  });
+
+  it('falls back to default when value is above max', () => {
+    expect(
+      readIntEnvOrDefault('RECFN_RATE_LIMIT_WINDOW_MS', {
+        env: { RECFN_RATE_LIMIT_WINDOW_MS: '6000' },
+        defaultValue: 1000,
+        min: 100,
+        max: 5000,
+      })
+    ).toBe(1000);
+  });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/config/src/__tests__/config.test.ts` around lines 5 - 33, Tests only
assert min-bound behavior for integer env validation; add cases covering the max
bound so upper-limit regressions are caught. Update the test file to include at
least one case calling readIntEnv('RECFN_RATE_LIMIT_WINDOW_MS', { env: {...},
defaultValue: X, max: Y }) that returns a value within the max, another case
that passes a value greater than max and expects a ConfigError (like the
existing min out-of-range test), and mirror those for readIntEnvOrDefault to
ensure fallback respects max; reference the existing test patterns around
readIntEnv, readIntEnvOrDefault and ConfigError to implement these additions.
packages/errors/src/__tests__/errors.test.ts (1)

20-27: Avoid double-invoking resolve in the unknown-code test.

You can assert type and code from one thrown instance to keep this test tighter.

♻️ Simplified assertion pattern
   it('throws ERROR_CODE_UNREGISTERED for unknown codes', () => {
     const registry = createErrorRegistry();
-
-    expect(() => registry.resolve('UNKNOWN_CODE')).toThrowError(SuperfunctionError);
-    try {
-      registry.resolve('UNKNOWN_CODE');
-    } catch (error) {
-      const typed = error as SuperfunctionError;
-      expect(typed.code).toBe('ERROR_CODE_UNREGISTERED');
-    }
+    let caught: unknown;
+    try {
+      registry.resolve('UNKNOWN_CODE');
+    } catch (error) {
+      caught = error;
+    }
+    expect(caught).toBeInstanceOf(SuperfunctionError);
+    expect((caught as SuperfunctionError).code).toBe('ERROR_CODE_UNREGISTERED');
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/errors/src/__tests__/errors.test.ts` around lines 20 - 27, The test
currently calls registry.resolve('UNKNOWN_CODE') twice; change it to invoke
resolve only once and assert on that single thrown instance: remove the
expect(() => registry.resolve('UNKNOWN_CODE')).toThrowError call and keep a
single try/catch around registry.resolve('UNKNOWN_CODE'), cast the caught error
to SuperfunctionError and assert its .code equals 'ERROR_CODE_UNREGISTERED' (and
optionally fail the test if no error is thrown) so you both verify the throw and
inspect the thrown object's properties in one invocation.
packages/envelope/src/__tests__/envelope.test.ts (1)

28-34: Consider explicit assertions for better test clarity.

The && pattern in the assertions works (returns data if ok is truthy) but is subtle. Separate assertions would be clearer and provide better failure messages.

✨ Clearer assertion pattern
   it('normalizes legacy {ok,result} and {ok,data} envelopes', () => {
     const resultEnvelope = normalizeLegacyEnvelope({ ok: true, result: { a: 1 } }, { timestamp: 't1' });
     const dataEnvelope = normalizeLegacyEnvelope({ ok: true, data: { a: 2 } }, { timestamp: 't2' });

-    expect(resultEnvelope.ok && resultEnvelope.data).toEqual({ a: 1 });
-    expect(dataEnvelope.ok && dataEnvelope.data).toEqual({ a: 2 });
+    expect(resultEnvelope.ok).toBe(true);
+    expect(resultEnvelope.ok && resultEnvelope.data).toEqual({ a: 1 });
+    expect(dataEnvelope.ok).toBe(true);
+    expect(dataEnvelope.ok && dataEnvelope.data).toEqual({ a: 2 });
   });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envelope/src/__tests__/envelope.test.ts` around lines 28 - 34, The
test uses a subtle `ok && data` expression; update the assertions in the test
for normalizeLegacyEnvelope so they explicitly assert the boolean and the
payload separately: for resultEnvelope and dataEnvelope first assert
resultEnvelope.ok/dataEnvelope.ok (e.g., toBe(true) or truthy) and then assert
resultEnvelope.data and dataEnvelope.data equal the expected objects; locate the
assertions referencing normalizeLegacyEnvelope, resultEnvelope and dataEnvelope
in the envelope.test.ts and replace the combined `ok && data` checks with two
explicit expect statements each.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/config/src/index.ts`:
- Around line 49-57: readIntEnvOrDefault currently swallows all errors and
returns options.defaultValue without validating it; update the catch to rethrow
non-ConfigError exceptions and to validate options.defaultValue against the same
constraints as readIntEnv (min/max and integer) before returning it. Concretely:
in readIntEnvOrDefault catch the error as e, if !(e instanceof ConfigError)
throw e; then validate options.defaultValue (check it's an integer and within
options.min/options.max) and return it if valid, otherwise throw a new
ConfigError describing the invalid default; keep references to readIntEnv,
readIntEnvOrDefault, ConfigError and options.defaultValue.

In `@packages/envelope/src/index.ts`:
- Around line 11-17: Update the CanonicalEnvelopeError type to widen details
from Record<string, unknown> to unknown so it accepts producers that set
details?: unknown; also change the conversion/normalization helpers that
currently check envelope.error.details using truthiness (the checks around
envelope.error.details in the conversion helpers) to use explicit undefined
checks (details !== undefined) so falsy-but-intentional values like false, 0, or
"" are preserved. Locate the CanonicalEnvelopeError type and the functions that
read envelope.error.details in this file and make these two targeted changes.

In `@packages/metrics/package.json`:
- Around line 1-37: The release-packages.json is missing the new package names
so CI won't pick them up; add the new package identifiers
"@superfunctions/metrics", "@superfunctions/envelope", "@superfunctions/queue",
"@superfunctions/config", "@superfunctions/errors", and
"@superfunctions/webhooks" to the release-packages.json package list (the
array/object that enumerates publishable/workspace packages) so they are
included in CI/CD builds, release tagging, and workspace dependency resolution.

In `@packages/queue/src/index.ts`:
- Around line 9-49: MemoryQueueAdapter uses raw queueName strings so names like
" jobs " or "../jobs" differ from FlowFnQueueAdapter; update MemoryQueueAdapter
(methods enqueue, dequeue, dequeueMatching, peek, size) to call the same
normalization/validation function encodeQueueName on the incoming queueName
before using it as a Map key, import or reference encodeQueueName as used by
FlowFnQueueAdapter, and ensure any validation errors from encodeQueueName are
propagated so behavior matches the FlowFn adapter.
- Around line 51-55: FlowFnQueueAdapterConfig and the fetch calls in enqueue and
dequeue need abort/timeout support: add an optional timeoutMs?: number to
FlowFnQueueAdapterConfig, create an AbortController inside enqueue and dequeue,
start a timer that calls controller.abort() after config.timeoutMs (or a
sensible default) and pass controller.signal to fetch, and clear the timer when
fetch resolves; ensure any per-request override can be accepted by adding an
optional { timeoutMs } param to enqueue/dequeue signatures if needed and
propagate it to the AbortController so requests can be cancelled on timeout.

In `@packages/webhooks/src/index.ts`:
- Around line 76-80: Add a per-attempt timeout signal to WebhookDeliveryOptions
and only retry on transport errors or transient HTTP statuses: extend
WebhookDeliveryOptions with a perAttemptTimeoutMs?: number (and keep existing
fetch, maxRetries, initialDelayMs, maxDelayMs), then in the fetch retry loop
create a fresh AbortController for each attempt, derive a timeout setTimeout
that calls controller.abort() after perAttemptTimeoutMs, pass controller.signal
to the fetch call, and clear the timeout after response; when deciding to retry,
treat thrown errors during fetch as retryable, and treat only HTTP 429 and 5xx
responses as retryable—do not retry on 4xx permanent statuses like 400, 403,
404—respect maxRetries and backoff (initialDelayMs/maxDelayMs) as before.

---

Nitpick comments:
In `@packages/config/src/__tests__/config.test.ts`:
- Around line 5-33: Tests only assert min-bound behavior for integer env
validation; add cases covering the max bound so upper-limit regressions are
caught. Update the test file to include at least one case calling
readIntEnv('RECFN_RATE_LIMIT_WINDOW_MS', { env: {...}, defaultValue: X, max: Y
}) that returns a value within the max, another case that passes a value greater
than max and expects a ConfigError (like the existing min out-of-range test),
and mirror those for readIntEnvOrDefault to ensure fallback respects max;
reference the existing test patterns around readIntEnv, readIntEnvOrDefault and
ConfigError to implement these additions.

In `@packages/envelope/src/__tests__/envelope.test.ts`:
- Around line 28-34: The test uses a subtle `ok && data` expression; update the
assertions in the test for normalizeLegacyEnvelope so they explicitly assert the
boolean and the payload separately: for resultEnvelope and dataEnvelope first
assert resultEnvelope.ok/dataEnvelope.ok (e.g., toBe(true) or truthy) and then
assert resultEnvelope.data and dataEnvelope.data equal the expected objects;
locate the assertions referencing normalizeLegacyEnvelope, resultEnvelope and
dataEnvelope in the envelope.test.ts and replace the combined `ok && data`
checks with two explicit expect statements each.

In `@packages/errors/src/__tests__/errors.test.ts`:
- Around line 20-27: The test currently calls registry.resolve('UNKNOWN_CODE')
twice; change it to invoke resolve only once and assert on that single thrown
instance: remove the expect(() => registry.resolve('UNKNOWN_CODE')).toThrowError
call and keep a single try/catch around registry.resolve('UNKNOWN_CODE'), cast
the caught error to SuperfunctionError and assert its .code equals
'ERROR_CODE_UNREGISTERED' (and optionally fail the test if no error is thrown)
so you both verify the throw and inspect the thrown object's properties in one
invocation.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 86fbd538-7367-4ef3-8fd2-c492e0355f4c

📥 Commits

Reviewing files that changed from the base of the PR and between e105c93 and 0792a03.

📒 Files selected for processing (24)
  • packages/config/package.json
  • packages/config/src/__tests__/config.test.ts
  • packages/config/src/index.ts
  • packages/config/tsconfig.json
  • packages/envelope/package.json
  • packages/envelope/src/__tests__/envelope.test.ts
  • packages/envelope/src/index.ts
  • packages/envelope/tsconfig.json
  • packages/errors/package.json
  • packages/errors/src/__tests__/errors.test.ts
  • packages/errors/src/index.ts
  • packages/errors/tsconfig.json
  • packages/metrics/package.json
  • packages/metrics/src/__tests__/metrics.test.ts
  • packages/metrics/src/index.ts
  • packages/metrics/tsconfig.json
  • packages/queue/package.json
  • packages/queue/src/__tests__/queue.test.ts
  • packages/queue/src/index.ts
  • packages/queue/tsconfig.json
  • packages/webhooks/package.json
  • packages/webhooks/src/__tests__/webhooks.test.ts
  • packages/webhooks/src/index.ts
  • packages/webhooks/tsconfig.json

Comment thread packages/config/src/index.ts
Comment on lines +11 to +17
export type CanonicalEnvelopeError = {
code: string;
message: string;
status: number;
retryable: boolean;
details?: Record<string, unknown>;
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's examine the envelope index.ts file at the specified lines
cat -n packages/envelope/src/index.ts | head -160

Repository: 21nCo/super-functions

Length of output: 4976


🏁 Script executed:

# Check if the producer files exist and examine their details types
git ls-files | grep -E "(datafn|searchfn).*errors\.ts"

Repository: 21nCo/super-functions

Length of output: 446


🏁 Script executed:

# Search for CanonicalEnvelopeError and related types to understand the full context
rg -n "CanonicalEnvelopeError|LegacyEnvelopeError" packages/envelope/src/

Repository: 21nCo/super-functions

Length of output: 204


🏁 Script executed:

# Search for the conversion helpers mentioned in the comment
rg -n "toLegacyDataEnvelope|toLegacyResultEnvelope|normalizeLegacyEnvelope" packages/envelope/src/

Repository: 21nCo/super-functions

Length of output: 1156


🏁 Script executed:

# Check datafn/server/src/http/errors.ts
cat -n datafn/server/src/http/errors.ts | head -100

Repository: 21nCo/super-functions

Length of output: 1239


🏁 Script executed:

# Check searchfn/server/src/http/errors.ts
cat -n searchfn/server/src/http/errors.ts | head -100

Repository: 21nCo/super-functions

Length of output: 3364


🏁 Script executed:

# Look for details type usage in these files
rg -n "details" datafn/server/src/http/errors.ts searchfn/server/src/http/errors.ts -A 2 -B 2

Repository: 21nCo/super-functions

Length of output: 1852


Widen details from Record<string, unknown> to unknown for compatibility with existing producers.

The types in this file require details?: Record<string, unknown>, but datafn/server/src/http/errors.ts and searchfn/server/src/http/errors.ts both produce envelopes with details?: unknown. This type mismatch forces consuming code to cast when normalizing these envelopes. Additionally, the conversion helpers at lines 136 and 153 use truthiness checks (envelope.error.details ?), which will discard falsy-but-defined values like false, 0, or empty strings. Switch these to details !== undefined to preserve all intentional values.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/envelope/src/index.ts` around lines 11 - 17, Update the
CanonicalEnvelopeError type to widen details from Record<string, unknown> to
unknown so it accepts producers that set details?: unknown; also change the
conversion/normalization helpers that currently check envelope.error.details
using truthiness (the checks around envelope.error.details in the conversion
helpers) to use explicit undefined checks (details !== undefined) so
falsy-but-intentional values like false, 0, or "" are preserved. Locate the
CanonicalEnvelopeError type and the functions that read envelope.error.details
in this file and make these two targeted changes.

Comment thread packages/metrics/package.json
Comment thread packages/queue/src/index.ts
Comment thread packages/queue/src/index.ts
Comment thread packages/webhooks/src/index.ts
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
packages/queue/src/index.ts (1)

64-68: ⚠️ Potential issue | 🔴 Critical

Add per-request timeout/abort for FlowFn HTTP calls.

enqueue and dequeue (Line 83 and Line 99) still await fetch without cancellation. A stalled endpoint can block function execution indefinitely.

⏱️ Proposed patch (timeout + AbortController)
 export interface FlowFnQueueAdapterConfig {
   baseUrl: string;
   apiKey: string;
   fetch?: typeof globalThis.fetch;
+  timeoutMs?: number;
 }
 
 function encodeQueueName(queueName: string): string {
   return encodeURIComponent(normalizeQueueName(queueName));
 }
 
 export class FlowFnQueueAdapter<TPayload> implements QueueAdapter<TPayload> {
   private readonly fetcher: typeof globalThis.fetch;
+  private static readonly DEFAULT_TIMEOUT_MS = 10_000;
 
   constructor(private readonly config: FlowFnQueueAdapterConfig) {
     this.fetcher = config.fetch ?? globalThis.fetch;
   }
+
+  private async fetchWithTimeout(input: string, init: RequestInit): Promise<Response> {
+    const timeoutMs = this.config.timeoutMs ?? FlowFnQueueAdapter.DEFAULT_TIMEOUT_MS;
+    const controller = new AbortController();
+    const timer = setTimeout(() => controller.abort(), timeoutMs);
+    try {
+      return await this.fetcher(input, { ...init, signal: controller.signal });
+    } finally {
+      clearTimeout(timer);
+    }
+  }
 
   async enqueue(queueName: string, payload: TPayload): Promise<void> {
     const encodedQueueName = encodeQueueName(queueName);
-    const response = await this.fetcher(`${this.config.baseUrl}/queues/${encodedQueueName}/enqueue`, {
+    const response = await this.fetchWithTimeout(`${this.config.baseUrl}/queues/${encodedQueueName}/enqueue`, {
       method: 'POST',
       headers: {
         'Content-Type': 'application/json',
         Authorization: `Bearer ${this.config.apiKey}`,
       },
       body: JSON.stringify(payload),
     });
@@
   async dequeue(queueName: string): Promise<TPayload | null> {
     const encodedQueueName = encodeQueueName(queueName);
-    const response = await this.fetcher(`${this.config.baseUrl}/queues/${encodedQueueName}/dequeue`, {
+    const response = await this.fetchWithTimeout(`${this.config.baseUrl}/queues/${encodedQueueName}/dequeue`, {
       method: 'POST',
       headers: {
         Authorization: `Bearer ${this.config.apiKey}`,
       },
     });
#!/bin/bash
set -euo pipefail

echo "Inspect FlowFn queue config and fetch call sites:"
cat -n packages/queue/src/index.ts | sed -n '64,115p'

echo
echo "Verify timeout/abort symbols are absent/present:"
rg -n -C2 'timeoutMs|AbortController|signal\s*:' packages/queue/src/index.ts

Also applies to: 81-90, 97-104

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/queue/src/index.ts` around lines 64 - 68, The enqueue and dequeue
implementations are calling the injected fetch without any per-request
timeout/abort, so add an optional timeoutMs (number) to FlowFnQueueAdapterConfig
and use AbortController when calling fetch in enqueue and dequeue (pass
controller.signal to fetch), starting a setTimeout to call controller.abort()
after timeoutMs (or a sensible default) and clear the timer on completion;
ensure errors from an aborted fetch are handled/propagated consistently in
enqueue and dequeue.
🧹 Nitpick comments (1)
packages/queue/src/index.ts (1)

34-35: Delete empty queues after removal to avoid stale Map growth.

After shift/splice, empty arrays remain keyed in queues. Cleaning them up keeps memory bounded when many transient queue names are used.

🧹 Proposed cleanup
   async dequeue(queueName: string): Promise<TPayload | null> {
@@
-    return queue.shift() ?? null;
+    const payload = queue.shift() ?? null;
+    if (queue.length === 0) {
+      this.queues.delete(normalizedQueueName);
+    }
+    return payload;
   }
@@
   async dequeueMatching(queueName: string, matcher: (payload: TPayload) => boolean): Promise<TPayload | null> {
@@
-    const [payload] = queue.splice(index, 1);
-    return payload ?? null;
+    const [payload] = queue.splice(index, 1);
+    if (queue.length === 0) {
+      this.queues.delete(normalizedQueueName);
+    }
+    return payload ?? null;
   }

Also applies to: 49-50

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/queue/src/index.ts` around lines 34 - 35, When removing items from a
per-name queue, clean up the backing Map entry if the array becomes empty to
avoid unbounded growth: after the queue.shift() call in the function that
returns queue.shift() ?? null (and similarly after any queue.splice(...) removal
around the other case at lines 49-50), check if queue.length === 0 and call
queues.delete(name) to remove the empty array from the queues Map; update the
code paths that use queue.shift() and queue.splice(...) to perform this
post-removal cleanup using the existing queues Map and the same name/key used to
look up the array.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/config/src/index.ts`:
- Around line 63-77: readStringEnv currently returns options.defaultValue when
the env key is missing or when raw is '' without validating the default; this
allows an empty or whitespace-only default to bypass allowEmpty: false. Update
readStringEnv (the function named readStringEnv) to validate
options.defaultValue when returning it: if options.allowEmpty is false ensure
the defaultValue is neither empty string nor only whitespace and throw
ConfigError('ENV_VALUE_INVALID', `${key} must not be empty`) if it is; keep the
existing validation for raw values (the existing raw.trim() check) and apply the
same rule to the default path so empty defaults cannot slip through.

---

Duplicate comments:
In `@packages/queue/src/index.ts`:
- Around line 64-68: The enqueue and dequeue implementations are calling the
injected fetch without any per-request timeout/abort, so add an optional
timeoutMs (number) to FlowFnQueueAdapterConfig and use AbortController when
calling fetch in enqueue and dequeue (pass controller.signal to fetch), starting
a setTimeout to call controller.abort() after timeoutMs (or a sensible default)
and clear the timer on completion; ensure errors from an aborted fetch are
handled/propagated consistently in enqueue and dequeue.

---

Nitpick comments:
In `@packages/queue/src/index.ts`:
- Around line 34-35: When removing items from a per-name queue, clean up the
backing Map entry if the array becomes empty to avoid unbounded growth: after
the queue.shift() call in the function that returns queue.shift() ?? null (and
similarly after any queue.splice(...) removal around the other case at lines
49-50), check if queue.length === 0 and call queues.delete(name) to remove the
empty array from the queues Map; update the code paths that use queue.shift()
and queue.splice(...) to perform this post-removal cleanup using the existing
queues Map and the same name/key used to look up the array.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 3e7df2c6-95a9-4f5a-8894-bf989a400f52

📥 Commits

Reviewing files that changed from the base of the PR and between 0792a03 and 0912aab.

📒 Files selected for processing (4)
  • packages/config/src/__tests__/config.test.ts
  • packages/config/src/index.ts
  • packages/queue/src/__tests__/queue.test.ts
  • packages/queue/src/index.ts
✅ Files skipped from review due to trivial changes (2)
  • packages/config/src/tests/config.test.ts
  • packages/queue/src/tests/queue.test.ts

Comment thread packages/config/src/index.ts Outdated
Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/config/src/index.ts">

<violation number="1" location="packages/config/src/index.ts:69">
P2: `readStringEnv` returns `options.defaultValue` without checking it against the `allowEmpty` constraint. If a caller passes `{ defaultValue: '', allowEmpty: false }` and the env var is missing or empty, the function silently returns `''`—contradicting the intent of `allowEmpty: false`. This is inconsistent with the analogous `readIntEnvOrDefault` fix in this PR, which now validates the default through `validateIntValue`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/config/src/index.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (3)
packages/queue/src/index.ts (2)

137-152: Timeout abort throws generic error.

When fetchWithTimeout aborts due to timeout, the thrown error is a generic AbortError (or DOMException with name AbortError). Callers cannot distinguish between a timeout and other abort reasons without inspecting error properties.

Consider wrapping the abort error with a more descriptive message for easier debugging:

♻️ Optional: Wrap timeout errors with context
 private async fetchWithTimeout(url: string, init: RequestInit): Promise<Response> {
   const controller = typeof AbortController === 'function' ? new AbortController() : undefined;
   const timeoutId =
     controller && this.timeoutMs > 0 ? setTimeout(() => controller.abort(), this.timeoutMs) : undefined;

   try {
     return await this.fetcher(url, {
       ...init,
       signal: controller?.signal,
     });
+  } catch (error) {
+    if (error instanceof Error && error.name === 'AbortError' && timeoutId !== undefined) {
+      throw new Error(`FlowFn request timed out after ${this.timeoutMs}ms`);
+    }
+    throw error;
   } finally {
     if (timeoutId) {
       clearTimeout(timeoutId);
     }
   }
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/queue/src/index.ts` around lines 137 - 152, fetchWithTimeout
currently lets an AbortError bubble up on timeout which is indistinguishable
from other aborts; modify fetchWithTimeout to catch errors from this.fetcher,
detect an abort-related error (e.g., error.name === 'AbortError' or instanceof
DOMException) and rethrow a new, descriptive timeout error (e.g., "Fetch to
<url> timed out after <this.timeoutMs>ms" or a custom TimeoutError) while
preserving the original error as the cause; for non-abort errors rethrow them
unchanged. Reference fetchWithTimeout, controller, timeoutId, this.timeoutMs and
this.fetcher when making the change.

117-118: Consider adding runtime validation of response shape.

The type assertion as { payload?: TPayload } assumes the server response conforms to the expected structure. If the server returns an unexpected shape (e.g., { data: { payload: ... } } or an error object), this could cause subtle bugs downstream.

For a more defensive approach, consider validating the response structure before accessing data.payload, or documenting the expected server contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/queue/src/index.ts` around lines 117 - 118, The current code casts
the JSON result from response.json() to { payload?: TPayload } and directly
returns data.payload, which can hide malformed responses; change this to
validate the parsed JSON at runtime before accessing data.payload: after calling
response.json() check that the result is an object (not null) and that it either
has a 'payload' property or matches the expected payload shape (e.g., typeof
payload checks or a small shape-guard for TPayload), then return the payload or
null/throw a descriptive error; update the logic around response.json(), the
local variable data, and the return path so callers of data.payload get either a
validated TPayload or a clear failure.
packages/webhooks/src/index.ts (1)

126-128: Consider exposing signature options for delivery.

When input.secret is provided, signWebhookPayload is called with default options (sha256, hex, no prefix). Users cannot customize the algorithm, encoding, or prefix during delivery without manually signing and passing the header.

This is fine for an MVP, but consider adding an optional signatureOptions?: WebhookSignatureOptions field to WebhookDeliveryInput if consumers need to integrate with services requiring different signature formats (e.g., sha1= prefix for legacy GitHub webhooks).

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/webhooks/src/index.ts` around lines 126 - 128, The delivery
currently always calls signWebhookPayload(payloadString, input.secret) when
input.secret exists, preventing callers from customizing
algorithm/encoding/prefix; add an optional signatureOptions?:
WebhookSignatureOptions property to the WebhookDeliveryInput type and pass it
through to signWebhookPayload (use input.signatureOptions) and still honor
input.signatureHeader for the header name; update the call site in
packages/webhooks/src/index.ts (where signWebhookPayload is invoked) to supply
the new options parameter and ensure WebhookSignatureOptions describes
algorithm, encoding, and prefix so consumers can request e.g. a "sha1=" prefix.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/queue/src/index.ts`:
- Around line 137-152: fetchWithTimeout currently lets an AbortError bubble up
on timeout which is indistinguishable from other aborts; modify fetchWithTimeout
to catch errors from this.fetcher, detect an abort-related error (e.g.,
error.name === 'AbortError' or instanceof DOMException) and rethrow a new,
descriptive timeout error (e.g., "Fetch to <url> timed out after
<this.timeoutMs>ms" or a custom TimeoutError) while preserving the original
error as the cause; for non-abort errors rethrow them unchanged. Reference
fetchWithTimeout, controller, timeoutId, this.timeoutMs and this.fetcher when
making the change.
- Around line 117-118: The current code casts the JSON result from
response.json() to { payload?: TPayload } and directly returns data.payload,
which can hide malformed responses; change this to validate the parsed JSON at
runtime before accessing data.payload: after calling response.json() check that
the result is an object (not null) and that it either has a 'payload' property
or matches the expected payload shape (e.g., typeof payload checks or a small
shape-guard for TPayload), then return the payload or null/throw a descriptive
error; update the logic around response.json(), the local variable data, and the
return path so callers of data.payload get either a validated TPayload or a
clear failure.

In `@packages/webhooks/src/index.ts`:
- Around line 126-128: The delivery currently always calls
signWebhookPayload(payloadString, input.secret) when input.secret exists,
preventing callers from customizing algorithm/encoding/prefix; add an optional
signatureOptions?: WebhookSignatureOptions property to the WebhookDeliveryInput
type and pass it through to signWebhookPayload (use input.signatureOptions) and
still honor input.signatureHeader for the header name; update the call site in
packages/webhooks/src/index.ts (where signWebhookPayload is invoked) to supply
the new options parameter and ensure WebhookSignatureOptions describes
algorithm, encoding, and prefix so consumers can request e.g. a "sha1=" prefix.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 62a3d931-d449-43bb-b429-8b2b6676eac7

📥 Commits

Reviewing files that changed from the base of the PR and between 0912aab and 378b678.

📒 Files selected for processing (6)
  • packages/config/src/__tests__/config.test.ts
  • packages/config/src/index.ts
  • packages/queue/src/__tests__/queue.test.ts
  • packages/queue/src/index.ts
  • packages/webhooks/src/__tests__/webhooks.test.ts
  • packages/webhooks/src/index.ts
✅ Files skipped from review due to trivial changes (3)
  • packages/queue/src/tests/queue.test.ts
  • packages/config/src/tests/config.test.ts
  • packages/config/src/index.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/webhooks/src/tests/webhooks.test.ts

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 378b67868b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

"url": "git+https://github.com/21nCo/super-functions.git",
"directory": "packages/config"
},
"publishConfig": { "access": "public" },
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Add release target for new publishable workspace

This package is declared as publicly publishable (publishConfig.access = "public"), but no matching entry was added to release-packages.json for it (or the other newly added runtime packages). The publish workflow (.github/workflows/publish-tag.yml) resolves tags through scripts/resolve-release-tag.mjs, which only accepts slugs present in that file, so release tags for these new packages will fail with No publishable workspace found and cannot be published to npm until release targets are registered.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 821727de37

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/queue/src/index.ts Outdated

async enqueue(queueName: string, payload: TPayload): Promise<void> {
const encodedQueueName = encodeQueueName(queueName);
const response = await this.fetchWithTimeout(`${this.config.baseUrl}/queues/${encodedQueueName}/enqueue`, {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Normalize FlowFn base URL before path concatenation

The adapter builds request URLs by directly concatenating baseUrl with "/queues/...", so a common configuration like baseUrl: "https://flowfn.example.com/" produces https://flowfn.example.com//queues/.... On routers that do not normalize double slashes, enqueue/dequeue calls can return 404 and make the queue adapter appear broken. Normalizing trailing slashes once in the constructor (or using URL) avoids this config-sensitive failure mode.

Useful? React with 👍 / 👎.

Comment thread packages/webhooks/src/index.ts Outdated
Comment on lines +121 to +124
const headers: Record<string, string> = {
'Content-Type': 'application/json',
...(input.headers ?? {}),
};
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Canonicalize header names before merging webhook headers

headers is assembled with object spread, which is case-sensitive for keys. If callers pass headers like content-type or x-webhook-signature, this object can contain both that key and the package’s Content-Type/X-Webhook-Signature; Fetch then normalizes and combines them into comma-separated values, yielding invalid Content-Type or a signature header that fails verification. This causes delivery failures for otherwise valid inputs when caller header casing differs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0e27c87e99

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/webhooks/src/index.ts Outdated
Comment on lines +90 to +93
const existingKey = findHeaderKey(headers, headerName);
if (existingKey && existingKey !== headerName) {
delete headers[existingKey];
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Drop all case variants before setting canonical header

setCanonicalHeader only deletes the first case-insensitive match, so when headers already contains both the canonical key and another case variant (for example after merging multiple header maps), the extra variant remains and fetch/Headers coalesces them into comma-separated values. This can still produce invalid Content-Type or signature headers during webhook delivery even after canonicalization; remove all case-insensitive duplicates except the canonical key before assigning the final value.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/webhooks/src/index.ts">

<violation number="1" location="packages/webhooks/src/index.ts:90">
P2: `setCanonicalHeader()` only removes one matching casing variant, so duplicate header names can still be sent and get merged into an invalid value by `fetch()`.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/webhooks/src/index.ts Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (2)
packages/webhooks/src/index.ts (2)

97-105: ⚠️ Potential issue | 🟠 Major

Fail fast on invalid delivery inputs before retrying.

If fetch is unavailable or the URL is malformed, the function only discovers that inside the retry loop and then classifies it as retryable. Those are permanent local failures, so the extra backoff just adds latency with no chance of recovery.

Run this to verify the malformed-URL case. Expected result: hasStatus is false, which means the current shouldRetryWebhookError() path will retry it.

#!/bin/bash
set -e
sed -n '97,105p;263,269p' packages/webhooks/src/index.ts

node - <<'NODE'
(async () => {
  if (typeof fetch !== 'function') {
    console.log('global fetch is unavailable in this Node runtime');
    process.exit(0);
  }

  try {
    await fetch('not a url');
  } catch (error) {
    console.log({
      name: error?.name,
      hasStatus: Boolean(error && typeof error === 'object' && 'status' in error),
      message: error?.message,
    });
  }
})();
NODE

Also applies to: 263-268


142-148: ⚠️ Potential issue | 🟠 Major

Don't let an empty secret silently disable signing.

if (input.secret) conflates “no secret provided” with “misconfigured empty secret”. In the latter case this sends an unsigned webhook instead of failing fast, which is a risky default for an auth path.

💡 Safer guard
-  if (input.secret) {
+  if (input.secret !== undefined) {
+    if (input.secret.length === 0) {
+      return {
+        ok: false,
+        attempts: [{ attempt: 1, ok: false, error: 'Webhook secret cannot be empty' }],
+      };
+    }
     setCanonicalHeader(
       headers,
       input.signatureHeader ?? 'X-Webhook-Signature',
       signWebhookPayload(payloadString, input.secret)
     );
   }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/webhooks/src/index.ts` around lines 142 - 148, The current guard if
(input.secret) lets an empty string silently skip signing; update the logic
around input.secret used with signWebhookPayload and setCanonicalHeader to
explicitly distinguish "no secret provided" (undefined/null) from a
misconfigured empty secret: if input.secret is undefined/null continue without
signing, but if it's an empty or all-whitespace string throw an error (fail
fast) so signing is not silently disabled; ensure you still use
input.signatureHeader, payloadString and headers when calling
signWebhookPayload/setCanonicalHeader.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@packages/webhooks/src/index.ts`:
- Around line 136-140: The current logic preserves any caller-supplied
Content-Type even though the body is always JSON.stringify(input.payload);
change the code so the request always sends a JSON content type: remove the
branch that preserves a non-JSON type and instead always call
setCanonicalHeader(headers, 'Content-Type', 'application/json') (or set
headers['Content-Type']='application/json' when
findHeaderKey(headers,'Content-Type') is absent) so that the Content-Type header
for the JSON body is unambiguously 'application/json'; update usages of
findHeaderKey and setCanonicalHeader accordingly.

---

Duplicate comments:
In `@packages/webhooks/src/index.ts`:
- Around line 142-148: The current guard if (input.secret) lets an empty string
silently skip signing; update the logic around input.secret used with
signWebhookPayload and setCanonicalHeader to explicitly distinguish "no secret
provided" (undefined/null) from a misconfigured empty secret: if input.secret is
undefined/null continue without signing, but if it's an empty or all-whitespace
string throw an error (fail fast) so signing is not silently disabled; ensure
you still use input.signatureHeader, payloadString and headers when calling
signWebhookPayload/setCanonicalHeader.
🪄 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: defaults

Review profile: CHILL

Plan: Pro

Run ID: 31ab3cba-4c4f-42db-93c0-aeb3e8567bfb

📥 Commits

Reviewing files that changed from the base of the PR and between 821727d and 0e27c87.

📒 Files selected for processing (4)
  • packages/queue/src/__tests__/queue.test.ts
  • packages/queue/src/index.ts
  • packages/webhooks/src/__tests__/webhooks.test.ts
  • packages/webhooks/src/index.ts
✅ Files skipped from review due to trivial changes (1)
  • packages/webhooks/src/tests/webhooks.test.ts

Comment thread packages/webhooks/src/index.ts Outdated
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f1423f5225

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/webhooks/src/index.ts Outdated
Comment on lines +254 to +255
const normalizedInput = value.replace(/=+$/, '');
const normalizedDecoded = decoded.toString('base64').replace(/=+$/, '');
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject over-padded base64 signatures in verifier

The base64 decoding path still accepts non-canonical over-padding because normalizedInput removes all trailing = before equality checking, so a valid digest can be altered and still verify. Fresh evidence on this commit: verifyWebhookSignature(payload, signature + '=', secret, { encoding: 'base64' }) returns true for a valid SHA-256 signature that was tampered by adding one extra =. This means modified signature headers can pass verification instead of being rejected as malformed.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: e49c4d815c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/queue/src/index.ts Outdated
return null;
}

return queue.shift() ?? null;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Preserve nullable payloads when dequeuing

dequeue uses queue.shift() ?? null, which collapses a legitimate queued payload of null (and undefined when TPayload allows it) into the same return value used for an empty queue. In that case the job is removed but callers interpret it as “no message,” so work can be silently skipped; the same nullish-coalescing pattern is also present in dequeueMatching and FlowFnQueueAdapter.dequeue.

Useful? React with 👍 / 👎.

@cubic-dev-ai
Copy link
Copy Markdown

cubic-dev-ai bot commented Apr 7, 2026

You're iterating quickly on this pull request. To help protect your rate limits, cubic has paused automatic reviews on new pushes for now—when you're ready for another review, comment @cubic-dev-ai review.

Comment thread packages/queue/src/index.ts Fixed
Comment thread packages/queue/src/__tests__/queue.test.ts Fixed
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: fe2f30355d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/webhooks/src/index.ts Outdated
options: WebhookDeliveryOptions = {}
): Promise<WebhookDeliveryResult> {
const fetchFn = options.fetch ?? globalThis.fetch;
const maxRetries = Math.max(1, options.maxRetries ?? 3);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reject non-finite maxRetries before retry loop

deliverWebhook computes maxRetries with Math.max(1, options.maxRetries ?? 3), but Math.max returns NaN when callers pass NaN (for example from Number(process.env.WEBHOOK_MAX_RETRIES)). With maxRetries = NaN, the for (attempt <= maxRetries) loop never runs, so the function exits with attempts: [] and sends no webhook at all; this is a silent delivery failure that should be prevented by validating maxRetries as finite before clamping.

Useful? React with 👍 / 👎.

Comment on lines +142 to +143
const existingContentType = headers[findHeaderKey(headers, 'Content-Type') ?? ''] ?? undefined;
const normalizedContentType =
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Respect latest Content-Type variant when canonicalizing

Content type normalization takes the first case-insensitive header match, so when merged headers contain multiple Content-Type case variants, an earlier stale value can override a later intended value. For example, content-type: text/plain followed by Content-Type: application/cloudevents+json will be normalized from the first entry and then canonicalized, dropping the later override; this can send the wrong media type to webhook consumers.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@cubic-dev-ai cubic-dev-ai bot left a comment

Choose a reason for hiding this comment

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

1 issue found across 4 files (changes from recent commits).

Prompt for AI agents (unresolved issues)

Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.


<file name="packages/queue/src/index.ts">

<violation number="1" location="packages/queue/src/index.ts:124">
P2: This branch does not actually preserve `undefined` payloads for the FlowFn adapter, because the transport is still JSON and JSON cannot carry `undefined` values.</violation>
</file>

Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.

Comment thread packages/queue/src/index.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0045d8de4b

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/config/src/index.ts Outdated
value: number,
options: { min?: number; max?: number }
): number {
if (!Number.isInteger(value)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject integers outside JS safe range

readIntEnv currently parses with Number.parseInt and then validates with Number.isInteger, which accepts rounded values beyond Number.MAX_SAFE_INTEGER. For example, an env value like "9007199254740993" is coerced to 9007199254740992 and treated as valid, silently changing configuration instead of raising ENV_VALUE_INVALID. This can produce incorrect runtime behavior when large numeric envs are used; the validator should require safe integers (or use BigInt parsing with explicit bounds).

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: f36d4116be

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/queue/src/index.ts Outdated

constructor(private readonly config: FlowFnQueueAdapterConfig) {
this.fetcher = config.fetch ?? globalThis.fetch;
this.timeoutMs = Math.max(0, config.timeoutMs ?? 15_000);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate FlowFn timeoutMs as a finite number

FlowFnQueueAdapter clamps timeoutMs with Math.max, but Math.max(0, NaN) yields NaN, so misconfigured values (for example Number(process.env.FLOWFN_TIMEOUT_MS) when unset/invalid) silently disable request timeouts because this.timeoutMs > 0 becomes false in fetchWithTimeout. In that case stalled enqueue/dequeue calls can hang indefinitely instead of aborting, which is a production reliability regression for bad env input.

Useful? React with 👍 / 👎.

Comment thread packages/webhooks/src/index.ts Outdated
Comment on lines +110 to +112
const initialDelayMs = Math.max(0, options.initialDelayMs ?? 1000);
const maxDelayMs = Math.max(initialDelayMs, options.maxDelayMs ?? 30_000);
const perAttemptTimeoutMs = Math.max(0, options.perAttemptTimeoutMs ?? 15_000);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject non-finite webhook timing options before clamping

initialDelayMs, maxDelayMs, and perAttemptTimeoutMs are also normalized with Math.max but never checked for finiteness, so NaN propagates (Math.max(0, NaN) => NaN). This makes per-attempt abort timeouts never start (NaN > 0 is false) and can collapse retry backoff to effectively immediate retries, causing hangs or retry storms under env/config parse errors instead of failing fast with a clear validation error.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5e598b7db6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/webhooks/src/index.ts Outdated
: 'application/json';
setCanonicalHeader(headers, 'Content-Type', normalizedContentType);

if (input.secret) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Do not silently skip signing for empty secrets

The signature header is only added when input.secret is truthy, so secret: "" is treated the same as no secret and the webhook is sent unsigned. This is a silent security/compatibility failure when secrets come from env/config and end up empty, because callers who intended signed delivery will instead emit unverifiable requests. Treat an empty secret as invalid (throw) or sign whenever the field is present (input.secret !== undefined).

Useful? React with 👍 / 👎.

@socket-security
Copy link
Copy Markdown

socket-security bot commented Apr 12, 2026

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9510eeec7

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/envelope/src/index.ts Outdated
Comment on lines +100 to +101
if ('data' in envelope) {
return envelope;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Check ok before treating envelopes as success

normalizeLegacyEnvelope currently classifies an input as a success envelope based only on the presence of data, without checking the ok discriminator. If upstream code sends an error payload that also includes a data field (for example { ok: false, error: {...}, data: null }), this path returns it as success and silently masks the error. This can flip failure handling logic in callers; gate success normalization on ok === true (and error normalization on ok === false) instead of key presence alone.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 23299201cc

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +117 to +118
code: envelope.error.code,
message: envelope.error.message,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Guard missing error payload in legacy normalization

normalizeLegacyEnvelope assumes every non-success input has envelope.error.code/message, so malformed legacy responses like { ok: false } (or { ok: false, error: null }) throw a TypeError instead of returning a normalized error envelope. Because this helper is typically used on external payloads, one bad upstream response can crash normalization paths; add an explicit runtime check for a valid error object before dereferencing its fields.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b8688f563e

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +128 to +129
code: envelope.error.code,
message: envelope.error.message,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate legacy error code/message before normalization

normalizeLegacyEnvelope forwards envelope.error.code and envelope.error.message without runtime validation, and err/isValidErrorShape only checks status and retryable. If a malformed legacy payload like { ok:false, error:{ status:500, retryable:false } } is received, this path emits a “canonical” envelope with code/message as undefined, breaking the package’s own error contract for downstream consumers that expect strings.

Useful? React with 👍 / 👎.

}
setCanonicalHeader(
headers,
input.signatureHeader ?? 'X-Webhook-Signature',
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Reject blank signature header names in webhook delivery

Using input.signatureHeader ?? 'X-Webhook-Signature' treats an empty string as a valid header name, so misconfigurations like signatureHeader: '' produce headers with an invalid key and cause fetch/Headers to throw on every attempt. This leads to guaranteed delivery failures (and unnecessary retries) instead of a clear configuration error or fallback to the default signature header.

Useful? React with 👍 / 👎.

@graphite-app
Copy link
Copy Markdown

graphite-app bot commented Apr 13, 2026

Merge activity

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 6ec8281e73

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +103 to +104
if (envelope.ok === false && 'error' in envelope && isValidErrorShape(envelope.error)) {
return envelope;
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Validate code/message in canonical-envelope fast path

In normalizeLegacyEnvelope, the canonical shortcut returns envelope whenever isValidErrorShape(envelope.error) is true, but that helper only checks status and retryable. A payload like { ok:false, meta:{timestamp:"..."}, error:{ status:500, retryable:false } } is therefore treated as canonical and returned with missing error.code/error.message, violating the declared ErrorEnvelope contract and causing downstream consumers that assume string fields to break.

Useful? React with 👍 / 👎.

## Summary
- add the missing shared runtime packages to `origin/dev`
- include config, envelope, errors, metrics, queue, and webhooks foundations
- keep the PR limited to the shared package layer used by multiple downstream functions

## Validation
- inspected the copied package set in a clean worktree created from `origin/dev`
- did not run a reliable monorepo validation pass in this worktree

## Notes
- no `.conduct` changes included
- opened directly against `origin/dev` with `GH_CONFIG_DIR=~/.config/gh-other`

## Summary by Sourcery

Introduce shared runtime utility packages for envelopes, errors, config, metrics, queues, and webhooks to be reused across Superfunctions services.

New Features:
- Add a canonical response envelope package with helpers for constructing, normalizing, and converting success/error envelopes.
- Add a shared typed error package providing an error registry and strongly-typed SuperfunctionError constructor.
- Add a shared config package exposing typed environment variable readers for ints, strings, and booleans.
- Add a shared metrics package with pluggable and namespaced metrics emitters.
- Add a shared queue package with an in-memory adapter and a FlowFn-backed queue adapter interface.
- Add a shared webhooks package for HMAC signing/verification and retrying webhook delivery.

Build:
- Configure TypeScript build settings and package metadata for the new config, envelope, errors, metrics, queue, and webhooks packages.

<!-- This is an auto-generated description by cubic. -->
---
## Summary by cubic
Adds shared runtime packages for config, envelope, errors, metrics, queue, and webhooks, and hardens envelope normalization and webhook signing/delivery edge cases. Registers these packages for release (SF-30).

- **New Features**
  - `@superfunctions/config`: typed int/string/boolean readers; safe-integer bounds and min/max; validates defaults; empty strings off by default (`allowEmpty` opt‑in); booleans accept true/false, 1/0, yes/no; throws `ENV_VALUE_INVALID`/`ENV_VALUE_OUT_OF_RANGE`.
  - `@superfunctions/envelope`: canonical ok/err with meta timestamp; legacy normalize/convert; fills missing legacy `status`/`retryable` from defaults; guards malformed legacy errors and `ok:false`+`data`; `err()` requires numeric `status` and boolean `retryable` or returns `INVALID_ENVELOPE_ERROR_SHAPE`.
  - `@superfunctions/errors`: registry; `SuperfunctionError` and `createTypedError`; unknown codes throw `ERROR_CODE_UNREGISTERED`.
  - `@superfunctions/metrics`: no‑op emitter; namespaced emitter with strict validation (`METRIC_NAMESPACE_INVALID`).
  - `@superfunctions/queue`: `MemoryQueueAdapter` with name normalization/validation; `FlowFnQueueAdapter` with Bearer auth, per‑request timeouts (`AbortController`), invalid timeout → `FLOWFN_QUEUE_TIMEOUT_INVALID`, trailing‑slash normalization; 404→null on dequeue; preserves explicit `undefined`; `peek`/`size`/`dequeueMatching` unsupported.
  - `@superfunctions/webhooks`: HMAC sign/verify (hex/base64) with strict decoding (accepts missing padding; rejects malformed/non‑canonical) and optional prefix; delivery with retries/backoff, per‑attempt timeout, and validated timing opts (`WEBHOOK_MAX_RETRIES_INVALID`/`WEBHOOK_INITIAL_DELAY_INVALID`/`WEBHOOK_MAX_DELAY_INVALID`/`WEBHOOK_PER_ATTEMPT_TIMEOUT_INVALID`); rejects empty secrets (`WEBHOOK_SECRET_INVALID`) and blank signature header names; no retries on non‑transient 4xx; canonicalizes headers (case‑insensitive de‑dup, prefers latest variant, ensures single `Content-Type` and signature header, forces `application/json` for non‑JSON); validates JSON‑serializable payload; returns structured attempts with final `responseStatus`.

<sup>Written for commit 6ec8281. Summary will update on new commits.</sup>

<!-- End of auto-generated description by cubic. -->

<!-- This is an auto-generated comment: release notes by coderabbit.ai -->
## Summary by CodeRabbit

* **New Features**
  * Configuration helpers for reading/validating env values (ints, strings, booleans).
  * Canonical envelope format with converters for legacy shapes.
  * Error registry and typed error construction utilities.
  * Metrics emitter with optional namespacing.
  * Queue abstraction with in-memory and remote adapters.
  * Webhook signing, verification, and delivery with retries/backoff and timeouts.

* **Tests**
  * Added comprehensive test suites covering the new packages and behaviors.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
@graphite-app graphite-app bot force-pushed the SF-30/runtime-shared-foundations branch from 6ec8281 to 8ff8f78 Compare April 13, 2026 11:56
@graphite-app graphite-app bot merged commit 8ff8f78 into dev Apr 13, 2026
14 checks passed
@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
2 Security Hotspots

See analysis details on SonarQube Cloud

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.

2 participants