npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@dtfoundry/foundry-sdk

v0.1.0

Published

Typed client for the Digital Trust Foundry Platform partner API

Downloads

164

Readme

@dtfoundry/foundry-sdk

Typed JavaScript client for the Digital Trust Foundry Platform partner API.

It is a transport layer over one tenant's Platform instance: auth header, envelope normalization, typed errors, retries, idempotency and the async-completion loop. It resolves no tenants, stores no credentials and caches no business objects.

npm install @dtfoundry/foundry-sdk

Requires Node 20 or newer. ESM only.

Getting started

import { FoundryClient } from "@dtfoundry/foundry-sdk";

const foundry = new FoundryClient(platformBaseUrl, apiKey);

const created = await foundry.createTrustable({
  content_type: "invoice",
  schema: { id: "urn:acme:schema:invoice", version: "1.0.0" },
  subject: { primary: { description: "Invoice 4471" } },
  content: { amount: 1200, currency: "EUR" },
  lifecycle: {
    model: "transaction",
    event: "issue",
    state: "issued",
    effective_time: new Date().toISOString(),
  },
  publication: { status_surface: "tel", visibility: "permissioned" },
  signing_authority: signingAuthority,
  signature_policy: { threshold: 1, required_roles: [] },
  idempotency_key: `${invoiceId}:create`,
});

if (created.kind === "pending") {
  const settled = await foundry.waitForRequest(created.requestId);
  // settled.status is one of completed | failed | timed_out | canceled
}

baseUrl is the platform_base_url delivered at provisioning. It must be https — the credential is a long-lived bearer token, so one plaintext deployment leaks a tenant-wide key permanently. http://localhost is accepted for local development. apiKey is the api_key delivered at provisioning: treat it as opaque, and never send it as X-API-Key, which the API refuses for application credentials.

Rotation is a push from Auth, not a refresh the client performs. When a key is replaced, drop the client and build a new one — a credential mutated in place races the requests already in flight.

Options

| Option | Default | Meaning | | -------------------------- | -------------- | ------------------------------- | | timeoutMs | 30000 | Per attempt, not per call | | maxRetries | 2 | Retries after the first attempt | | userAgent | — | Appended to the SDK's own | | requestIdFactory | random UUID v4 | Per-call X-Request-Id | | onRequest / onResponse | — | Observability hooks | | transport | fetch | Injection seam for tests |

The hooks are a side channel: one that throws does not change the outcome of a call. The credential never appears in either payload.

The surface

| Method | Endpoint | | ----------------- | --------------------------------------------- | | createTrustable | POST /api/v1/trustables | | listTrustables | GET /api/v1/trustables | | getTrustable | GET /api/v1/trustables/{said} | | appendEvent | POST /api/v1/trustables/{said}/events | | getStats | GET /api/v1/trustables/stats | | listSigners | GET /api/v1/trustables/signers | | createChallenge | POST /api/v1/trustables/challenges | | getRequest | GET /api/v1/trustables/requests/{requestId} | | listRequests | GET /api/v1/trustables/requests | | verify | POST /api/v1/trustables/{said}/verify | | getIssuerAid | GET /api/v1/tenant/issuer-aid | | getBranding | GET /api/v1/tenant/branding |

waitForRequest(requestId, { timeoutMs, signal }) polls a request to a terminal status. Every method takes an optional trailing { signal }.

Three things the types cannot tell you

An idempotency key is tenant-wide per operation, and does not include the target. The lookup is keyed on the tenant, the operation and the key — the SAID you are appending to is in neither the key nor the payload hash. The same key with the same body, sent against two different trustables, replays the first append and never touches the second. Derive the key from the target: `${said}:append:${callerId}`. Keys are limited to 255 characters and the client rejects a longer one before sending.

A failed write may still have landed. The server records the idempotency key at the end of the operation, so a retry issued after it accepted the first attempt finds no record and runs the whole thing again — which, on a signature-collecting write, is a second approval prompt on someone's phone. The SDK therefore retries a write only where the request provably never reached the handler, and marks everything else mayHaveLanded:

try {
  await foundry.appendEvent(said, body);
} catch (error) {
  if (error instanceof FoundryError && error.mayHaveLanded) {
    // Reconcile — read the trustable, or list requests. Do not re-send.
  }
}

Reads, and verify, retry freely.

A 202 does not mean pending. A multi-signer write always answers 202, and its status is re-read after dispatch, so an all-custodial set that met its threshold comes back 202-and-finished — and a dispatch that failed comes back 202-and-rejected. Meanwhile a message append answers 201 while echoing the trustable's pending_signatures, which belongs to the trustable rather than to that write. So createTrustable and appendEvent return the whole response body with a kind added:

const result = await foundry.appendEvent(said, body);
switch (result.kind) {
  case "completed": // finished synchronously
  case "pending": // poll result.requestId
  case "rejected": // a signer declined; read the request for the reason
}

Because the server stamps a caller's lifecycle.state verbatim, pending_signature, pending_signatures and signature_rejected are reserved: a write naming one of them would have its own outcome misread. The client refuses them.

Errors

Everything the SDK raises derives from FoundryError, so one catch covers the whole surface: AuthenticationError, InsufficientScopeError, NotFoundError, ValidationError (and its ClientValidationError subtype for checks that ran before anything was sent), ConflictError, ServiceUnavailableError, RateLimitError, TransportError, TimeoutError, CancelledError.

error.credentialRejected means the key is dead — after a rotation, or signed under another instance's secret. Drop the client rather than retrying.

Regenerating the types

Request and response types are generated from the Platform partner OpenAPI document and committed, so consuming the package never runs the generator. After that document changes:

pnpm --filter @dtfoundry/foundry-sdk generate