Skip to content

HT-9: platform provider interfaces - #4

Merged
zaridan merged 1 commit into
mainfrom
docs/ht-9-provider-interfaces
Jul 10, 2026
Merged

HT-9: platform provider interfaces#4
zaridan merged 1 commit into
mainfrom
docs/ht-9-provider-interfaces

Conversation

@zaridan

@zaridan zaridan commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

Defines the four platform seams the charter commits to (§4 'Vercel-first, not Vercel-only') — queue, scheduler/durable-work, blob storage, inbound email — as TypeScript contracts only. No adapters, no implementations. This is the architecture-locking artifact: it exists so engine code has interfaces to depend on before any code can call a platform SDK directly.

Design points worth a look on review:

  • Cron is a declaration surface, not a runtime call (no long-lived process to call it) — TSDoc says resolving ≠ live-without-deploy.
  • Queue results are a typed union (ack / retry+backoff / deadLetter), not throw/catch.
  • Durable actions are poll-delivered from a Postgres scheduled_actions table by an engine-owned cron tick — runAt is a lower bound.
  • Attachment bytes live in the BlobStore; inbound emails carry contentRefs, not inline bytes (adapter persists before parseWebhook resolves).
  • verifySignature is async (Promise) — Gmail push verifies an OIDC JWT.

Typechecks clean via tsc --noEmit. Note: typescript is not a devDependency yet — wire the typecheck script when HT-6/7/9 merge and the root package.json consolidates (kept off this branch to avoid a merge conflict).

Jira: https://resonantiq.atlassian.net/browse/HT-9

🤖 Generated with Claude Code

https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b

Summary by CodeRabbit

  • New Features

    • Added provider interfaces for queues, scheduling, blob storage, and inbound email.
    • Added support for queued work with retries, deduplication, delayed delivery, and dead-letter handling.
    • Added secure attachment storage with signed URLs and existence checks.
    • Added normalized inbound email and attachment handling contracts.
    • Added centralized provider exports for simpler integration.
  • Documentation

    • Documented provider integration, deployment considerations, and testing guidance.
  • Chores

    • Added strict TypeScript compiler configuration.

Locks the charter §4 'Vercel-first, not Vercel-only' seams before any
engine code can bypass them. Interfaces only — no adapters, no impls:

- queue.ts: QueueProvider (at-least-once enqueue + dedupeKey), push-
  delivered QueueMessage, explicit ack/retry/deadLetter result union
- scheduler.ts: recurring cron (declaration surface) + durable
  scheduled-actions (the scheduled_actions-table + cron-tick pattern)
- blob.ts: BlobStore, signed-URL-only attachment access, per-tenant keys
- inbound-email.ts: NormalizedInboundEmail (provider-agnostic) +
  InboundEmailProvider webhook verify/parse — no IMAP polling
- index.ts: the single import surface for engine modules
- src/providers/README.md, tsconfig.json (strict, src-scoped, noEmit)

Proof-pass fixes: dropped an unused BlobStore import; widened
verifySignature to Promise<boolean> since the first adapter (Gmail
push) verifies an OIDC JWT that may fetch signing certs. Typechecks
clean (tsc --noEmit).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TqG66PPZreBrj17VbAqe3b
@zaridan

zaridan commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@coderabbitai

coderabbitai Bot commented Jul 10, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Introduces provider interfaces for queues, scheduling, blob storage, and inbound email, plus a barrel export, architectural documentation, and strict TypeScript compiler configuration.

Changes

Provider Interface Seams

Layer / File(s) Summary
Queue and scheduling contracts
src/providers/queue.ts, src/providers/scheduler.ts
Defines typed queue messages, handler outcomes, enqueueing, cron registration, delayed actions, and cancellation semantics.
Storage and inbound email contracts
src/providers/blob.ts, src/providers/inbound-email.ts
Defines blob operations and normalized inbound email, attachment, signature verification, and webhook parsing contracts.
Provider exports and project configuration
src/providers/index.ts, src/providers/README.md, tsconfig.json
Adds centralized provider type exports, documents adapter boundaries and testing conventions, and configures strict no-emit TypeScript compilation.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly matches the PR’s main change: introducing platform provider interfaces.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch docs/ht-9-provider-interfaces

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 5

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

Inline comments:
In `@src/providers/blob.ts`:
- Around line 41-48: Update the BlobProvider getSignedUrl contract and every
implementing adapter to validate that expiresInSeconds is finite, strictly
positive, and no greater than a documented maximum TTL; reject invalid values
before provider-specific URL generation, and use the same bound consistently
across all adapters.
- Around line 29-33: Update the blob provider’s put method to keep contentLength
consistent with the materialized data: derive it from data.byteLength or
validate and reject any provided value that differs, ensuring stored metadata
matches the object bytes and NormalizedInboundAttachment.size.

In `@src/providers/queue.ts`:
- Around line 52-68: Update the QueueMessage.id documentation to define it as
the stable logical-message identifier reused across all redeliveries, rather
than unique to a delivery attempt. Clarify in QueueMessage that attempts
represents the delivery-attempt count and should be used for per-attempt
identity, preserving the existing deduplication contract.

In `@src/providers/scheduler.ts`:
- Around line 75-79: Update scheduleAction and its implementation to accept an
optional stable deduplication key or caller-supplied action ID, matching
EnqueueOptions.dedupeKey semantics. Enforce uniqueness during persistence and
return the existing scheduled action ID when the key or ID already exists,
rather than creating a duplicate, while preserving current behavior for calls
without a key.
- Around line 55-73: The scheduler contract documents durable, at-least-once
delivery without exposing the lifecycle needed to implement it. Update the
scheduler interface and its provider implementation around the scheduling method
to define atomic claiming/lease renewal, completion acknowledgement, and failure
rescheduling or retry operations for due actions; ensure claims prevent
concurrent cron ticks from dispatching the same action and preserve
at-least-once semantics. Alternatively, remove these guarantees from the
provider documentation and clearly assign the lifecycle and delivery guarantees
to the adapter-owned contract.
🪄 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 Plus

Run ID: 3b986e5e-8730-4102-832d-48f80b3ae1f7

📥 Commits

Reviewing files that changed from the base of the PR and between 8cc62d1 and 4a5609a.

📒 Files selected for processing (7)
  • src/providers/README.md
  • src/providers/blob.ts
  • src/providers/inbound-email.ts
  • src/providers/index.ts
  • src/providers/queue.ts
  • src/providers/scheduler.ts
  • tsconfig.json

Comment thread src/providers/blob.ts
Comment on lines +29 to +33
put(
key: string,
data: Uint8Array,
opts: { contentType: string; contentLength?: number },
): Promise<void>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

Keep contentLength consistent with the bytes.

Because data is fully materialized, data.byteLength is authoritative. An independent optional length can make stored metadata disagree with the actual object and with NormalizedInboundAttachment.size; reject mismatches or derive the value internally.

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

In `@src/providers/blob.ts` around lines 29 - 33, Update the blob provider’s put
method to keep contentLength consistent with the materialized data: derive it
from data.byteLength or validate and reject any provided value that differs,
ensuring stored metadata matches the object bytes and
NormalizedInboundAttachment.size.

Comment thread src/providers/blob.ts
Comment on lines +41 to +48
/**
* Mint a time-limited, signed URL for reading the object at `key`. The
* URL expires after `expiresInSeconds` and must not be usable
* afterward. This is the only way callers outside the engine (e.g. a
* browser rendering an attachment) ever read blob contents — attachments
* are served via signed URLs, never a public path.
*/
getSignedUrl(key: string, expiresInSeconds: number): Promise<string>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🔒 Security & Privacy | 🟠 Major | 🏗️ Heavy lift

Bound and validate signed URL lifetimes.

expiresInSeconds: number permits NaN, negative values, zero, and unbounded values. Provider-specific handling could produce invalid or excessively long-lived URLs, contradicting the contract that blobs are never publicly readable. Require a finite positive TTL and enforce a documented maximum in every adapter.

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

In `@src/providers/blob.ts` around lines 41 - 48, Update the BlobProvider
getSignedUrl contract and every implementing adapter to validate that
expiresInSeconds is finite, strictly positive, and no greater than a documented
maximum TTL; reject invalid values before provider-specific URL generation, and
use the same bound consistently across all adapters.

Comment thread src/providers/queue.ts
Comment on lines +52 to +68
export interface QueueMessage<T> {
/** Provider-assigned unique id for this delivery attempt's message. */
id: string;

/** The topic/queue name this message was enqueued on. */
topic: string;

/** The payload as originally enqueued. */
payload: T;

/**
* How many times delivery of this message has been attempted, starting
* at `1` for the first delivery. Handlers can use this to implement
* their own retry-count-aware logic (e.g. escalate to dead-letter after
* N attempts) independent of what the platform's own retry policy does.
*/
attempts: number;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Make QueueMessage.id stable across redeliveries.

The module contract requires deduplication using the same QueueMessage.id, but this field is described as unique for “this delivery attempt.” If an adapter generates a new ID per retry, handlers cannot reliably prevent duplicate side effects. Define id as the stable logical-message ID reused on redelivery; use attempts for per-attempt identity.

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

In `@src/providers/queue.ts` around lines 52 - 68, Update the QueueMessage.id
documentation to define it as the stable logical-message identifier reused
across all redeliveries, rather than unique to a delivery attempt. Clarify in
QueueMessage that attempts represents the delivery-attempt count and should be
used for per-attempt identity, preserving the existing deduplication contract.

Comment on lines +55 to +73
/**
* Schedule `action` to run at or after `runAt`, carrying `payload`.
* Returns the id of the scheduled action so it can later be cancelled.
*
* This is the durable-delayed-action pattern the charter names
* alongside cron (a `scheduled_actions` table, polled by a cron tick the
* engine owns): the action must survive redeploys, cold starts, and
* process restarts, because there is no in-memory timer or long-lived
* process holding it. Delivery is via poll, not push: a recurring cron
* tick (registered separately, via `registerCron` or equivalent
* platform config) queries for actions due at-or-before "now" and
* dispatches each to its `handlerRef`. `runAt` is therefore a lower
* bound on execution time, not a precise deadline — actual delivery
* latency is bounded by the polling cron's own interval.
*
* Like `QueueProvider`, this is an at-least-once contract: a poll tick
* that dispatches an action but crashes/times out before marking it
* delivered may cause the next tick to dispatch it again. Handlers MUST
* be idempotent for the same action id.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift

Define the durable-action claim and completion lifecycle.

The documentation promises poll-based, at-least-once delivery backed by scheduled_actions, but the interface exposes no operation to atomically claim/lease due actions, acknowledge completion, or reschedule failures. Without that lifecycle, overlapping cron ticks can dispatch the same row concurrently, or adapter code must bypass this provider seam. Add explicit claim/lease and completion/retry operations, or move these delivery guarantees into a clearly adapter-owned contract.

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

In `@src/providers/scheduler.ts` around lines 55 - 73, The scheduler contract
documents durable, at-least-once delivery without exposing the lifecycle needed
to implement it. Update the scheduler interface and its provider implementation
around the scheduling method to define atomic claiming/lease renewal, completion
acknowledgement, and failure rescheduling or retry operations for due actions;
ensure claims prevent concurrent cron ticks from dispatching the same action and
preserve at-least-once semantics. Alternatively, remove these guarantees from
the provider documentation and clearly assign the lifecycle and delivery
guarantees to the adapter-owned contract.

Comment on lines +75 to +79
scheduleAction<T>(
runAt: Date,
action: HandlerRef,
payload: T,
): Promise<string>;

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Add idempotency to scheduleAction.

If persistence succeeds but the caller times out before receiving the returned ID, retrying scheduleAction can create a second action. Idempotency by action ID does not help because the duplicate receives a different ID. Accept a stable deduplication key or caller-supplied action ID with uniqueness and return-existing semantics, mirroring EnqueueOptions.dedupeKey.

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

In `@src/providers/scheduler.ts` around lines 75 - 79, Update scheduleAction and
its implementation to accept an optional stable deduplication key or
caller-supplied action ID, matching EnqueueOptions.dedupeKey semantics. Enforce
uniqueness during persistence and return the existing scheduled action ID when
the key or ID already exists, rather than creating a duplicate, while preserving
current behavior for calls without a key.

@zaridan
zaridan merged commit 261e73a into main Jul 10, 2026
1 check passed
zaridan added a commit that referenced this pull request Jul 20, 2026
…101)

All four counsel-gate documents were drafted 2026-07-19 (Codex-reviewed,
fix-round applied): module commercial license (legal/, PR #100), terms
of sale / privacy policy / managed-hosting terms (marketplace repo
legal/, PR #4 merged). The gate is now sign-off + adoption/publication,
not drafting — the table said 'Counsel-drafted' as if unwritten.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
@zaridan
zaridan deleted the docs/ht-9-provider-interfaces branch August 2, 2026 19:19
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant