@guardia-technology/sdk
v0.0.10
Published
TypeScript SDK for the Guardia cowork API — typed Result-based HTTP client (ADR-178 thin client).
Readme
@guardia-technology/sdk (TypeScript)
Typed, Result-based HTTP client for the Guardia financial API — the thin client
of ADR-178 (rule 6: no domain, no engine), following the
bounded-context-template sdks/ pattern. Consumed by the UI (in-container).
Scope. Ships the transport, the injectable auth seam, injected-
baseUrlconfig, the error contract, auth providers, and resource clients whose DTOs derive from the OpenAPI contract. The resource surface isfinancialSources(create/list/get),financialRecords(push— STREAM_PUSH batch ingest), anddocuments(batches, files, and pre-signed uploads); further resources (schemas,counterparties) follow the OASpaths.
Base URL (injected)
baseUrl is caller-supplied configuration — the package ships no internal
hosts. It resolves: explicit option > $GRD_BASE_API_URL > public default
https://api.guardia.technology. What each deployment injects:
| Caller | what to set |
| ------ | ----------- |
| external / public | nothing — uses the public default |
| service-to-service (VPC) | $GRD_BASE_API_URL=https://api.guardia.technology.intra (from the deployment env) |
| in-container (SSR/MCP) | $GRD_BASE_API_URL=http://localhost:… |
CallerPosition is kept only as the auth-strategy axis for #752 — it does not
resolve the URL.
Usage
import { GuardiaClient } from "@guardia-technology/sdk";
const client = new GuardiaClient({ baseUrl: "https://api.guardia.technology" });
const result = await client.financialSources.list({ pageSize: 20 });
if (result.ok) console.log(result.value.data.length);Omit baseUrl to use $GRD_BASE_API_URL / the public default. Inject a custom fetchImpl
(proxy/retry/test) or an auth provider via the constructor.
Resources
The client exposes one typed subclient per API resource as a lazy, cached
accessor. Every method returns Result<DTO, RequestError> and never throws on a
documented failure path. State-changing methods take an optional
{ idempotencyKey } forwarded as the Idempotency-Key header.
| Accessor | Path | Methods |
| -------- | ---- | ------- |
| client.financialSources | /v1/financial-sources | create, list, get |
| client.financialRecords | /financial/v1/financial-records | push |
| client.documents | /documents/v1/* | uploadBatch, createBatch, createDocument(s), getBatch, listBatchFiles, getBatchFile + .batches / .files / .storage |
financialRecords.push ingests a batch of 1–500 records and returns
PushRecordsAccepted (failedRecordCount). The idempotencyKey is required —
the gateway derives each record's event id from it, so a retry is safe
(lex-idempotency):
const result = await client.financialRecords.push(
{ records: [{ data: recordIngest }] },
{ idempotencyKey: crypto.randomUUID() },
);
if (result.ok) console.log(result.value.failed_record_count);import { GuardiaClient } from "@guardia-technology/sdk";
const client = new GuardiaClient({ baseUrl: "https://api.guardia.technology" });
const result = await client.financialSources.get("0199b9f1-3a4b-7c8d-9e0f-1a2b3c4d5e6f");
if (result.ok) {
console.log(result.value.data.code); // e.g. "corporate-checking-statement-feed"
} else {
console.error(result.error.code); // e.g. "ERR404_NOT_FOUND"
}
await client.financialSources.create(
{ name: "Corporate Checking", code: "corporate-checking" },
{ idempotencyKey: crypto.randomUUID() },
);client.documents covers batches, files, and uploads through both a high-level
workflow and the thin per-resource surface.
The documents write path sets a
User-Agent(server-required) and is server/Node-targeted (the BFF intra position —User-Agentis a forbidden header in the browser Fetch spec). Do not call the write path from a browser.
High-level workflow
uploadBatch runs the full flow — create the batch, then register and upload each
file with bounded concurrency (maxConcurrency, default 5). The batch
Idempotency-Key is derived from the filenames (a retry collapses onto the same
batch), and per-file failures live in the result aggregate rather than a global
error (partial-success): the aggregate is err(...) only when batch creation or the
inputs themselves fail.
const uploaded = await client.documents.uploadBatch({
files: [{ filename: "invoice", fileExtension: "pdf", body: bytes }],
// optional: callbackUrl, metadata, externalEntityId, idempotencyKey, maxConcurrency, userAgent
});
if (uploaded.ok) {
const { batch, successfulDocuments, failures, isFullySucceeded, isPartiallyFailed } = uploaded.value;
if (isPartiallyFailed) console.warn(failures.length, "file(s) failed");
}To register without uploading bytes, create the batch then create the documents (the
returned Document carries the pre-signed POST material the caller uploads itself).
createBatch requires an explicit idempotencyKey; createDocuments is
partial-success like uploadBatch:
const batch = await client.documents.createBatch({
expectedFilesCount: 2,
idempotencyKey: crypto.randomUUID(),
});
if (batch.ok) {
const registered = await client.documents.createDocuments(batch.value.entity_id, {
files: [
{ filename: "invoice", fileExtension: "pdf" },
{ filename: "statement", fileExtension: "ofx" },
],
});
// registered.value.successfulDocuments[i].pre_signed_url / .pre_signed_fields
}Reads — getBatch, getBatchFile, and listBatchFiles (keyset pagination, ADR-0053).
Cursors are opaque: feed pagination.next_cursor straight into the next pageToken,
and keep pageSize fixed across the walk (changing it mid-walk is a 400):
let pageToken: string | undefined;
do {
const page = await client.documents.listBatchFiles(batchId, { pageSize: 50, pageToken });
if (!page.ok) break;
for (const file of page.value.data) console.log(file.entity_id);
pageToken = page.value.pagination.next_cursor ?? undefined;
} while (pageToken);Thin resources
| Accessor | Path | Methods |
| -------- | ---- | ------- |
| client.documents.batches | /documents/v1/batches | create, get |
| client.documents.files | /documents/v1/batches/{id}/files | createInBatch, list, get, update (PATCH) |
| client.documents.storage | pre-signed POST target | upload |
files.update is a partial (RFC 7386) update and, like every state-changing call,
takes a required idempotencyKey:
await client.documents.files.update(
batchId,
fileId,
{ metadata: { reviewed: true } },
{ idempotencyKey: crypto.randomUUID() },
);Idempotency.
uploadBatchderives the batch key from the filenames and each per-file key from(batchId, filename.ext), so a retried upload collapses onto the same batch and documents. Pass an explicitidempotencyKeyon any file to override the derived one.createBatchrequires the key explicitly (no filenames to derive from).
Types derive from the OpenAPI contract
DTOs and enums are derived from the API contract
(components/deployables/api/docs/oas.yaml) via
openapi-typescript — not hand-written (ADR-204). The
generated module is src/resources/openapi.ts; friendly aliases
(FinancialSourceResponse, EntityId, Pagination, …) are re-exported from
src/resources/models.ts. Regenerate after a contract change:
npm run generate:typesDevelop
npm install
npm run typecheck
npm test
npm run build