Specification: Arda API Proxy
Overview
Section titled “Overview”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.
Technology Stack
Section titled “Technology Stack”| Concern | Choice | Notes |
|---|---|---|
| Language | TypeScript (strict mode) | strict: true, verbatimModuleSyntax |
| Runtime | Node.js 18.18+ | Native fetch, no polyfills |
| Build | tsc (project references) | ES2022 target, Node16 module resolution |
| Tests | Vitest | Mock-only, restoreMocks: true |
| Coverage | @vitest/coverage-istanbul | 80% line and branch thresholds, CI-gated |
| Linting | ESLint (typescript-eslint strictTypeChecked) + Prettier | Matches api-mcp configuration |
| CI | GitHub Actions | Format, typecheck, lint, test, coverage |
| Publishing | GitHub Packages (npm.pkg.github.com) | @arda-cards/api-proxy |
Package Structure
Section titled “Package Structure”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.ymlShared Utilities (src/shared/)
Section titled “Shared Utilities (src/shared/)”HttpClient
Section titled “HttpClient”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/jsonNote: The full Arda API requires
Authorization,X-Tenant-Id, andX-Request-IDheaders as well (seeapi-design.md). Phase 1 replicates the existingapi-mcpheader 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.
Common Types (types.ts)
Section titled “Common Types (types.ts)”Extracted directly from api-mcp/packages/shared/src/types.ts:
TimeCoordinates— bitemporal effective/recorded pairEntityRecord<TPayload, TMetadata>— standard response envelopePageResult<TPayload, TMetadata>— paginated response wrapperQuery,Filter,Sort,SortEntry,SortDirection,PaginationHistoryRequestBodyErrorResponseTimeCoordinateParams
Error Handling (errors.ts)
Section titled “Error Handling (errors.ts)”Extracted from api-mcp/packages/shared/src/errors.ts:
ArdaApiError— typed error withstatus,responseMessage, anddetailsparseErrorResponse(response: Response)— parses failed fetch responses
Proxy Object Design
Section titled “Proxy Object Design”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}Standard Operations
Section titled “Standard Operations”Every proxy that wraps a standard Arda entity endpoint exposes:
| Method | HTTP | Path | Description |
|---|---|---|---|
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}/query | Query with filter/sort/page | |
queryHistory(entityId, body) | POST /{entity}/{entityId}/history | Audit history |
Where params is TimeCoordinateParams (optional bitemporal coordinates).
Module-Specific Operations
Section titled “Module-Specific Operations”Beyond standard CRUD, each module exposes its endpoint-specific operations:
System Domain
Section titled “System Domain”TenantProxy — standard CRUD only.
UserAccountProxy — standard CRUD only.
AgentForProxy — standard CRUD only.
InvitationProxy — standard CRUD only.
Reference Data Domain
Section titled “Reference Data Domain”BusinessAffiliateProxy — standard CRUD plus:
| Method | HTTP | Path |
|---|---|---|
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:
| Method | HTTP | Path |
|---|---|---|
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} |
Resources Domain
Section titled “Resources Domain”KanbanProxy — standard CRUD plus:
| Method | HTTP | Path |
|---|---|---|
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".
Procurement Domain
Section titled “Procurement Domain”OrderProxy — standard CRUD plus:
| Method | HTTP | Path |
|---|---|---|
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} |
Per-Module Types
Section titled “Per-Module Types”Each <module>.types.ts file contains the request/response types for that module. Types are sourced from:
api-mcptype definitions — reuse directly where they exist (currently onlytenant-mcphas types).- OpenAPI specs — fetch from
{host}/v1/{module}/docs/openApi.jsonfor modules not yet typed. - Kotlin source — when OpenAPI types are ambiguous (e.g., due to
kotlinx.serializationor generic type erasure), inspect the Kotlin types inoperationsoraccounts-componentfor the authoritative definition.
Import Surface
Section titled “Import Surface”The package exposes both barrel imports and deep imports:
// Barrel import — all proxiesimport { TenantProxy, ItemProxy, KanbanProxy } from "@arda-cards/api-proxy";
// Domain-scoped importimport { TenantProxy } from "@arda-cards/api-proxy/system";import { ItemProxy } from "@arda-cards/api-proxy/reference";
// Shared types onlyimport 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" } }}Usage Example
Section titled “Usage Example”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");Testing Strategy
Section titled “Testing Strategy”All tests use mocked fetch — no live API calls.
Each proxy gets a test file that verifies:
- Construction — proxy builds correct base URL from config.
- Standard CRUD — each method calls the correct HTTP method, path, and body.
- Time coordinates — optional
effectiveasof/recordedasofparams are appended to URLs. - Module-specific operations — sub-resources, actions, lookups, views.
- Error handling —
ArdaApiErroris 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
Section titled “Coverage”Coverage is collected via Istanbul (using @vitest/coverage-istanbul) and enforced in CI.
Thresholds (fail the build if not met):
| Metric | Threshold |
|---|---|
| Lines | 80% |
| Branches | 80% |
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.
GitHub Actions Workflows
Section titled “GitHub Actions Workflows”ci.yml — Pull Request and Push Checks
Section titled “ci.yml — Pull Request and Push Checks”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:coveragepublish.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-mcpwhich usesnpm publish --workspaces, this is a single package sonpm publishsuffices.
Branch Protection Rules
Section titled “Branch Protection Rules”Configure on Arda-cards/api-proxy repository, main branch:
| Rule | Setting |
|---|---|
| Require pull request before merging | Yes |
| Required approvals | 1 |
| Require status checks to pass | Yes |
| Required status checks | check (from ci.yml) |
| Require branches to be up to date | Yes |
| Require conversation resolution | Yes |
| Allow force pushes | No |
| Allow deletions | No |
Repository Settings
Section titled “Repository Settings”| Setting | Value |
|---|---|
| Default branch | main |
| Allow merge commits | Yes |
| Allow squash merging | Yes |
| Allow rebase merging | Yes |
| Delete branch on merge | Yes |
package.json
Section titled “package.json”{ "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-istanbulis the only addition beyond the standardapi-mcpdev stack.
tsconfig.json
Section titled “tsconfig.json”{ "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"]}Implementation Phases
Section titled “Implementation Phases”Phase 1 — Shared + Tenant (Reference Implementation)
Section titled “Phase 1 — Shared + Tenant (Reference Implementation)”- Initialize repository:
package.json,tsconfig.json,eslint.config.js,vitest.config.ts,.prettierrc,.gitignore. - Implement
src/shared/—HttpClient,types.ts,errors.ts. - Implement
src/system/tenant.ts+tenant.types.tsas the reference proxy. - Write tests for
HttpClientandTenantProxy. - Set up CI workflows (
.github/workflows/ci.ymlandpublish.yml). - Verify:
npm run format:check && npm run typecheck && npm run lint && npm run test:coverage && npm run build. - Configure branch protection rules on the repository.
Phase 2 — Remaining System Domain
Section titled “Phase 2 — Remaining System Domain”- Implement
user-account,agent-for,invitationproxies + types + tests. - Create domain
index.tsbarrel exports.
Phase 3 — Reference Data + Resources + Procurement
Section titled “Phase 3 — Reference Data + Resources + Procurement”- Implement
business-affiliate,itemproxies + types + tests. - Implement
kanbanproxy + types + tests. - Implement
orderproxy + types + tests. - Create domain
index.tsbarrel exports. - Create root
src/index.tsbarrel export.
Phase 4 — Polish and Publish
Section titled “Phase 4 — Polish and Publish”- Write
README.mdwith usage examples and installation instructions. - Write initial
CHANGELOG.mdentry. - Verify full build + test + publish dry run.
- Merge to
main, verify GitHub Actions publish to GitHub Packages.
Source Material
Section titled “Source Material”api-mcp/packages/shared/src/— base client, types, auth, errorsapi-mcp/packages/tenant-mcp/src/— reference module client and typesdocumentation/.../architecture/patterns/api-design.md— required headers, error formats, query patternsdocumentation/.../functional/api-endpoint-catalog.md— definitive endpoint catalog- Arda API OpenAPI specs at
{host}/v1/{module}/docs/openApi.json - Kotlin source in
operationsandaccounts-componentfor ambiguous type resolution
Copyright: © Arda Systems 2025-2026, All rights reserved