Skip to content

feat: add SeaRates ocean tracking compatibility gateway - #331

Open
dodeja wants to merge 5 commits into
mainfrom
cursor/searates-compat-proxy-a51c
Open

feat: add SeaRates ocean tracking compatibility gateway#331
dodeja wants to merge 5 commits into
mainfrom
cursor/searates-compat-proxy-a51c

Conversation

@dodeja

@dodeja dodeja commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

  • add a dedicated apps/migrate Vercel application for vendor compatibility APIs, separate from the MCP deployment
  • expose SeaRates-compatible GET /searates-api/tracking and GET /searates-api/info/sealines routes
  • translate SeaRates query auth and CT/BL/BK requests onto the public Terminal49 JSON:API
  • normalize shipment, container, location, facility, vessel, route, and event data into SeaRates' documented envelope
  • handle Terminal49's asynchronous tracking lifecycle with bounded polling; while T49 remains pending, return SeaRates' verified successful SEALINE_HASNT_PROVIDE_INFO empty-data outcome

Deployment architecture

The root vercel.json remains MCP-only: /, /mcp, and OAuth discovery routes still target the MCP handlers. It contains no SeaRates functions or rewrites.

Create a second Vercel project in the Terminal49 team from this same repository with:

Setting Value
Root Directory apps/migrate
Include source files outside the Root Directory Enabled
Framework Preset Other
Install Command cd ../.. && npm ci
Build Command cd ../.. && npm run build --workspace @terminal49/searates-compat && npm run build --workspace @terminal49/migrate-app
Node.js Version 24

Configure the migrate app's authentication environment variables in that project, not the MCP project. After a successful production deployment and once DNS is ready, add migrate.terminal49.com as the project's production domain in the Vercel dashboard. The custom domain is not currently live; previews use the Vercel preview hostname with the same /searates-api/... paths. CI does not create or configure the Vercel project.

Scope

Ocean tracking and the shipping-lines dictionary only. This does not add rates, schedules, air, parcel, road, historical route, AIS pin, or terminal dictionary products. /info/terminals is omitted because the public API has no terminal-list endpoint.

Compatibility details

  • SeaRates error envelopes remain HTTP 200 and use data: {}
  • missing api_key maps to verified WRONG_PARAMETERS; rejected credentials map to API_KEY_WRONG
  • empty-origin pickup maps to PICK / CPS
  • the sealines dictionary is generated from Terminal49 /shipping_lines

Verification

  • dedicated CI job builds, type-checks, tests, and lints the compatibility package and migrate app
  • all workspace builds, lint, and type checks pass locally
  • 327 tests pass across MCP, SDK, CLI, and compatibility packages (1 existing SDK test skipped)
  • deployment assertions verify the root config has no compatibility routes, the migrate app has both vendor-prefixed routes, and the migrate app does not rewrite /
  • tests use fixtures and do not call production APIs
Open in Web Open in Cursor 

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.

Greptile Summary

The PR adds a dedicated Vercel application and package that expose SeaRates-compatible ocean-tracking and shipping-line endpoints over Terminal49's public JSON:API.

  • Adds request authentication, CT/BL/BK lookup, asynchronous tracking-request polling, refresh handling, and SeaRates error envelopes.
  • Maps shipments, containers, routes, facilities, vessels, and transport events into the compatibility response.
  • Adds workspace configuration, deployment rewrites, CI coverage, fixtures, and tests for the new gateway.

Confidence Score: 2/5

The PR should not merge until tracking responses preserve the requested identifier and correctly represent valid container status and event provenance.

The new mapping layer returns observably incorrect compatibility data for current public API responses: active container states can become unknown, CT/BK metadata can contain a bill-of-lading number, and transport-event provenance is derived from an unavailable attribute.

Files Needing Attention: packages/searates-compat/src/mapping.ts, packages/searates-compat/src/fixtures/t49.ts

Important Files Changed

Filename Overview
packages/searates-compat/src/mapping.ts Implements the core SeaRates projection but misclassifies valid container states, reports BOL identifiers for CT/BK requests, and relies on an undocumented event-provenance field.
packages/searates-compat/src/service.ts Coordinates authentication, shipment resolution, polling, refreshes, event fetching, and compatibility error translation.
packages/searates-compat/src/client.ts Adds the public JSON:API client and bounded tracking-request polling; request reuse by number and SCAC remains ambiguous but was not established as defective.
packages/searates-compat/src/http.ts Adds GET/OPTIONS handlers, query normalization, environment configuration, and SeaRates-style method/type errors.
apps/migrate/vercel.json Defines the isolated compatibility deployment, function durations, and vendor-prefixed rewrites without altering the root MCP routes.
.github/workflows/ci.yml Adds build, type-check, test, and lint coverage for the compatibility package and migrate application.

Sequence Diagram

sequenceDiagram
  participant C as SeaRates Client
  participant V as Migrate Vercel Handler
  participant G as Compatibility Gateway
  participant T as Terminal49 API
  C->>V: GET /searates-api/tracking
  V->>G: tracking(api_key, query)
  G->>T: Find existing shipment
  alt Shipment exists
    T-->>G: Shipment and containers
  else Shipment missing
    G->>T: Create or reuse tracking request
    loop Bounded polling
      G->>T: Read tracking request
      T-->>G: Pending, failed, or shipment
    end
  end
  G->>T: Fetch container transport events
  T-->>G: JSON:API events
  G-->>V: SeaRates-compatible envelope
  V-->>C: HTTP 200 JSON
Loading

Fix all with Greploop Fix All in Codex Fix All in Claude Code

Prompt To Fix All With AI
### Issue 1
packages/searates-compat/src/mapping.ts:201-218
**Active statuses become unknown**

When a container has a valid active Terminal49 status such as `grounded`, `on_rail`, `off_dock`, `dropped`, or `loaded`, this function falls through to `UNKNOWN`, causing both the container and potentially the shipment metadata to report an indeterminate status despite active tracking.

### Issue 2
packages/searates-compat/src/mapping.ts:412-414
**Requested tracking number is replaced**

When a CT or BK request resolves to a shipment with a bill-of-lading number, this expression reports that BOL instead of the requested identifier, causing clients to correlate, display, or cache the response under the wrong tracking number.

```suggestion
        number: payload.requestedNumber,
```

### Issue 3
packages/searates-compat/src/mapping.ts:274
**Event provenance uses absent field**

Public transport-event responses do not provide the `data_source` attribute checked here, so real events are always emitted with `is_date_from_sealine: false`, producing incorrect provenance despite the fixture supplying the undocumented field.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Reviews (1): Last reviewed commit: "refactor: isolate migration APIs in dedi..." | Re-trigger Greptile

Greptile also left 3 inline comments on this PR.

Context used:

Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
@vercel

vercel Bot commented Aug 20, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
api Ready Ready Preview Aug 20, 2026 8:13pm

Request Review

Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
Co-authored-by: Akshay Dodeja <dodeja@gmail.com>
@dodeja
dodeja marked this pull request as ready for review August 20, 2026 21:49
Comment on lines +201 to +218
isoCode: typeCode[type] ? `${first}${second}${typeCode[type]}` : null,
sizeType: `${length}'${heightLabel} ${label[type] || type}`,
};
}

function seaRatesStatus(value: unknown): string {
if (typeof value !== 'string') return 'UNKNOWN';
if (['delivered', 'empty_returned', 'picked_up'].includes(value)) {
return 'DELIVERED';
}
if (
[
'available',
'awaiting_inland_transfer',
'in_transit',
'not_available',
'on_ship',
].includes(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.

P1 Active statuses become unknown

When a container has a valid active Terminal49 status such as grounded, on_rail, off_dock, dropped, or loaded, this function falls through to UNKNOWN, causing both the container and potentially the shipment metadata to report an indeterminate status despite active tracking.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/searates-compat/src/mapping.ts
Line: 201-218

Comment:
**Active statuses become unknown**

When a container has a valid active Terminal49 status such as `grounded`, `on_rail`, `off_dock`, `dropped`, or `loaded`, this function falls through to `UNKNOWN`, causing both the container and potentially the shipment metadata to report an indeterminate status despite active tracking.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

Comment on lines +412 to +414
number:
stringValue(shipmentAttributes.bill_of_lading_number) ||
payload.requestedNumber,

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 Requested tracking number is replaced

When a CT or BK request resolves to a shipment with a bill-of-lading number, this expression reports that BOL instead of the requested identifier, causing clients to correlate, display, or cache the response under the wrong tracking number.

Suggested change
number:
stringValue(shipmentAttributes.bill_of_lading_number) ||
payload.requestedNumber,
number: payload.requestedNumber,
Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/searates-compat/src/mapping.ts
Line: 412-414

Comment:
**Requested tracking number is replaced**

When a CT or BK request resolves to a shipment with a bill-of-lading number, this expression reports that BOL instead of the requested identifier, causing clients to correlate, display, or cache the response under the wrong tracking number.

```suggestion
        number: payload.requestedNumber,
```

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

status: mapping.status,
facility: facilityId ? (ids.facilities.get(facilityId) ?? null) : null,
is_additional_event: false,
is_date_from_sealine: attributes.data_source === 'shipping_line',

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 Event provenance uses absent field

Public transport-event responses do not provide the data_source attribute checked here, so real events are always emitted with is_date_from_sealine: false, producing incorrect provenance despite the fixture supplying the undocumented field.

Prompt To Fix With AI
This is a comment left during a code review.
Path: packages/searates-compat/src/mapping.ts
Line: 274

Comment:
**Event provenance uses absent field**

Public transport-event responses do not provide the `data_source` attribute checked here, so real events are always emitted with `is_date_from_sealine: false`, producing incorrect provenance despite the fixture supplying the undocumented field.

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Codex Fix in Claude Code

@chatgpt-codex-connector chatgpt-codex-connector 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.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ac228f8c01

ℹ️ 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".

...init,
headers: {
Accept: 'application/vnd.api+json',
Authorization: `Bearer ${this.token}`,

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 Send API keys with the Token scheme

In both pass-through and service-token modes, the credential is a Terminal49 API key, but every upstream request is sent as Authorization: Bearer .... Terminal49 API keys require the Token scheme (the existing SDK only preserves Bearer for explicitly supplied OAuth tokens), so valid gateway credentials receive 401 responses and tracking is returned as API_KEY_WRONG. Use Token for these API-key-backed requests.

AGENTS.md reference: AGENTS.md:L27-L27

Useful? React with 👍 / 👎.

Comment on lines +212 to +218
[
'available',
'awaiting_inland_transfer',
'in_transit',
'not_available',
'on_ship',
].includes(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.

P1 Badge Map active container statuses to IN_TRANSIT

When Terminal49 returns common active statuses such as grounded, on_rail, off_dock, or loaded, this predicate falls through to UNKNOWN; in_transit, meanwhile, is not one of the documented Terminal49 current_status values. Consequently both containers[].status and the aggregate metadata status become UNKNOWN during substantial portions of a shipment's journey instead of IN_TRANSIT.

AGENTS.md reference: AGENTS.md:L27-L27

Useful? React with 👍 / 👎.

Comment on lines +184 to +186
dry: 'G1',
flat_rack: 'P1',
hard_top: 'U1',

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 Match the API's equipment type values

For open-top and flat-rack containers, the public API returns equipment_type as "open top" and "flat rack", but this lookup only recognizes underscore variants. Those supported containers therefore get iso_code: null and an unnormalized size_type, despite the gateway's stated ISO reconstruction support; normalize the incoming value or key the maps by the schema-defined strings.

AGENTS.md reference: AGENTS.md:L27-L27

Useful? React with 👍 / 👎.

@vorflux vorflux 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.

Summary

Reviewed — found 9 issues. This PR adds an isolated Vercel migration app that exposes SeaRates-compatible tracking and shipping-line endpoints backed by Terminal49’s JSON:API. I reviewed workspace and deployment configuration, request authentication, tracking-request resolution and refresh behavior, response mappings, error handling, and unit coverage.

Findings

packages/searates-compat/src/client.ts

  1. API-key requests use the wrong authorization scheme.
  2. Tracking-request reuse can select an incompatible or failed historical request.
  3. Upstream requests have no timeout or abort signal.

packages/searates-compat/src/service.ts

  1. force_update exceeds the refresh rate limit for shipments with more than ten containers.
  2. Post-refresh responses retain stale shipment attributes.

packages/searates-compat/src/mapping.ts

  1. Valid journey states map to incorrect SeaRates statuses.
  2. CT and BK responses can report the bill of lading as the requested number.
  3. Offset timestamps violate the promised SeaRates timestamp shape.
  4. Public equipment values produce missing or invalid ISO codes.

Verdict

⚠️ Changes requested. The authentication failure blocks valid production requests, while the request-selection, refresh, and mapping issues can return failed, stale, or incorrectly identified tracking data.


Review with Vorflux

...init,
headers: {
Accept: 'application/vnd.api+json',
Authorization: `Bearer ${this.token}`,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue — use the API-key authorization scheme. This client receives Terminal49 API keys, but sends them as Authorization: Bearer .... Terminal49 API keys require the Token scheme; Bearer is reserved for user-scoped JWTs and requires account context. As written, valid pass-through keys and T49_SEARATES_API_TOKEN credentials will receive 401 responses. Send API keys as Authorization: Token ... and update the header assertion in the tests.

Comment on lines +194 to +200
const params = new URLSearchParams({
'filter[request_number]': number,
include: 'tracked_object',
'page[size]': '1',
});
if (scac) params.set('filter[scac]', scac);
return this.request(`/tracking_requests?${params.toString()}`);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue — do not reuse an arbitrary historical request. This query returns only one record for the number and optional SCAC, without matching request_type or selecting by status and recency. Multiple requests can exist for the same number, including different request types and failed attempts followed by successful ones, so a BL lookup can reuse a BK/CT request or an old failure and then never create a valid replacement. Retrieve enough candidates to enforce the requested type, prefer a tracked or active request, and create a new request when only failed candidates exist.

Comment on lines +203 to +214
private async request(
path: string,
init: RequestInit = {},
): Promise<JsonApiDocument> {
const response = await this.fetchImpl(`${this.baseUrl}${path}`, {
...init,
headers: {
Accept: 'application/vnd.api+json',
Authorization: `Bearer ${this.token}`,
...init.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.

Issue — bound upstream requests with an abort timeout. The polling loop has a nominal deadline, but each fetch can stall beyond that deadline and eventually be terminated by Vercel. That bypasses the gateway's SeaRates error envelope and can affect shipment, event, shipping-line, and polling calls. Add an AbortSignal timeout bounded by the remaining polling/function budget and translate timeout failures to SEALINE_NO_RESPONSE.

Comment on lines +124 to +127
if (query.forceUpdate && containers.length > 0) {
await Promise.all(
containers.map((container) => client.refreshContainer(container.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.

Issue — throttle force_update refreshes. Container refreshes are limited to 10 requests per minute, but Promise.all submits every refresh concurrently. Any shipment with more than ten containers can therefore trigger 429 responses, fail the entire otherwise-valid lookup after partially scheduling refreshes, and immediately consume the shared quota in service-token mode. Queue or throttle these calls to the documented limit and define partial-failure behavior.

Comment on lines +128 to +146
shipmentDocument = await client.shipment(shipment.id);
containers = containerResources(shipmentDocument);
}

const eventsByContainerId = new Map<string, JsonApiDocument>();
await Promise.all(
containers.map(async (container) => {
eventsByContainerId.set(
container.id,
await client.transportEvents(container.id),
);
}),
);
const payload: TrackingPayload = {
eventsByContainerId,
included: shipmentDocument.included || [],
requestedNumber: query.number,
requestedType: type,
shipment,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue — replace the shipment resource after refresh. After force_update, this fetches a new shipmentDocument and replaces containers, but the payload still references the pre-refresh shipment. Updated route dates, vessel or carrier metadata, and tracking timestamps from the second response are discarded while newer included containers are used. Re-extract and validate the shipment resource from the refreshed document before constructing TrackingPayload.

Comment on lines +206 to +222
function seaRatesStatus(value: unknown): string {
if (typeof value !== 'string') return 'UNKNOWN';
if (['delivered', 'empty_returned', 'picked_up'].includes(value)) {
return 'DELIVERED';
}
if (
[
'available',
'awaiting_inland_transfer',
'in_transit',
'not_available',
'on_ship',
].includes(value)
) {
return 'IN_TRANSIT';
}
return '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.

Issue — map all public journey states correctly. picked_up is classified as DELIVERED even though it represents transport from the terminal to the warehouse, while active states such as grounded, on_rail, off_dock, loaded, and dropped fall through to UNKNOWN; new also lacks a planned-state mapping. This makes container and aggregate shipment status incorrect for common journeys. Keep pickup and active terminal/movement states in transit, reserve delivery for actual delivery or empty return, and explicitly map the pre-departure state.

Comment on lines +411 to +414
type: payload.requestedType,
number:
stringValue(shipmentAttributes.bill_of_lading_number) ||
payload.requestedNumber,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue — preserve the identifier associated with metadata.type. For type=CT or type=BK, this replaces the submitted identifier with bill_of_lading_number, producing metadata whose type and number refer to different identifiers. Clients can then mis-correlate or mis-cache the response. Preserve payload.requestedNumber, or choose the identifier corresponding to requestedType.

Comment on lines +153 to +155
function formatDate(value: unknown): string | null {
if (typeof value !== 'string' || !value) return null;
return value.replace('T', ' ').replace(/(?:\.\d+)?Z$/, '');

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue — normalize offset timestamps to UTC. This only strips a trailing Z; a valid value such as 2020-04-04T16:13:35-07:00 becomes 2020-04-04 16:13:35-07:00, which does not match the documented YYYY-MM-DD HH:MM:SS response shape and is not UTC. Parse valid ISO-8601 values, normalize to UTC, format consistently, and add coverage for both Z and non-zero offsets.

Comment on lines +181 to +198
const first = length === 20 ? '2' : length === 40 ? '4' : 'L';
const second = height === 'high_cube' ? '5' : '2';
const typeCode: Record<string, string> = {
dry: 'G1',
flat_rack: 'P1',
hard_top: 'U1',
open_top: 'U1',
reefer: 'R1',
tank: 'T1',
};
const label: Record<string, string> = {
dry: 'Dry',
flat_rack: 'Flat Rack',
hard_top: 'Hard Top',
open_top: 'Open Top',
reefer: 'Reefer',
tank: 'Tank',
};

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Issue — handle the public equipment values and lengths. The Terminal49 schema emits equipment types such as open top and flat rack, while this table only recognizes underscore variants, yielding iso_code: null and an unnormalized size_type. The fallback length prefix L is also only appropriate for 45-foot equipment, not supported 10-foot containers, whose ISO length character is 1. Map the actual enum values and handle each supported length explicitly.

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