Skip to content

Specification: Phase 3.1 — API Proxy Publish

Extend @arda-cards/api-proxy with BFF-compatible request context headers, add the missing processUploadJob endpoint, update CHANGELOG, and publish.

  • api-proxy worktree on jmpicnic/image-upload-frontend branch
  • Code changes already implemented (see api-proxy-update-to-operations-2.21.md):
    • createImageUploadUrl() method on ItemProxy
    • ImageUploadRequest / ImageUploadResponse types
    • deleteDraft(), 8 new lookup methods, bulkCreate(), bulkUpdate(), getQueryPage(), getHistoryPage()
    • Unit tests for all (225 tests, all passing)

T-0: RequestContext type and HttpClient header refactoring

Section titled “T-0: RequestContext type and HttpClient header refactoring”

Gap identified: The HttpClient currently sends only X-Author (set to the API key) and Content-Type. The arda-frontend-app BFF routes send 5 headers: Authorization: Bearer ${apiKey}, X-Author (user email), X-Tenant-Id, X-oidc-subject, X-Request-ID. The api-proxy must support this pattern to be usable by the BFF.

New type (src/shared/types.ts):

/** Per-request context for BFF usage. Carries user identity headers. */
export interface RequestContext {
/** User attribution — maps to X-Author header. */
author: string;
/** Tenant isolation — maps to X-Tenant-Id header. */
tenantId: string;
/** OIDC subject — maps to X-oidc-subject header. */
userId: string;
}

ProxyConfig changes (src/shared/http-client.ts):

export interface ProxyConfig {
host: string;
apiKey: string;
/** Optional custom request ID generator. Default: UUID v4. */
generateRequestId?: () => string;
}
  • apiKey is set once at construction (singleton for the app’s lifetime).
  • generateRequestId is an optional factory function; if not provided, HttpClient uses a default UUID v4 generator.

RequestOptions type (new, internal to HttpClient):

interface RequestOptions {
/** Per-request user context. When provided, sends X-Author, X-Tenant-Id,
X-oidc-subject headers. When omitted, X-Author defaults to apiKey
(backward-compatible standalone mode). */
context?: RequestContext;
/** Content-Type for this request. Default: 'application/json'. */
contentType?: string;
}

HttpClient.request() changes:

async request<T>(
method: string,
url: string,
body?: unknown,
options?: RequestOptions,
): Promise<T> {
const headers: Record<string, string> = {
'Authorization': `Bearer ${this.apiKey}`,
'Content-Type': options?.contentType ?? 'application/json',
'X-Request-ID': this.generateRequestId(),
};
if (options?.context) {
headers['X-Author'] = options.context.author;
headers['X-Tenant-Id'] = options.context.tenantId;
headers['X-oidc-subject'] = options.context.userId;
} else {
// Backward-compatible: standalone mode uses apiKey as author
headers['X-Author'] = this.apiKey;
}
const response = await fetch(url, {
method,
headers,
body: body != null ? JSON.stringify(body) : undefined,
});
// ... error handling and response parsing unchanged
}

Key behavior changes:

  • Authorization: Bearer ${apiKey}always sent (replaces old X-Author: apiKey for auth).
  • X-Author — set to context.author (user email) when context provided; falls back to apiKey when no context (standalone mode).
  • X-Tenant-Id and X-oidc-subject — sent only when context provided.
  • X-Request-ID — always sent, using the configured generator.
  • Content-Type — configurable per-request; defaults to application/json.

Propagate RequestOptions to all proxy methods:

Every public method on every proxy class gains an optional last parameter:

// Before:
create(input: ItemInput, params?: TimeCoordinateParams): Promise<ItemRecord>
// After:
create(input: ItemInput, params?: TimeCoordinateParams, options?: RequestOptions): Promise<ItemRecord>

For methods that already have optional parameters, options is appended as the last parameter. Since it’s optional, existing callers compile without changes.

Affected proxy classes: ItemProxy, BusinessAffiliateProxy, TenantProxy, UserAccountProxy, AgentForProxy, InvitationProxy, KanbanProxy, OrderProxy (all 8).

HttpClient convenience methods (create, getById, update, remove, query, queryHistory, subCreate, subUpdate, subRemove, subList, action, getView, lookup) — all gain options?: RequestOptions as last parameter, forwarded to request().

Default UUID v4 generator:

function defaultGenerateRequestId(): string {
return crypto.randomUUID();
}

Uses Node.js crypto.randomUUID() (stable since Node 20). The package engines field is updated to >=20.0.0 in this phase (FD-08).

Export RequestContext and RequestOptions from src/shared/index.ts barrel.

Tests:

  • HttpClient tests: verify Authorization: Bearer header always present; verify X-Author/X-Tenant-Id/X-oidc-subject headers when context provided; verify fallback to apiKey as X-Author when no context; verify custom generateRequestId called; verify X-Request-ID always present; verify custom contentType forwarded.
  • Each proxy test: verify options parameter forwarded to request() (can be a single representative test per proxy, not per method).

The OpenAPI spec at POST /v1/item/upload-job/{job-id} (trigger a CSV upload job for processing) is present in the deployed API but missing from ItemProxy. This is the companion to the existing getUploadJobStatus() method (GET /v1/item/upload-job/{job-id}).

Add to src/reference/item/proxy.ts:

processUploadJob(jobId: string, options?: RequestOptions): Promise<UploadJobStatus> {
return this.client.request("POST", this.client.buildUrl(`/upload-job/${jobId}`), undefined, options);
}

Add unit test to tests/reference/item/proxy.test.ts:

  • Verify POST to /upload-job/{jobId}
  • Verify response parsed as UploadJobStatus
Terminal window
npm run typecheck
npm run lint
npm run test
npm run build

All must pass.

Add entries per the release-lifecycle skill conventions (Keep a Changelog format). Categories:

  • Added: RequestContext type, RequestOptions type for per-request user context and content-type override. generateRequestId option on ProxyConfig. processUploadJob() on ItemProxy. createImageUploadUrl(), ImageUploadRequest, ImageUploadResponse, deleteDraft(), 8 lookup methods (lookupDepartments through lookupUsecases), bulkCreate(), bulkUpdate(), BulkUpdateEntry, BulkUpdateRequest, getQueryPage(), getHistoryPage().
  • Changed: HttpClient.request() now sends Authorization: Bearer header (previously sent API key only as X-Author). All proxy methods accept optional RequestOptions as last parameter. X-Request-ID header sent on every request. Minimum Node version raised to >=20.0.0 (from >=18.18.0) for crypto.randomUUID() support.

Commit all changes + CHANGELOG + version bump. Push to the branch.

  • Create PR to main
  • Run /pr-steward <pr-url> to monitor CI checks, surface reviewer comments, implement fixes, reply to threads, and resolve conversations
  • Merge and verify package is published to GitHub Packages
  • Confirm the published version is installable: npm view @arda-cards/api-proxy versions --registry=https://npm.pkg.github.com
  • Update session log / byproducts in the documentation worktree
  • Run make pr-checks in the documentation worktree
  • Commit documentation changes referencing Phase 3.1
  • RequestContext and RequestOptions types exported from shared barrel
  • HttpClient sends Authorization: Bearer, X-Request-ID on all requests
  • HttpClient sends X-Author, X-Tenant-Id, X-oidc-subject when RequestContext provided
  • All proxy methods accept optional RequestOptions as last parameter
  • processUploadJob() method added
  • package.json engines.node updated to >=20.0.0
  • All tests pass (existing + new context/header tests)
  • Full local checks pass: lint, typecheck, tests, build
  • CHANGELOG updated
  • @arda-cards/api-proxy published to GitHub Packages at the new version
  • PR merged to main
  • Documentation worktree: make pr-checks passes, changes committed

STOP: Verify package is consumable before proceeding to Phase 3.2.

#QuestionOptionsRecommendationDecision
1Node 18 vs 20 for crypto.randomUUID()A: require Node 20+ and use crypto.randomUUID(), B: manual UUID v4 for Node 18 compatADecided (FD-08): Use crypto.randomUUID(). Update engines.node to >=20.0.0. Both arda-frontend-app (Node 20.19) and AWS Amplify (Node 20/22 supported, Node 18 deprecated Sept 2025) are on Node 20+.
2Should RequestOptions be re-exported from each domain barrel?A: only from shared, B: from each domain barrel tooADecided (FD-09): Export from shared only. Callers import RequestOptions from @arda-cards/api-proxy/shared.

Copyright: (c) Arda Systems 2025-2026, All rights reserved