Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 28 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,3 +118,31 @@ jobs:
run: npm run lint --workspace @terminal49/mcp
- name: Check API gateway (Vite+ and anti-slop)
run: npm run lint:api

migrate:
runs-on: ${{ (startsWith(vars.CI_RUNNER, 'blacksmith-') && vars.CI_RUNNER) || 'blacksmith-4vcpu-ubuntu-2404' }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v6
with:
node-version: 24
cache: 'npm'
cache-dependency-path: package-lock.json
- name: Install workspace dependencies
run: npm ci
- name: Build compatibility package
run: npm run build --workspace @terminal49/searates-compat
- name: Build migrate application
run: npm run build --workspace @terminal49/migrate-app
- name: Type-check compatibility package and migrate application
run: |
npm run type-check --workspace @terminal49/searates-compat
npm run type-check --workspace @terminal49/migrate-app
- name: Test compatibility package and migrate configuration
run: |
npm run test --workspace @terminal49/searates-compat
npm run test --workspace @terminal49/migrate-app
- name: Check compatibility package and migrate handlers
run: |
npm run lint --workspace @terminal49/searates-compat
npm run lint --workspace @terminal49/migrate-app
11 changes: 11 additions & 0 deletions apps/migrate/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Pass-through mode is the default: leave service-token variables unset and
# send a Terminal49 API key in the SeaRates-compatible `api_key` query.
T49_API_BASE_URL=https://api.terminal49.com/v2

# Service-token mode: clients send T49_SEARATES_CLIENT_SECRET as `api_key`.
# T49_SEARATES_API_TOKEN=YOUR_T49_API_KEY
# T49_SEARATES_CLIENT_SECRET=YOUR_GATEWAY_KEY

# Optional bounded polling settings.
# T49_SEARATES_POLL_TIMEOUT_MS=4000
# T49_SEARATES_POLL_INTERVAL_MS=500
3 changes: 3 additions & 0 deletions apps/migrate/api/info/sealines.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createShippingLinesHandler } from '@terminal49/searates-compat';

export default createShippingLinesHandler();
3 changes: 3 additions & 0 deletions apps/migrate/api/tracking.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { createTrackingHandler } from '@terminal49/searates-compat';

export default createTrackingHandler();
25 changes: 25 additions & 0 deletions apps/migrate/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@terminal49/migrate-app",
"version": "0.1.0",
"private": true,
"description": "Vercel application for vendor compatibility APIs",
"type": "module",
"scripts": {
"build": "tsc --noEmit",
"test": "node -e \"JSON.parse(require('fs').readFileSync('vercel.json', 'utf8'))\"",
"lint": "vp lint api && vp fmt --check api",
"format": "vp fmt --write api",
"type-check": "tsc --noEmit"
},
"dependencies": {
"@terminal49/searates-compat": "0.1.0"
},
"devDependencies": {
"@types/node": "^24.10.13",
"typescript": "^5.6.3",
"vite-plus": "0.2.9"
},
"engines": {
"node": "24.x"
}
}
17 changes: 17 additions & 0 deletions apps/migrate/tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"lib": ["ES2022"],
"moduleResolution": "NodeNext",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"noEmit": true,
"types": ["node"]
},
"include": ["api/**/*"],
"exclude": ["node_modules"]
}
24 changes: 24 additions & 0 deletions apps/migrate/vercel.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"$schema": "https://openapi.vercel.sh/vercel.json",
"version": 2,
"installCommand": "cd ../.. && npm ci",
"buildCommand": "cd ../.. && npm run build --workspace @terminal49/searates-compat && npm run build --workspace @terminal49/migrate-app",
"functions": {
"api/tracking.ts": {
"maxDuration": 30
},
"api/info/sealines.ts": {
"maxDuration": 15
}
},
"rewrites": [
{
"source": "/searates-api/tracking",
"destination": "/api/tracking"
},
{
"source": "/searates-api/info/sealines",
"destination": "/api/info/sealines"
}
]
}
37 changes: 37 additions & 0 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@
"private": true,
"description": "Terminal49 API with MCP Server",
"workspaces": [
"apps/*",
"packages/*",
"sdks/*"
],
"type": "module",
"scripts": {
"build": "npm run build --workspace @terminal49/sdk && npm run build --workspace @terminal49/mcp && npm run build --workspace @terminal49/cli",
"build": "npm run build --workspace @terminal49/sdk && npm run build --workspace @terminal49/mcp && npm run build --workspace @terminal49/searates-compat && npm run build --workspace @terminal49/migrate-app && npm run build --workspace @terminal49/cli",
"check": "npm run lint:api && npm run lint --workspaces && npm run type-check --workspaces",
"format": "vp fmt --write api && npm run format --workspaces",
"lint:api": "vp lint api && vp fmt --check api",
Expand Down
142 changes: 142 additions & 0 deletions packages/searates-compat/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
# SeaRates ocean-tracking compatibility gateway

This package exposes a small SeaRates-compatible HTTP surface backed only by
Terminal49's public JSON:API. It is a compatibility gateway for ocean container,
Bill of Lading (BOL), and booking tracking. It is not a clone of SeaRates'
rates, schedules, air, parcel, road, route-history, or Automatic Identification
System (AIS) products.

## Endpoints

- `GET /searates-api/tracking`
- `GET /searates-api/info/sealines`

The dedicated migrate Vercel application in `apps/migrate` maps these paths to
its tracking and sealines handlers. The vendor-prefixed layout leaves room for
future compatibility APIs without placing them on the MCP application.
`/info/terminals` is intentionally omitted because the Terminal49 public API can
fetch a known terminal but does not provide a supported-terminals list.

The intended production URLs are:

- `https://migrate.terminal49.com/searates-api/tracking`
- `https://migrate.terminal49.com/searates-api/info/sealines`

The custom domain is not live yet. Until DNS and the production domain are
configured, deployments use their Vercel preview hostname with the same
`/searates-api/...` paths.

## Configure authentication

Choose one of two modes:

### Pass-through mode

Leave `T49_SEARATES_API_TOKEN` unset. The gateway treats the SeaRates `api_key`
query parameter as a Terminal49 API key and sends it upstream as
`Authorization: Bearer <api_key>`.

### Service-token mode

Set both values:

```bash
T49_SEARATES_API_TOKEN=YOUR_T49_API_KEY
T49_SEARATES_CLIENT_SECRET=YOUR_GATEWAY_KEY
```

Clients send `YOUR_GATEWAY_KEY` as `api_key`. The gateway compares it in
constant time and uses `T49_SEARATES_API_TOKEN` only for requests to the public
Terminal49 API. This is one shared deployment credential, not a multi-tenant
billing or key-management system.

Optional settings:

```bash
T49_API_BASE_URL=https://api.terminal49.com/v2
T49_SEARATES_POLL_TIMEOUT_MS=4000
T49_SEARATES_POLL_INTERVAL_MS=500
```

## Point an existing client at the gateway

Change the SeaRates base URL and keep the existing query parameters:

```bash
curl "https://migrate.terminal49.com/searates-api/tracking?api_key=YOUR_GATEWAY_KEY&number=MSCU1234567&type=CT&sealine=MSCU"
```

The gateway accepts `type=CT`, `type=BL`, and `type=BK`, plus `force_update`,
`route`, and `ais`. `force_update=true` requests a Terminal49 container refresh
when a tracked container already exists. The compatibility response always
includes SeaRates' route summary. Detailed route geometry and AIS pins are not
implemented.

Fetch the carrier dictionary with:

```bash
curl "https://migrate.terminal49.com/searates-api/info/sealines?api_key=YOUR_GATEWAY_KEY"
```

In service-token mode, `/info/sealines` also works without `api_key`, matching
SeaRates' public dictionary behavior. Its rows are generated from Terminal49
`GET /shipping_lines`; they are not a hardcoded sample.

## Asynchronous tracking behavior

Terminal49 creates tracking requests asynchronously. On a cache miss, the
gateway:

1. creates or reuses a Terminal49 tracking request;
2. polls it for a short, bounded interval;
3. returns the full SeaRates envelope if the shipment becomes available; or
4. returns SeaRates' successful empty-data outcome:
`status: "success"`, `message: "SEALINE_HASNT_PROVIDE_INFO"`,
`metadata.status: "UNKNOWN"`, and empty data arrays.

SeaRates has no documented pending response, so the gateway does not invent one.
Retry the same `GET /tracking` request after the empty-data outcome. The gateway
reuses the existing Terminal49 tracking request instead of creating another one.
Terminal49 failure reasons are translated to SeaRates-style messages such as
`WRONG_NUMBER`, `AUTO_CANT_DETECT_SEALINE`, and
`SEALINE_HASNT_PROVIDE_INFO`.

## Compatibility limits

- Timestamps are rendered in SeaRates' `YYYY-MM-DD HH:MM:SS` shape but remain
UTC because Terminal49 stores canonical event timestamps in UTC.
- SeaRates quota counters and cache expiration have no Terminal49 equivalent,
so those fields are `null`.
- Equipment ISO codes are reconstructed for common dry, reefer, open-top,
flat-rack, hard-top, and tank combinations. Unknown combinations are `null`.
- Holds, fees, Last Free Day (LFD), and other Terminal49-only terminal
intelligence are deliberately excluded.

## Create the dedicated Vercel project

Create a second Vercel project in the Terminal49 team and import this same
repository. This is a dashboard setup step; CI does not create or configure the
project.

Use these project settings:

| 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 |

The outside-root source setting is required because the app consumes the
`@terminal49/searates-compat` workspace from `packages/searates-compat`.

Configure either pass-through mode or the service-token environment variables
described above in the new project. Do not copy them into the MCP Vercel
project.

After the project has a successful production deployment and DNS is ready, add
`migrate.terminal49.com` under the project's production domains. Vercel will
show the DNS record that must be added; do not assume the domain is active until
Vercel verifies it.
25 changes: 25 additions & 0 deletions packages/searates-compat/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
"name": "@terminal49/searates-compat",
"version": "0.1.0",
"private": true,
"description": "SeaRates ocean tracking compatibility gateway backed by the Terminal49 public API",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"scripts": {
"build": "tsc",
"test": "vp test --run src",
"lint": "vp lint src && vp fmt --check src",
"format": "vp fmt --write src",
"type-check": "tsc --noEmit"
},
"devDependencies": {
"@types/node": "^24.10.13",
"@vitest/coverage-v8": "4.1.10",
"typescript": "^5.6.3",
"vite-plus": "0.2.9"
},
"engines": {
"node": "24.x"
}
}
Loading
Loading