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.
Entry Criteria
Section titled “Entry Criteria”api-proxyworktree onjmpicnic/image-upload-frontendbranch- Code changes already implemented (see
api-proxy-update-to-operations-2.21.md):
createImageUploadUrl()method onItemProxyImageUploadRequest/ImageUploadResponsetypesdeleteDraft(), 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;}apiKeyis set once at construction (singleton for the app’s lifetime).generateRequestIdis an optional factory function; if not provided,HttpClientuses 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 oldX-Author: apiKeyfor auth).X-Author— set tocontext.author(user email) when context provided; falls back toapiKeywhen no context (standalone mode).X-Tenant-IdandX-oidc-subject— sent only when context provided.X-Request-ID— always sent, using the configured generator.Content-Type— configurable per-request; defaults toapplication/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:
HttpClienttests: verifyAuthorization: Bearerheader always present; verifyX-Author/X-Tenant-Id/X-oidc-subjectheaders when context provided; verify fallback to apiKey as X-Author when no context; verify customgenerateRequestIdcalled; verifyX-Request-IDalways present; verify customcontentTypeforwarded.- Each proxy test: verify
optionsparameter forwarded torequest()(can be a single representative test per proxy, not per method).
T-1: Add missing processUploadJob method
Section titled “T-1: Add missing processUploadJob 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
POSTto/upload-job/{jobId} - Verify response parsed as
UploadJobStatus
T-2: Verify all checks
Section titled “T-2: Verify all checks”npm run typechecknpm run lintnpm run testnpm run buildAll must pass.
T-3: Update CHANGELOG
Section titled “T-3: Update CHANGELOG”Add entries per the release-lifecycle skill conventions (Keep a Changelog
format). Categories:
- Added:
RequestContexttype,RequestOptionstype for per-request user context and content-type override.generateRequestIdoption onProxyConfig.processUploadJob()onItemProxy.createImageUploadUrl(),ImageUploadRequest,ImageUploadResponse,deleteDraft(), 8 lookup methods (lookupDepartmentsthroughlookupUsecases),bulkCreate(),bulkUpdate(),BulkUpdateEntry,BulkUpdateRequest,getQueryPage(),getHistoryPage(). - Changed:
HttpClient.request()now sendsAuthorization: Bearerheader (previously sent API key only asX-Author). All proxy methods accept optionalRequestOptionsas last parameter.X-Request-IDheader sent on every request. Minimum Node version raised to>=20.0.0(from>=18.18.0) forcrypto.randomUUID()support.
T-4: Commit and push
Section titled “T-4: Commit and push”Commit all changes + CHANGELOG + version bump. Push to the branch.
T-5: Create PR and publish
Section titled “T-5: Create PR and publish”- 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
T-6: Documentation commit
Section titled “T-6: Documentation commit”- Update session log / byproducts in the
documentationworktree - Run
make pr-checksin thedocumentationworktree - Commit documentation changes referencing Phase 3.1
Exit Criteria
Section titled “Exit Criteria”RequestContextandRequestOptionstypes exported from shared barrelHttpClientsendsAuthorization: Bearer,X-Request-IDon all requestsHttpClientsendsX-Author,X-Tenant-Id,X-oidc-subjectwhenRequestContextprovided- All proxy methods accept optional
RequestOptionsas last parameter processUploadJob()method addedpackage.jsonengines.nodeupdated 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-proxypublished to GitHub Packages at the new version- PR merged to
main - Documentation worktree:
make pr-checkspasses, changes committed
STOP: Verify package is consumable before proceeding to Phase 3.2.
Open Questions and Decisions
Section titled “Open Questions and Decisions”| # | Question | Options | Recommendation | Decision |
|---|---|---|---|---|
| 1 | Node 18 vs 20 for crypto.randomUUID() | A: require Node 20+ and use crypto.randomUUID(), B: manual UUID v4 for Node 18 compat | A | Decided (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+. |
| 2 | Should RequestOptions be re-exported from each domain barrel? | A: only from shared, B: from each domain barrel too | A | Decided (FD-09): Export from shared only. Callers import RequestOptions from @arda-cards/api-proxy/shared. |
Copyright: (c) Arda Systems 2025-2026, All rights reserved
Copyright: © Arda Systems 2025-2026, All rights reserved