@thumbmarkjs/management
v0.1.3
Published
TypeScript SDK for the Thumbmark Management API — programmatic access to allowed-hostnames and other account resources.
Readme
@thumbmarkjs/management
TypeScript SDK for the Thumbmark Management API.
Provides programmatic access to the Thumbmark Admin API from Node.js or any
runtime that supports the native fetch API.
Install
npm install @thumbmarkjs/managementRequirements
- Node.js ≥ 18 (uses native
fetch— no polyfill needed) - Zero runtime dependencies
Authentication
Create a management secret key in the Thumbmark Admin Console at
admin.thumbmarkjs.com/management-api
→ Create Management Secret. The key looks like tmsec_live_… and is shown
only once, at creation — copy it immediately, as it cannot be retrieved later. Keep it secure: it grants write access to your account's allowed-hostnames
list.
Secrets are created in the console only. A management secret cannot create or manage other secrets, so there is no way to mint one programmatically with this SDK — you must be signed in to the console.
import { createManagementClient } from '@thumbmarkjs/management';
const client = createManagementClient({
apiKey: process.env.THUMBMARK_MGMT_SECRET!, // tmsec_live_…
});The key is sent as Authorization: Bearer <apiKey> on every request. No
client-side hashing is applied.
Options
const client = createManagementClient({
apiKey: 'tmsec_live_…', // required
timeoutMs: 10_000, // optional, default 10 s
retry: { maxRetries: 2 }, // optional, default 2 retries
});- Retries are applied automatically to transient failures — network errors,
timeouts, and
5xx— with exponential backoff. Every operation here is idempotent (GET / PUT / DELETE), so a retry never causes a double mutation.4xxresponses are never retried.
Operations
Hostname semantics
- Account-wide. The allowed list applies to all API keys on your account, not a single key.
- Subdomains are included. Adding
example.comalso allowsapp.example.com,api.example.com, and any other subdomain. Wildcards are not supported (and aren't needed).- Bare hostnames only. Scheme, port, and path are stripped and the value is lowercased server-side (
https://Example.com:443/path→example.com).- Up to 1000 hostnames per account.
client.allowedHostnames.list(options?)
Lists the allowed hostnames for the authenticated account, one page at a time.
// First page
const { items } = await client.allowedHostnames.list();
console.log(items); // ['example.com', 'app.example.com', ...]
// Walk every page (follow nextCursor until it's absent)
const all: string[] = [];
let cursor: string | undefined;
do {
const page = await client.allowedHostnames.list({ cursor });
all.push(...page.items);
cursor = page.nextCursor;
} while (cursor);Returns { items: string[]; nextCursor?: string }. Each page's items are
sorted alphabetically, but ordering is not guaranteed across pages — when
paginating, do not rely on a globally sorted sequence (collect all pages and
sort yourself if you need that). When nextCursor is absent this is the final
page.
client.allowedHostnames.add(hostname)
Adds a hostname to the account's allowed list. This is an idempotent upsert — adding a hostname that is already present resolves without error.
Resolves { status }, the HTTP status the API returned, so you can tell the
two cases apart:
const { status } = await client.allowedHostnames.add('example.com');
// status === 201 -> hostname newly added
// status === 204 -> hostname was already present (no-op)Throws ValidationError (HTTP 400) if the hostname is invalid (e.g., wildcard
format) or would exceed the 1 000-hostname cap.
client.allowedHostnames.remove(hostname)
Removes a hostname from the account's allowed list. Resolves { status }.
try {
const { status } = await client.allowedHostnames.remove('old.example.com');
// status === 204 -> hostname existed and was removed
} catch (err) {
if (err instanceof NotFoundError) {
// err.status === 404 -> hostname was not in the allowed list
}
}Throws NotFoundError (HTTP 404) if the hostname was not present. This
differs from add() — by default remove() is not idempotent on a missing
hostname.
Pass { idempotent: true } to make it idempotent: a hostname that is already
absent resolves { status: 204 } instead of throwing.
// Ensure the hostname is gone; don't care whether it was there.
const { status } = await client.allowedHostnames.remove('old.example.com', {
idempotent: true,
});
// status === 204 — whether it existed and was removed, or was already absentStatus codes
The HTTP status (on { status }, or error.status for the thrown case) lets
you confirm exactly which outcome occurred:
| Operation | Outcome | Status |
| ----------------------------------------------- | -------------------------- | ------ |
| add() a hostname not yet present | newly added | 201 |
| add() a hostname already present | already present (no-op) | 204 |
| remove() a hostname that existed | removed | 204 |
| remove() a hostname not present | NotFoundError (rejected) | 404 |
| remove(…, { idempotent: true }) a missing one | no-op (not thrown) | 204 |
Error classes
All errors are exported and can be caught with instanceof.
| Class | HTTP status | When thrown |
| --------------------- | ----------- | ------------------------------------------------------------------ |
| NotFoundError | 404 | remove() of a hostname not in the allowed list |
| ValidationError | 400 | Invalid hostname, malformed cursor, or 1 000-hostname cap exceeded |
| AuthenticationError | 401 / 403 | Missing, invalid, or revoked API key |
| ServerError | 5xx | Internal server error |
| NetworkError | — | DNS failure, connection refused, etc. (before any HTTP response) |
| TimeoutError | — | Request exceeded timeoutMs |
| ConfigurationError | — | Thrown synchronously by createManagementClient for bad options |
| ManagementApiError | any | Base class for all HTTP error classes above |
All HTTP error classes extend ManagementApiError and expose:
.status— HTTP status code.code— machine-readable error code string.body— raw parsed response body
Example: catching specific errors
import {
createManagementClient,
NotFoundError,
AuthenticationError,
} from '@thumbmarkjs/management';
const client = createManagementClient({
apiKey: process.env.THUMBMARK_MGMT_SECRET!,
});
try {
await client.allowedHostnames.remove('maybe-missing.example.com');
} catch (err) {
if (err instanceof NotFoundError) {
console.log('Hostname was not in the list');
} else if (err instanceof AuthenticationError) {
console.log('Check your API key');
} else {
throw err;
}
}License
MIT
