@taphubhq/sdk-server
v0.9.0
Published
Node-only SDK for Taphub server-to-server API with HMAC authentication
Downloads
101
Readme
@taphubhq/sdk-server
Node-only SDK for Taphub's server-to-server API with HMAC authentication.
Installation
npm install @taphubhq/sdk-server
# or
pnpm add @taphubhq/sdk-serverThis package is for Node.js only. It must not be imported from browser or edge runtimes. The constructor will throw if window is defined.
Configuration
All five fields are required. The SDK does not read environment variables internally — you control how credentials are provided.
import { TaphubServer } from '@taphubhq/sdk-server';
const taphub = new TaphubServer({
apikey: process.env.TAPHUB_APIKEY!,
agencyId: process.env.TAPHUB_AGENCY_ID!,
agencySecret: process.env.TAPHUB_AGENCY_SECRET!,
builderCode: process.env.TAPHUB_BUILDER_CODE!,
endpoint: process.env.TAPHUB_ENDPOINT!, // e.g. https://api.taphub.io
});| Field | Purpose |
|----------------|--------------------------------------------------------------------------------------------------------------------------|
| apikey | Builder API key. Sent as x-api-key header on REST + GraphQL, AND as apikey field in REST request bodies. |
| agencyId | Agency UUID. Sent as agency_id field in REST request bodies, and used as a variable on auth.userLogin. |
| agencySecret | HMAC signing secret for REST calls. Never logged or serialized. |
| builderCode | Builder identifier (human-readable handle). Sent as x-builder-code header on REST + GraphQL. |
| endpoint | Base URL of the TapHub API. Must be https:// (or http://localhost). |
REST transport authentication layers
REST methods (wallet.*, user.create, ping) send S2S credentials on two layers simultaneously, because the backend enforces both:
- Headers —
x-api-keyandx-builder-codeare read by theBuilderAuthmiddleware, which resolves the agency UUID fromx-builder-codevia its in-process cache and exposes it to controllers that read the agency from middleware context (e.g.wallet.getTxByRef's route). - Body —
apikeyandagency_idare required by every Agency request VO'sbinding:"required"tags (controllerShouldBindJSONstep). Some controllers also readagency_idfrom the body directly (e.g.wallet.transfer,wallet.getUserWallet,wallet.listUserWallets).
Authorization: HMAC <sig> is computed over the full JSON body including the SDK-appended apikey and agency_id. Do not modify the body after the SDK constructs it.
The endpoint must use https:// (or http://localhost for local development). Plain http:// URLs are rejected.
Operations
taphub.auth.userLogin(sessionToken)
Exchange an opaque builder session token (issued by your own backend after the user logs in) for a TapHub access token. The resulting accessToken is a JWT the frontend can hand to @taphubhq/sdk-core to make authenticated GraphQL calls.
const { accessToken, user } = await taphub.auth.userLogin('abc123');
// user: { id, name, currency }
// Return accessToken to your frontend; sdk-core will send it as Authorization: Bearer <jwt>Errors:
TaphubValidationError(codeInvalidPayload) —sessionTokenwas missing, empty, or not a string.TaphubAuthError— TapHub rejected the session (codes includeTokenExpired,Unauthorized,NotAuthenticated).TaphubValidationError— other TapHub-side error with the originalcodepreserved on the error.TaphubServerError/TaphubInternalError— TapHub returned a 5xx or an unparseable response.
This method uses a GraphQL transport (POST {endpoint}/query) and sends x-builder-code + x-api-key headers. It does not sign requests with HMAC and is not subject to the SDK's retry policy — userLogin always issues exactly one request per call.
taphub.auth.loginS2S({ agencyUid, name? })
Server-to-server login. Use this from a builder backend when you want to mint a TapHub user accessToken directly from your own server identity — no Privy session token, no frontend round-trip. The user is keyed by agencyUid (the builder's own user identifier); pass name on first login to seed the display name.
const { accessToken, user } = await taphub.auth.loginS2S({
agencyUid: 'user-123',
name: 'Alice', // optional, used on user creation
});
// user: { id, name, currency }Errors:
TaphubValidationError(codeInvalidPayload) —inputmissing/non-object,agencyUidmissing/empty/non-string, ornameprovided as non-string.TaphubAuthError— HMAC signature rejected, missing agency context, orUnauthorized/TokenExpired/NotAuthenticatedGraphQL error code.TaphubValidationError— other TapHub-side error with the originalcodepreserved on the error.TaphubServerError/TaphubInternalError— TapHub returned a 5xx or an unparseable response.
This method uses the HMAC GraphQL transport (POST {endpoint}/query with Authorization: HMAC <sig> signed over the request body using agencySecret). It requires agencyId, agencySecret, builderCode, and apikey from the constructor — identical to other HMAC module calls (user.create, wallet.transfer). Like userLogin, it is not subject to the SDK's retry policy.
When to use which:
userLogin(sessionToken)— your frontend already has a Privy session for the user; your backend forwards the session token to TapHub.loginS2S({ agencyUid })— your backend is the source of truth for the user identity and just needs a TapHubaccessTokento act on their behalf.
taphub.user.create({ agencyUid, username })
Provision a user under your agency.
const user = await taphub.user.create({
agencyUid: 'user-123',
username: 'alice',
});taphub.wallet.transfer({ ref, agencyUid, amount, direction, currency })
Transfer funds to or from a user's wallet. ref is a mandatory idempotency key — use crypto.randomUUID() or a stable external reference.
import { randomUUID } from 'node:crypto';
const result = await taphub.wallet.transfer({
ref: randomUUID(),
agencyUid: 'user-123',
amount: '10.00',
direction: 'deposit',
currency: 'USDC',
});direction must be 'deposit' or 'withdraw'. amount must be a positive decimal string (not a number, and not prefixed with -) — the SDK negates it on the wire when direction === 'withdraw'. currency is required and must match a currency supported by your agency configuration.
Wire shape: the SDK posts { apikey, agency_id, tx_ref, agency_uid, amount, currency } to POST /api/user/v1/agency/transfer. The backend derives deposit/withdraw from the sign of amount; there is no direction field on the wire.
Result shape (WalletTransferResult): { id, walletId, userId, amount, beforeAmount, afterAmount, ref, timestamp }. amount carries the signed wire value (negative for withdraw). currency, type, and status are intentionally not on this response — use wallet.getTxByRef({ ref }) if you need them.
Wallet lookups
The SDK provides three read-only wallet methods. They use the same HMAC transport as wallet.transfer and return camelCase-typed objects (no snake_case keys leak to the caller).
taphub.wallet.getUserWallet({ agencyUid })
Fetch a user's default wallet for your agency's configured currency.
const wallet = await taphub.wallet.getUserWallet({ agencyUid: 'user-123' });
// wallet: { id, amount, currency, isEnabled }taphub.wallet.listUserWallets({ agencyUid })
List all wallets for a user. In single-currency setups (v0.2) this typically returns one entry.
const wallets = await taphub.wallet.listUserWallets({ agencyUid: 'user-123' });
// wallets: Wallet[]taphub.wallet.getTxByRef({ ref })
Look up a wallet transaction by its reference key. This uses the same ref value you pass to wallet.transfer — pass the ref from a transfer to retrieve the transaction details.
const tx = await taphub.wallet.getTxByRef({ ref: 'my-ref-123' });
// tx: { id, walletId, userId, amount, beforeAmount, afterAmount, currency, ref, source, type }If the transaction is not found, the SDK throws TaphubNotFoundError with the backend error code preserved (e.g., 'TransactionNotFound').
v0.2 note: These methods do not accept a currency parameter. The backend infers the currency from your agency configuration. Multi-currency support is planned for a future version.
Exported types
import type { Wallet, WalletTransaction } from '@taphubhq/sdk-server';taphub.ping()
Connectivity check against the Taphub API.
const response = await taphub.ping();Security
Do not intercept the request body
The SDK signs the request body with HMAC-SHA256. The same bytes that are signed are sent on the wire. If you modify the body (e.g., via middleware that re-serializes JSON), the signature will not match and the server will reject the request with an authentication error.
Ensure that any HTTP client or proxy between your application and Taphub transmits the request body verbatim.
Secret redaction
agencySecret is stored as a non-enumerable property. JSON.stringify(client) returns '[REDACTED]' in place of the secret. Error messages never include the secret or the computed signature.
Replay risk (v0.1)
In v0.1, each signed request is valid indefinitely if replayed. This will be addressed in v0.2 with a timestamp and nonce in the HMAC payload, coordinated with a backend change. In the meantime, use ref-based idempotency for mutation endpoints and ensure that your agency's IP whitelist is configured in the Taphub dashboard.
Error handling
The SDK throws typed error classes:
| Class | HTTP Status |
|---|---|
| TaphubValidationError | 400, 422 |
| TaphubAuthError | 401, 403 |
| TaphubNotFoundError | 404 |
| TaphubConflictError | 409 |
| TaphubInternalError | 500–599, unmapped |
| TaphubRateLimitError | 429 |
| TaphubNetworkError | DNS/TCP/TLS failures |
TaphubRateLimitError and TaphubInternalError carry an optional retryAfterMs?: number parsed from the Retry-After response header when present.
All errors extend TaphubServerError and include code (backend error tag or HTTP_<status>) and status (HTTP status when available).
import { TaphubAuthError } from '@taphubhq/sdk-server';
try {
await taphub.ping();
} catch (err) {
if (err instanceof TaphubAuthError) {
console.error('Auth failed:', err.code, err.status);
}
}Retry
Starting in 0.3.0, the SDK retries transient failures automatically with full-jitter exponential backoff.
Defaults
| Field | Default | Description |
|---|---|---|
| maxAttempts | 3 | Total attempts including the first |
| baseDelay | 200 ms | Base for exponential backoff |
| maxDelay | 5000 ms | Upper bound per sleep |
Trigger matrix
| Error class | HTTP context | Retry? |
|---|---|---|
| TaphubNetworkError | fetch failure | ✅ |
| TaphubInternalError | 5xx | ✅ |
| TaphubRateLimitError | 429 | ✅ |
| TaphubAuthError | 401, 403 | ❌ |
| TaphubValidationError | 400, 422 | ❌ |
| TaphubNotFoundError | 404 | ❌ |
| TaphubConflictError | 409 | ❌ |
Sync validation errors (thrown before any HTTP request) bypass the retry loop.
Backoff formula
delay = random_between(0, min(maxDelay, baseDelay * 2 ** attemptIndex))attemptIndex is 0-based for the first scheduled retry.
Retry-After honoring
If a failed response carries a Retry-After header (integer seconds or HTTP-date), the SDK uses that value (clamped to maxDelay) for the next attempt instead of the computed backoff. No jitter is applied to a server-supplied delay.
Disable per call
await taphub.wallet.transfer(
{ ref: 'r-1', agencyUid: 'u-1', amount: '1.00', direction: 'deposit', currency: 'USDC' },
{ retry: false },
);Disable for the whole client
const taphub = new TaphubServer({ apikey, agencyId, agencySecret, endpoint, retry: false });Observability with onRetry
const taphub = new TaphubServer({
apikey, agencyId, agencySecret, endpoint,
retry: {
onRetry: ({ attempt, error, nextDelayMs }) => {
console.warn(`retry attempt ${attempt} after ${error.code}, sleeping ${nextDelayMs}ms`);
},
},
});Errors thrown inside onRetry are caught and ignored — the retry loop always continues.
Mutation retry safety
Retry applies uniformly to mutations and reads. The backend dedupes by idempotency keys:
wallet.transferdedupes byref.user.creatededupes byagencyUid.
A duplicate request surfaces as TaphubConflictError (409) — a deterministic, recognizable signal.
Upgrading from 0.2
In 0.2.x the SDK issued exactly one attempt per call. 0.3.0 enables retry by default. To preserve the previous fail-fast behavior, pass retry: false to the constructor or to individual calls.
HMAC algorithm
The SDK uses HMAC-SHA256 over the raw UTF-8 bytes of the JSON request body, hex-encoded as a 64-character string. This matches the algorithm in grid-user-service/pkg/cryptography/hmac.go. See the design doc for details.
License
MIT
