Package Exports
- tracing-platform-sdk
Readme
tracing-platform-sdk
Official Node.js/TypeScript SDK for the Tracing Platform — DID-based supply chain traceability built on EPCIS 2.0 and W3C DID standards.
Installation
npm install tracing-platform-sdkRequirements: Node.js >= 18, zero production dependencies.
Quick Start
import { TracingClient } from 'tracing-platform-sdk';
const client = new TracingClient({
baseUrl: 'https://api.tracing.sotatek.works',
clientId: process.env.TRACING_CLIENT_ID!,
clientSecret: process.env.TRACING_CLIENT_SECRET!,
});
// List products (page 1)
const { data, meta } = await client.products.list({ page: 1, limit: 20 });
console.log(`${meta.total} total products`);
// Stream all pages
for await (const page of client.products.paginate()) {
console.log(page); // FooItem[]
}Architecture
The SDK is organized as a layered DI container:
TracingClient ← consumer entry point
├── OAuthManager ← token lifecycle: fetch, cache, refresh, dedup
├── HttpClient ← fetch wrapper: auth injection, retry, error mapping
└── 14 Feature Modules ← one class per domain, each receives HttpClientTracingClient wires everything together and exposes modules as client.products, client.commission, etc. No global state — each TracingClient instance is fully isolated.
Configuration
const client = new TracingClient({
baseUrl: 'https://api.tracing.sotatek.works', // required
clientId: process.env.TRACING_CLIENT_ID!, // required
clientSecret: process.env.TRACING_CLIENT_SECRET!, // required
timeout: 30_000, // per-request timeout in ms (default: 30000)
maxRetries: 3, // retry attempts on 429/5xx (default: 3)
debug: false, // log METHOD URL → STATUS (Xms) (default: false)
});Authentication
The SDK implements OAuth2 Client Credentials (RFC 6749 §4.4) via OAuthManager:
POST {baseUrl}/api/v1/oauth/token
{ "client_id": "...", "client_secret": "...", "grant_type": "client_credentials" }
→ { "access_token": "...", "expires_in": 3600, "token_type": "Bearer" }Token lifecycle:
- Token is fetched lazily on the first API call
- Cached in memory; reused until
expires_in - 60s(auto-refresh before expiry) - Concurrent calls share a single in-flight token request — no duplicate fetches
- On
401, the cached token is invalidated and one retry is attempted automatically
Obtain credentials from the Enterprise Portal → Settings → API Credentials.
HTTP Client
All requests go through HttpClient which handles:
| Feature | Detail |
|---|---|
| Base path | All requests prefixed with /api/v1 |
| Auth injection | Authorization: Bearer {token} added automatically |
| Retry | 429, 500, 502, 503, 504 — exponential backoff: 1s → 2s → 4s … max 16s |
| Idempotency | POST requests auto-generate a UUID Idempotency-Key header |
| Timeout | Per-request AbortController (default 30s, configurable) |
| Debug logging | [tracing-platform-sdk] GET /api/v1/products → 200 (142ms) — token redacted |
Modules
client.auth — Authentication Status
const status = await client.auth.status();
// { connected: true, expiresAt: 1716290400000, scopes: ['products.read', ...] }client.products — Product Registry
// List products
const { data, meta } = await client.products.list({ page: 1, limit: 20 });
// Get product detail
const product = await client.products.get('product-id');
// Create a product
const newProduct = await client.products.create({
name: 'Organic Coffee',
gtin: '00012345600012',
unit: 'kg',
});
// Search lots
const lots = await client.products.listLots('product-id', { page: 1 });client.commission — LOT Genesis Events
Commission events record the creation (genesis) of a LOT in the supply chain.
const event = await client.commission.create({
productId: 'prod-123',
lotCode: 'LOT-2026-001',
quantity: 1000,
unit: 'kg',
producedAt: '2026-01-15T08:00:00Z',
});
// Paginate all commission events
for await (const page of client.commission.paginate({ productId: 'prod-123' })) {
page.forEach(e => console.log(e.lotCode));
}client.aggregation — Pack / Unpack Containers
// Pack items into a container
await client.aggregation.pack({
parentId: 'container-sscc-001',
childIds: ['lot-a', 'lot-b', 'lot-c'],
});
// Unpack a container
await client.aggregation.unpack({ parentId: 'container-sscc-001' });client.shipping — Shipment Tracking
// Create shipment
const shipment = await client.shipping.create({
lotIds: ['lot-001', 'lot-002'],
fromLocationId: 'loc-warehouse',
toLocationId: 'loc-distribution',
shippedAt: '2026-02-01T09:00:00Z',
});
// Get Advanced Shipping Notice (ASN)
const asn = await client.shipping.getAsn(shipment.id);client.receiving — QC & Custody Transfer
// Receive and QC lots
await client.receiving.receive({
shipmentId: 'ship-001',
receivedAt: '2026-02-02T14:00:00Z',
qcStatus: 'passed',
notes: 'All items verified',
});client.transformation — Input LOTs → Output LOT
Records processing events where input lots are consumed to produce output lots.
await client.transformation.create({
inputLotIds: ['raw-lot-001', 'raw-lot-002'],
outputLot: {
productId: 'finished-product-id',
lotCode: 'FINISHED-001',
quantity: 500,
unit: 'kg',
},
transformedAt: '2026-03-01T10:00:00Z',
});client.recall — Recall Alerts
// Create a recall alert
const recall = await client.recall.create({
lotIds: ['lot-001', 'lot-002'],
reason: 'Contamination detected',
severity: 'high',
});
// List active recalls
const { data } = await client.recall.list({ status: 'active' });client.traceability — LOT Timeline & Trace
// Full event timeline for a lot
const timeline = await client.traceability.getTimeline('lot-001');
// Trace upstream (find source lots)
const upstream = await client.traceability.traceUpstream('lot-001');
// Trace downstream (find where it went)
const downstream = await client.traceability.traceDownstream('lot-001');client.anchor — Blockchain Anchoring
// Get blockchain anchor status for a lot
const anchor = await client.anchor.getStatus('lot-001');
// { anchored: true, txHash: '0xabc...', chain: 'ethereum', anchoredAt: '...' }client.custody — Custody Chain
// Get custody overview for a product
const overview = await client.custody.getOverview('product-id');
// Get custody stats by product
const stats = await client.custody.getStats({ productId: 'prod-123' });client.didProfile — DID Identity Profiles
// Get DID profile for an organization
const profile = await client.didProfile.get('did:example:123');
// Update profile
await client.didProfile.update('did:example:123', {
name: 'Acme Farms',
certifications: ['organic', 'fair-trade'],
});client.members — Enterprise Members
// List organization members
const { data } = await client.members.list();
// Invite a member
await client.members.invite({ email: 'user@example.com', role: 'operator' });client.webhook — Webhook Events
// List configured webhooks
const webhooks = await client.webhook.list();
// Create a webhook endpoint
await client.webhook.create({
url: 'https://myapp.com/webhooks/tracing',
events: ['commission.created', 'recall.created'],
secret: 'my-signing-secret',
});Pagination
Every list endpoint exposes a paginate() async generator that handles page iteration automatically:
// Iterate page by page (memory efficient)
for await (const page of client.products.paginate({ limit: 50 })) {
for (const product of page) {
await process(product);
}
}
// Or use PaginationHelper for more control
import { PaginationHelper } from 'tracing-platform-sdk';
const helper = new PaginationHelper((page, limit) =>
client.products.list({ page, limit })
);
// Fetch a single page
const page1 = await helper.fetchPage(1, 20);
// Fetch all at once (caution: large datasets)
const all = await helper.fetchAll(100);
// Get pagination metadata only
const meta = await helper.meta(1);
// { total: 1500, page: 1, limit: 1, totalPages: 1500 }Error Handling
All errors extend TracingError and include a statusCode:
TracingError (base — statusCode: number)
├── AuthError 401 — invalid or expired credentials
├── ForbiddenError 403 — insufficient permissions
├── NotFoundError 404 — resource not found
├── ValidationError 422 — request validation failed (includes field errors)
├── RateLimitError 429 — too many requests (includes retryAfter)
└── ServerError 5xx — platform-side errorimport {
TracingError, AuthError, ForbiddenError, NotFoundError,
ValidationError, RateLimitError, ServerError,
} from 'tracing-platform-sdk';
try {
await client.products.create({ name: '' });
} catch (err) {
if (err instanceof ValidationError) {
// err.fields: Record<string, string[]>
console.error('Field errors:', err.fields);
// { name: ['Name is required'], gtin: ['Invalid GTIN format'] }
} else if (err instanceof RateLimitError) {
// err.retryAfter: number (seconds)
await sleep(err.retryAfter * 1000);
// retry...
} else if (err instanceof AuthError) {
console.error('Check your clientId and clientSecret');
} else if (err instanceof TracingError) {
console.error(`HTTP ${err.statusCode}: ${err.message}`);
}
}OAuth Scopes
Use SDK_SCOPES to reference scope strings safely:
import { SDK_SCOPES, SdkScope } from 'tracing-platform-sdk';
const requiredScopes: SdkScope[] = [
SDK_SCOPES.PRODUCTS_READ, // 'products.read'
SDK_SCOPES.COMMISSION_WRITE, // 'commission.write'
SDK_SCOPES.TRACEABILITY_READ, // 'traceability.read'
];
// All available scopes:
// commission.read/write | aggregation.read/write | shipping.read/write
// receiving.read/write | transformation.read/write | recall.read/write
// products.read/write | traceability.read | anchor.read
// custody.read | did-profile.read/write | members.read/writeDebug Mode
const client = new TracingClient({ ..., debug: true });
// Console output:
// [tracing-platform-sdk] GET /api/v1/products → 200 (142ms)
// [tracing-platform-sdk] POST /api/v1/commission → 201 (89ms)
// Authorization header is automatically redacted in logsLicense
MIT