Skip to content

Specification: Arda API Proxy

A standalone TypeScript package (@arda-cards/api-proxy) providing type-safe proxy objects for every Arda REST API endpoint. Each API endpoint has its own independently instantiable proxy, configured with explicit host and apiKey parameters. The package is organized by functional domain — system, reference, resources, procurement — mirroring the Arda functional architecture.


ConcernChoiceNotes
LanguageTypeScript (strict mode)strict: true, verbatimModuleSyntax
RuntimeNode.js 18.18+Native fetch, no polyfills
Buildtsc (project references)ES2022 target, Node16 module resolution
TestsVitestMock-only, restoreMocks: true
Coverage@vitest/coverage-istanbul80% line and branch thresholds, CI-gated
LintingESLint (typescript-eslint strictTypeChecked) + PrettierMatches api-mcp configuration
CIGitHub ActionsFormat, typecheck, lint, test, coverage
PublishingGitHub Packages (npm.pkg.github.com)@arda-cards/api-proxy

api-proxy/
├── src/
│ ├── shared/ # Shared utilities — imported by each proxy
│ │ ├── http-client.ts # Composable HttpClient class
│ │ ├── types.ts # EntityRecord, PageResult, Query, Filter, etc.
│ │ ├── errors.ts # ArdaApiError, parseErrorResponse
│ │ └── index.ts # Re-exports shared surface
│ │
│ ├── system/ # System domain (accounts-component)
│ │ ├── tenant.ts # TenantProxy
│ │ ├── tenant.types.ts
│ │ ├── user-account.ts # UserAccountProxy
│ │ ├── user-account.types.ts
│ │ ├── agent-for.ts # AgentForProxy
│ │ ├── agent-for.types.ts
│ │ ├── invitation.ts # InvitationProxy
│ │ ├── invitation.types.ts
│ │ └── index.ts
│ │
│ ├── reference/ # Reference Data domain (operations)
│ │ ├── item.ts # ItemProxy
│ │ ├── item.types.ts
│ │ ├── business-affiliate.ts # BusinessAffiliateProxy
│ │ ├── business-affiliate.types.ts
│ │ └── index.ts
│ │
│ ├── resources/ # Resources domain (operations)
│ │ ├── kanban.ts # KanbanProxy
│ │ ├── kanban.types.ts
│ │ └── index.ts
│ │
│ ├── procurement/ # Procurement domain (operations)
│ │ ├── order.ts # OrderProxy
│ │ ├── order.types.ts
│ │ └── index.ts
│ │
│ └── index.ts # Root re-exports all proxies + shared types
├── tests/ # Mock-only tests mirroring src/
│ ├── shared/
│ │ └── http-client.test.ts
│ ├── system/
│ │ ├── tenant.test.ts
│ │ ├── user-account.test.ts
│ │ ├── agent-for.test.ts
│ │ └── invitation.test.ts
│ ├── reference/
│ │ ├── item.test.ts
│ │ └── business-affiliate.test.ts
│ ├── resources/
│ │ └── kanban.test.ts
│ └── procurement/
│ └── order.test.ts
├── package.json
├── tsconfig.json
├── vitest.config.ts
├── eslint.config.js
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── .gitignore
└── .github/
└── workflows/
├── ci.yml
└── publish.yml

A composable HTTP client that each proxy receives via constructor injection. Not a base class — proxies hold a reference to an HttpClient instance.

export interface ProxyConfig {
host: string; // e.g. "https://stage.alpha002.io.arda.cards"
apiKey: string; // Arda API key
}
export class HttpClient {
constructor(
private readonly config: ProxyConfig,
private readonly basePath: string, // e.g. "/v1/tenant"
) {}
buildUrl(path: string, params?: TimeCoordinateParams): string;
request<T>(method: string, url: string, body?: unknown): Promise<T>;
}

Headers — every request includes:

X-Author: <apiKey>
Content-Type: application/json

Note: The full Arda API requires Authorization, X-Tenant-Id, and X-Request-ID headers as well (see api-design.md). Phase 1 replicates the existing api-mcp header pattern (X-Author + Content-Type). Support for additional headers (Bearer token, tenant ID, request ID) is deferred to a future enhancement once the proxy is used in contexts that require them.

Extracted directly from api-mcp/packages/shared/src/types.ts:

  • TimeCoordinates — bitemporal effective/recorded pair
  • EntityRecord<TPayload, TMetadata> — standard response envelope
  • PageResult<TPayload, TMetadata> — paginated response wrapper
  • Query, Filter, Sort, SortEntry, SortDirection, Pagination
  • HistoryRequestBody
  • ErrorResponse
  • TimeCoordinateParams

Extracted from api-mcp/packages/shared/src/errors.ts:

  • ArdaApiError — typed error with status, responseMessage, and details
  • parseErrorResponse(response: Response) — parses failed fetch responses

Each API endpoint gets its own proxy class. All proxies follow the same construction pattern:

export class TenantProxy {
private readonly client: HttpClient;
constructor(config: ProxyConfig) {
this.client = new HttpClient(config, "/v1/tenant");
}
// CRUD + query + history methods
}

Every proxy that wraps a standard Arda entity endpoint exposes:

MethodHTTPPathDescription
create(input, params?)POST /{entity}Create entity
get(entityId, params?)GET /{entity}/{entityId}Get by entity ID
getByRecordId(recordId)GET /{entity}/rid/{recordId}Get by record ID
update(entityId, input, params?)PUT /{entity}/{entityId}Update entity
delete(entityId, params?)DELETE /{entity}/{entityId}Delete entity
query(query)POST /{entity}/queryQuery with filter/sort/page
queryHistory(entityId, body)POST /{entity}/{entityId}/historyAudit history

Where params is TimeCoordinateParams (optional bitemporal coordinates).

Beyond standard CRUD, each module exposes its endpoint-specific operations:

TenantProxy — standard CRUD only.

UserAccountProxy — standard CRUD only.

AgentForProxy — standard CRUD only.

InvitationProxy — standard CRUD only.

BusinessAffiliateProxy — standard CRUD plus:

MethodHTTPPath
getWithDetails(entityId, params?)GET /business-affiliate/with-details/{entityId}
getRoles(entityId)GET /business-affiliate/{entityId}/roles
createRole(entityId, input)POST /business-affiliate/{entityId}/roles
updateRole(entityId, roleId, input)PUT /business-affiliate/{entityId}/roles/{roleId}

ItemProxy — standard CRUD plus:

MethodHTTPPath
getDraft(entityId)GET /item/{entityId}/draft
updateDraft(entityId, input)PUT /item/{entityId}/draft
getSupplies(entityId)GET /item/{entityId}/supply
createSupply(entityId, input)POST /item/{entityId}/supply
updateSupply(entityId, supplyId, input)PUT /item/{entityId}/supply/{supplyId}
deleteSupply(entityId, supplyId)DELETE /item/{entityId}/supply/{supplyId}
lookupSuppliers(query)GET /lookup-suppliers?q={query}
lookupUnits(query)GET /lookup-units?q={query}
printLabels(entityIds)POST /item/print-label
printBreadcrumbs(entityIds)POST /item/print-breadcrumb
createUploadUrl()POST /upload-job/upload-url
getUploadJobStatus(jobId)GET /upload-job/{jobId}

KanbanProxy — standard CRUD plus:

MethodHTTPPath
getDetails(entityId, params?)GET /kanban-card/details/{entityId}
queryDetails(query)POST /kanban-card/details
queryDetailsByStatus(status, query)POST /kanban-card/details/{status}
getCardsForItem(itemEntityId)GET /kanban-card/for-item/{itemEntityId}
getSummary()GET /kanban-card/summary
getSummaryRequested()GET /kanban-card/summary/requested
getSummaryInProcess()GET /kanban-card/summary/in-process
updateNotes(entityId, input)PUT /kanban-card/{entityId}/notes
postEvent(entityId, event, body?)POST /kanban-card/{entityId}/event/{event}
printCards(entityIds)POST /kanban-card/print-card

The postEvent method accepts an event name from a typed union: "request" | "accept" | "start-processing" | "complete-processing" | "fulfill" | "receive" | "use" | "deplete" | "withdraw".

OrderProxy — standard CRUD plus:

MethodHTTPPath
getFull(entityId, params?)GET /order/full/{entityId}
createFromItems(input)POST /order/from-items
addItemsToOrder(entityId, input)POST /order/from-items/{entityId}
createFromKanbanCards(input)POST /order/from-kanban-cards
addKanbanCardsToOrder(entityId, input)POST /order/from-kanban-cards/{entityId}
addLine(entityId, input)POST /order/{entityId}/lines
updateLine(entityId, lineId, input)PUT /order/{entityId}/lines/{lineId}
deleteLine(entityId, lineId)DELETE /order/{entityId}/lines/{lineId}
moveLine(entityId, lineId, input)PUT /order/{entityId}/lines/move-line/{lineId}
submit(entityId)POST /order/{entityId}/submit
accept(entityId)POST /order/{entityId}/accepted
receive(entityId, input)POST /order/{entityId}/receive
archive(entityId)POST /order/{entityId}/archive
annotate(entityId, input)PUT /order/{entityId}/annotate
annotateLine(entityId, lineId, input)PUT /order/{entityId}/annotate-line/{lineId}

Each <module>.types.ts file contains the request/response types for that module. Types are sourced from:

  1. api-mcp type definitions — reuse directly where they exist (currently only tenant-mcp has types).
  2. OpenAPI specs — fetch from {host}/v1/{module}/docs/openApi.json for modules not yet typed.
  3. Kotlin source — when OpenAPI types are ambiguous (e.g., due to kotlinx.serialization or generic type erasure), inspect the Kotlin types in operations or accounts-component for the authoritative definition.

The package exposes both barrel imports and deep imports:

// Barrel import — all proxies
import { TenantProxy, ItemProxy, KanbanProxy } from "@arda-cards/api-proxy";
// Domain-scoped import
import { TenantProxy } from "@arda-cards/api-proxy/system";
import { ItemProxy } from "@arda-cards/api-proxy/reference";
// Shared types only
import type { EntityRecord, Query, Filter } from "@arda-cards/api-proxy/shared";

The package.json exports map enables this:

{
"exports": {
".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
"./shared": { "import": "./dist/shared/index.js", "types": "./dist/shared/index.d.ts" },
"./system": { "import": "./dist/system/index.js", "types": "./dist/system/index.d.ts" },
"./reference": { "import": "./dist/reference/index.js", "types": "./dist/reference/index.d.ts" },
"./resources": { "import": "./dist/resources/index.js", "types": "./dist/resources/index.d.ts" },
"./procurement": { "import": "./dist/procurement/index.js", "types": "./dist/procurement/index.d.ts" }
}
}

import { TenantProxy } from "@arda-cards/api-proxy/system";
import { KanbanProxy } from "@arda-cards/api-proxy/resources";
const tenant = new TenantProxy({
host: "https://stage.alpha002.io.arda.cards",
apiKey: "my-api-key",
});
const result = await tenant.query({
filter: { locator: "tenantName", regex: ".*test.*" },
sort: { entries: [{ key: "tenantName", direction: "ASC" }] },
paginate: { index: 0, size: 10 },
});
const kanban = new KanbanProxy({
host: "https://stage.alpha002.io.arda.cards",
apiKey: "my-api-key",
});
await kanban.postEvent("card-entity-id", "fulfill");

All tests use mocked fetch — no live API calls.

Each proxy gets a test file that verifies:

  1. Construction — proxy builds correct base URL from config.
  2. Standard CRUD — each method calls the correct HTTP method, path, and body.
  3. Time coordinates — optional effectiveasof/recordedasof params are appended to URLs.
  4. Module-specific operations — sub-resources, actions, lookups, views.
  5. Error handlingArdaApiError is thrown with correct status and message on non-OK responses.

The HttpClient shared utility gets its own test covering URL building, header assembly, error parsing, and request/response flow.

Coverage is collected via Istanbul (using @vitest/coverage-istanbul) and enforced in CI.

Thresholds (fail the build if not met):

MetricThreshold
Lines80%
Branches80%

vitest.config.ts coverage section:

import { defineConfig } from "vitest/config";
export default defineConfig({
test: {
globals: false,
restoreMocks: true,
include: ["tests/**/*.test.ts"],
exclude: ["**/dist/**", "**/node_modules/**"],
coverage: {
provider: "istanbul",
include: ["src/**/*.ts"],
exclude: ["src/**/index.ts"], // barrel re-exports only
reporter: ["text", "lcov", "json-summary"],
reportsDirectory: "coverage",
thresholds: {
lines: 80,
branches: 80,
},
},
},
});

The coverage/ directory is added to .gitignore.


Triggers on pull requests to main and pushes to main.

name: CI
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
- run: npm ci
- run: npm run format:check
- run: npm run typecheck
- run: npm run lint
- run: npm run test:coverage

publish.yml — Publish to GitHub Packages

Section titled “publish.yml — Publish to GitHub Packages”

Triggers on push to main (after CI passes).

name: Publish
on:
push:
branches: [main]
permissions:
contents: read
packages: write
jobs:
publish:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: npm
registry-url: https://npm.pkg.github.com
- run: npm ci
- run: npm run build
- run: npm test
- run: npm publish
env:
NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }}

Note: Unlike api-mcp which uses npm publish --workspaces, this is a single package so npm publish suffices.

Configure on Arda-cards/api-proxy repository, main branch:

RuleSetting
Require pull request before mergingYes
Required approvals1
Require status checks to passYes
Required status checkscheck (from ci.yml)
Require branches to be up to dateYes
Require conversation resolutionYes
Allow force pushesNo
Allow deletionsNo
SettingValue
Default branchmain
Allow merge commitsYes
Allow squash mergingYes
Allow rebase mergingYes
Delete branch on mergeYes

{
"name": "@arda-cards/api-proxy",
"version": "0.1.0",
"description": "Type-safe TypeScript proxy for Arda REST APIs",
"type": "module",
"exports": {
".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" },
"./shared": { "import": "./dist/shared/index.js", "types": "./dist/shared/index.d.ts" },
"./system": { "import": "./dist/system/index.js", "types": "./dist/system/index.d.ts" },
"./reference": { "import": "./dist/reference/index.js", "types": "./dist/reference/index.d.ts" },
"./resources": { "import": "./dist/resources/index.js", "types": "./dist/resources/index.d.ts" },
"./procurement": { "import": "./dist/procurement/index.js", "types": "./dist/procurement/index.d.ts" }
},
"files": ["dist"],
"engines": {
"node": ">=18.18.0"
},
"scripts": {
"build": "tsc -b",
"test": "vitest run",
"test:coverage": "vitest run --coverage",
"test:watch": "vitest",
"lint": "eslint .",
"format": "prettier --write .",
"format:check": "prettier --check .",
"typecheck": "tsc --noEmit"
},
"repository": {
"type": "git",
"url": "https://github.com/Arda-cards/api-proxy.git"
},
"publishConfig": {
"registry": "https://npm.pkg.github.com"
},
"license": "UNLICENSED",
"keywords": ["arda", "api-client", "api-proxy", "bitemporal", "typescript"],
"devDependencies": {
"@types/node": "^22.15.0",
"@vitest/coverage-istanbul": "^3.1.1",
"eslint": "^9.25.0",
"prettier": "^3.5.3",
"typescript": "^5.8.3",
"typescript-eslint": "^8.30.1",
"vitest": "^3.1.1"
}
}

No runtime dependencies. @vitest/coverage-istanbul is the only addition beyond the standard api-mcp dev stack.


{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"strict": true,
"verbatimModuleSyntax": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "dist",
"rootDir": "src"
},
"include": ["src"],
"exclude": ["dist", "node_modules", "tests"]
}

Phase 1 — Shared + Tenant (Reference Implementation)

Section titled “Phase 1 — Shared + Tenant (Reference Implementation)”
  1. Initialize repository: package.json, tsconfig.json, eslint.config.js, vitest.config.ts, .prettierrc, .gitignore.
  2. Implement src/shared/HttpClient, types.ts, errors.ts.
  3. Implement src/system/tenant.ts + tenant.types.ts as the reference proxy.
  4. Write tests for HttpClient and TenantProxy.
  5. Set up CI workflows (.github/workflows/ci.yml and publish.yml).
  6. Verify: npm run format:check && npm run typecheck && npm run lint && npm run test:coverage && npm run build.
  7. Configure branch protection rules on the repository.
  1. Implement user-account, agent-for, invitation proxies + types + tests.
  2. Create domain index.ts barrel exports.

Phase 3 — Reference Data + Resources + Procurement

Section titled “Phase 3 — Reference Data + Resources + Procurement”
  1. Implement business-affiliate, item proxies + types + tests.
  2. Implement kanban proxy + types + tests.
  3. Implement order proxy + types + tests.
  4. Create domain index.ts barrel exports.
  5. Create root src/index.ts barrel export.
  1. Write README.md with usage examples and installation instructions.
  2. Write initial CHANGELOG.md entry.
  3. Verify full build + test + publish dry run.
  4. Merge to main, verify GitHub Actions publish to GitHub Packages.

  • api-mcp/packages/shared/src/ — base client, types, auth, errors
  • api-mcp/packages/tenant-mcp/src/ — reference module client and types
  • documentation/.../architecture/patterns/api-design.md — required headers, error formats, query patterns
  • documentation/.../functional/api-endpoint-catalog.md — definitive endpoint catalog
  • Arda API OpenAPI specs at {host}/v1/{module}/docs/openApi.json
  • Kotlin source in operations and accounts-component for ambiguous type resolution