Skip to content

feat(admin): align API contracts and expand coverage - #61

Merged
johnpmitsch merged 7 commits into
mainfrom
dx-7437-align-admin-sdk-contracts-and-add-missing-public-api
Oct 29, 2026
Merged

johnpmitsch merged 7 commits into
mainfrom
dx-7437-align-admin-sdk-contracts-and-add-missing-public-api

Conversation

@johnpmitsch

@johnpmitsch johnpmitsch commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator

tl;dr

Fixes Admin endpoint reads that reject structured request filters or discard response fields. Adds 24 public Admin API operations across Rust, Python, Node.js, and Ruby for security controls, usage and metrics summaries, activity logs, and transactions.

Impact

  • Seven new _with_response methods expose useful mutation payloads while existing methods keep their return values.
  • Adds custom filter patterns, per-IP method and endpoint limits, and endpoint metric percentiles. Endpoint rate-limit updates accept the per-IP flag alone, including explicit false.
  • Summary endpoint dimensions, transaction endpoint IDs, and account-error/transaction endpoint filters use integers. Transaction ID lists preserve null entries. Callers using UUID strings must update these fields.
  • Rust callers must wrap UpdateRateLimitsRequest.rate_limits in Some(...), update affected full struct literals, and handle filter matchers as JSON values. TypeScript matcher arrays are now unknown[]. All four language READMEs include usage and compatibility notes.

Admin API changes

This PR adds support for 24 operations, improves 24 existing operations, and corrects documentation for 5. Only affected routes are listed below. Paths are relative to /v0; slash-separated verbs share the same path.

Status Endpoint(s) What this PR adds or changes
New GET /endpoints/{id}/security/request_filters List whitelist and custom filters with complete patterns.
New GET/POST/PATCH /endpoints/{id}/security/client_certs
DELETE /endpoints/{id}/security/client_certs/{fingerprint}
Read, add, enable/disable, and remove mTLS certificates.
New GET/PUT /endpoints/{id}/security/method_blacklist Read or replace blocked methods; an empty list clears them.
New GET/PUT /endpoints/{id}/security/range_limits Read or replace per-method block ranges; an empty object clears them.
New GET/POST /endpoints/{id}/security/blocked_ips
DELETE /endpoints/{id}/security/blocked_ips/{ip_id}
Read, add, and remove blocked IPs or CIDR ranges.
New GET /usage/credits/summary
GET /usage/rpc/summary
GET /usage/streams/summary
GET /usage/webhooks/summary
Usage totals, supported grouping, and cursor pagination; RPC endpoint groups use integer IDs.
New GET /metrics/traffic/summary
GET /metrics/errors/summary
GET /endpoints/{id}/metrics/errors/summary
Traffic totals and account or endpoint error summaries; endpoint groups and account-error endpoint filters use integer IDs.
New GET /activity_logs
GET /activity_logs/filters
GET /activity_logs/{id}
Activity list, filter metadata, and entry details.
New GET /transactions
GET /transactions/{network}/{hash}
Retained transactions and attempts, cursor pagination, optional payloads; endpoint IDs and filters are integers, ID lists preserve null entries, and heights stay strings.
Changed POST /endpoints
GET /endpoints/{id}
GET /endpoints/{id}/security
Preserve structured filters, pattern fields, blocked IPs, and security flags in nested responses.
Changed PATCH /endpoints/{id}/security_options Accept responseLogging updates.
Changed POST /endpoints/{id}/security/request_filters
PUT /endpoints/{id}/security/request_filters/{request_filter_id}
Add custom-pattern create/update methods; keep existing whitelist methods.
Changed GET/POST /endpoints/{id}/method-rate-limits
PATCH /endpoints/{id}/method-rate-limits/{method_rate_limit_id}
Expose limit_by_ip in requests and responses, including explicit false.
Changed GET /endpoints/{id}/metrics Add optional percentile for latency metrics.
Changed GET /usage/rpc/by-endpoint
GET /usage/rpc/by-method
GET /usage/rpc/by-chain
GET /usage/rpc/by-tag
Retain RPC/data credits and bytes where returned, including method bytes.
Changed POST /endpoints/bulk/status
POST/DELETE /endpoints/bulk/tags
Retain each failed item's error in partial-success responses.
Changed POST /endpoints/{id}/security/tokens
POST /endpoints/{id}/security/referrers
POST /endpoints/{id}/security/ips
POST /endpoints/{id}/security/domain_masks
POST /endpoints/{id}/security/jwts
Add _with_response methods that return created records and IDs. Existing methods keep their returns.
Changed POST /endpoints/{id}/tags Add _with_response method for tag associations; clarify label requirements.
Changed PATCH /endpoints/{id}/rate-limits Add _with_response for effective RPS/RPM/RPD limits. Both methods accept limit_by_ip, including flag-only updates and explicit false.
Docs only GET /endpoints
PATCH /endpoints/{id}
Clarify the page-size cap, mutually exclusive tag filters, and required update label. Runtime behavior stays the same.
Docs only GET /tooling-access Clarify that feature access can return 403.
Docs only GET /endpoints/{id}/logs Clarify ISO timestamp inputs and the 1–100 page-size range.
Docs only POST /teams/{id}/members Document standard roles and account custom-role keys.

npm examples for every added or affected method

These examples cover 62 methods: 33 new methods, changed request/response types, documentation-only updates, and the existing mutation counterparts. Each section uses the setup below. Calls are illustrative alternatives, not a single test workflow; mutation calls change the selected account or endpoint. Replace IDs and PEM placeholders with test-account values. WithResponse calls return the useful payload; the original mutation methods still return void.

import { QuicknodeSdk, ApiError } from "@quicknode/sdk";

const qn = QuicknodeSdk.fromEnv(); // QN_SDK__API_KEY
const admin = qn.admin;
const endpointId = "123";          // String for endpoint path parameters.
const numericEndpointId = 123;     // Integer for summary/transaction filters.
const teamId = 456;
const tagId = 789;
const certificatePem = "<PEM-encoded client certificate>";
const publicKeyPem = "<PEM-encoded JWT verification public key>";

Endpoint reads, updates, and security snapshots

const endpoints = await admin.getEndpoints({ limit: 20, tagLabels: ["sdk-example"] });
// Use tagLabels or tagIds, not both. Maximum page size: 100.
console.log(endpoints.pagination);

const createdEndpoint = await admin.createEndpoint({ chain: "ethereum", network: "mainnet" });
console.log(createdEndpoint.data.id, createdEndpoint.data.security?.requestFilters);

const endpoint = await admin.showEndpoint(endpointId);
console.log(endpoint.data?.security?.options?.responseLogging);

await admin.updateEndpoint(endpointId, { label: "SDK example" });

const security = await admin.getEndpointSecurity(endpointId);
console.log(security.data?.blockedIps?.length, security.data?.options?.hsts);
for (const filter of security.data?.requestFilters ?? []) {
  console.log(filter.id, filter.method, filter.params); // Matchers can contain JSON objects.
}

await admin.updateSecurityOptions(endpointId, { options: { responseLogging: "enabled" } });

Request filters

const filters = await admin.getRequestFilters(endpointId);
console.log(filters.data.map(filter => ({ pattern: filter.pattern, whitelist: filter.methodWhitelist })));

const whitelist = await admin.createRequestFilter(endpointId, { method: ["eth_chainId"] });
if (whitelist.data) {
  await admin.updateRequestFilter(endpointId, whitelist.data.id, {
    method: ["eth_chainId", "eth_blockNumber"],
  });
}

const customFilter = await admin.createRequestFilterPattern(endpointId, {
  pattern: { method: [{ prefix: "eth_" }], jsonrpc: ["2.0"] },
});
if (customFilter.data) {
  console.log(customFilter.data.pattern);
  await admin.updateRequestFilterPattern(endpointId, customFilter.data.id, {
    pattern: { method: [{ prefix: "eth_get" }], jsonrpc: ["2.0"] },
  });
}

Client certificates, blocked methods, ranges, and IPs

Certificate operations can return ApiError with status 403 when mTLS is unavailable. Blacklist and range updates replace the complete configuration; empty collections clear it. Blocked IPs are enforced when IP security is enabled.

const certificates = await admin.getClientCerts(endpointId);
console.log(certificates.data.enabled, certificates.data.certs.map(cert => cert.fingerprint));

const addedCertificate = await admin.createClientCert(endpointId, { certificate: certificatePem });
console.log(addedCertificate.data.certs.map(cert => cert.fingerprint));

await admin.updateClientCerts(endpointId, { enabled: true });
await admin.deleteClientCert(endpointId, "<fingerprint-to-remove>");

const blacklist = await admin.getMethodBlacklist(endpointId);
console.log(blacklist.data.methods);
await admin.updateMethodBlacklist(endpointId, { methods: ["eth_sendRawTransaction"] });

const ranges = await admin.getRangeLimits(endpointId);
console.log(ranges.data.supportedMethods, ranges.data.limits);
if (ranges.data.supportedMethods.includes("eth_getLogs")) {
  await admin.updateRangeLimits(endpointId, { limits: { eth_getLogs: 1000 } });
}

const blockedIps = await admin.getBlockedIps(endpointId);
console.log(blockedIps.data.map(record => record.id));
const blockedIp = await admin.createBlockedIp(endpointId, { ip: "192.0.2.0/24" });
await admin.deleteBlockedIp(endpointId, blockedIp.data.id);

Mutation responses and existing counterparts

The paired calls below are alternatives. Choose one form for each mutation. Do not log returned authentication tokens.

await admin.createToken(endpointId);
const token = await admin.createTokenWithResponse(endpointId);
console.log(token.data.id); // token.data.token contains the credential.

await admin.createReferrer(endpointId, { referrer: "https://app.example.com" });
const referrer = await admin.createReferrerWithResponse(endpointId, {
  referrer: "https://app.example.com",
});
console.log(referrer.data.id, referrer.data.url);

await admin.createIp(endpointId, { ip: "192.0.2.10" });
const allowedIp = await admin.createIpWithResponse(endpointId, { ip: "192.0.2.10" });
console.log(allowedIp.data.id, allowedIp.data.ip);

await admin.createDomainMask(endpointId, { domainMask: "rpc.example.com" });
const domain = await admin.createDomainMaskWithResponse(endpointId, { domainMask: "rpc.example.com" });
console.log(domain.data.id, domain.data.domainMask);

const jwtParams = { publicKey: publicKeyPem, kid: "sdk-example-key", name: "SDK example" };
await admin.createJwt(endpointId, jwtParams);
const jwt = await admin.createJwtWithResponse(endpointId, jwtParams);
console.log(jwt.data.id);

await admin.createTag(endpointId, { label: "sdk-example" });
const tags = await admin.createTagWithResponse(endpointId, { label: "sdk-example" });
for (const tag of tags.data.tags) {
  console.log(tag.id, tag.tagId, tag.endpointId); // Association ID and account tag ID are distinct.
}

Method and endpoint rate limits

const methodLimits = await admin.getMethodRateLimits(endpointId);
console.log(methodLimits.data?.rateLimiters.map(rule => ({ id: rule.id, perIp: rule.limitByIp })));

const methodLimit = await admin.createMethodRateLimit(endpointId, {
  interval: "second", methods: ["eth_call"], rate: 10, limitByIp: true,
});
if (methodLimit.data) {
  const updated = await admin.updateMethodRateLimit(endpointId, methodLimit.data.id, {
    limitByIp: false,
  });
  console.log(updated.data?.limitByIp);
}

// The original method can update only the per-IP flag, leaving caps unchanged.
await admin.updateRateLimits(endpointId, { limitByIp: false });
const effectiveLimits = await admin.updateRateLimitsWithResponse(endpointId, {
  rateLimits: { rps: 100 }, limitByIp: true,
});
console.log(effectiveLimits.data.limits); // rps/rpm/rpd can be null when no override exists.

Endpoint metrics and legacy usage breakdowns

const latency = await admin.getEndpointMetrics(endpointId, {
  period: "day", metric: "response_time_over_time", percentile: 95,
});
console.log(latency.data);

const endpointUsage = await admin.getUsageByEndpoint();
for (const row of endpointUsage.data?.endpoints ?? []) {
  console.log(row.rpcCredits, row.dataCredits, row.bytes);
  console.log(row.methodsBreakdown.map(method => method.bytes));
}

const methodUsage = await admin.getUsageByMethod();
console.log(methodUsage.data?.methods.map(row => row.bytes));

const chainUsage = await admin.getUsageByChain();
console.log(chainUsage.data?.chains.map(row => ({ rpc: row.rpcCredits, data: row.dataCredits, bytes: row.bytes })));

const tagUsage = await admin.getUsageByTag();
console.log(tagUsage.data?.tags.map(row => ({ rpc: row.rpcCredits, data: row.dataCredits, bytes: row.bytes })));

Usage and metrics summaries

For the first page, supply last or both from and to. Continue with cursor alone. Endpoint grouping values are integers or null; unselected dimensions are absent. Some count fields are nullable and metrics can be fractional.

const credits = await admin.getUsageCreditsSummary({ last: "1h" });
console.log(credits.data.totals);

const rpc = await admin.getUsageRpcSummary({ last: "1h", groupBy: ["endpoint", "method"], limit: 20 });
for (const row of rpc.data.breakdown ?? []) {
  console.log(row.endpoint, row.requests, Object.hasOwn(row, "endpoint"));
}
if (rpc.data.nextCursor) {
  const nextPage = await admin.getUsageRpcSummary({ cursor: rpc.data.nextCursor });
  console.log(nextPage.data.breakdown);
}

const streams = await admin.getUsageStreamsSummary({ last: "1h", groupBy: ["stream"] });
console.log(streams.data.totals, streams.data.breakdown);

const webhooks = await admin.getUsageWebhooksSummary({ last: "1h", groupBy: ["webhook"] });
console.log(webhooks.data.totals, webhooks.data.breakdown);

const traffic = await admin.getTrafficSummary({ last: "1h", groupBy: ["endpoint"] });
console.log(traffic.data.breakdown?.map(row => ({ endpoint: row.endpoint, requests: row.successfulRequests })));

const accountErrors = await admin.getErrorsSummary({
  last: "1h", kind: "rpc", endpoints: [numericEndpointId], groupBy: ["endpoint", "code"],
});
console.log(accountErrors.data.totals, accountErrors.data.breakdown);

const endpointErrors = await admin.getEndpointErrorsSummary(endpointId, {
  last: "1h", kind: "http", groupBy: ["code"], statusCodes: ["500"],
});
console.log(endpointErrors.data.totals, endpointErrors.data.breakdown);

Bulk operations and item errors

const statuses = await admin.bulkUpdateEndpointStatus({ ids: [endpointId], status: "active" });
console.log(statuses.data?.results.map(item => ({ id: item.id, success: item.success, error: item.error })));

const bulkTags = await admin.bulkAddTag({ ids: [endpointId], label: "sdk-example" });
console.log(bulkTags.data?.results.map(item => item.error));

const removedTags = await admin.bulkRemoveTag({ ids: [endpointId], tagId });
console.log(removedTags.data?.results.map(item => item.error));

Activity logs and transactions

const activityFilters = await admin.listActivityLogFilters();
console.log(activityFilters.data.types, activityFilters.data.actors);

const activity = await admin.listActivityLogs({ endpointId, targetType: "endpoint", limit: 20 });
const entry = activity.data[0];
if (entry) {
  const detail = await admin.getActivityLog(entry.id);
  console.log(detail.data?.action, detail.data?.target);
}

const transactions = await admin.listTransactions({ last: "1h", endpoints: [numericEndpointId], limit: 20 });
const transaction = transactions.data.transactions[0];
if (transaction) {
  console.log(transaction.endpointIds, transaction.height); // IDs: integer/null; height: string.
  const lookup = await admin.getTransaction(transaction.network, transaction.transactionHash, {
    includePayloads: true,
  });
  if (lookup.data.exists && lookup.data.data) {
    console.log(lookup.data.data.endpointIds);
    for (const attempt of lookup.data.data.attempts) {
      console.log(attempt.endpointId, attempt.payloads !== undefined);
      // Payload text stays unparsed. Avoid logging request/response contents.
    }
  }
}

Documentation-only clarifications

const to = new Date();
const from = new Date(to.getTime() - 60 * 60 * 1000);
const logs = await admin.getEndpointLogs(endpointId, {
  from: from.toISOString(), to: to.toISOString(), limit: 20, includeDetails: false,
}); // ISO timestamps; limit is 1 through 100.
console.log(logs.data);

// Use a standard role or a custom-role key configured on the account.
await admin.inviteTeamMember(teamId, {
  email: "teammate@example.com", fullName: "Example Teammate", role: "viewer",
});

try {
  const tooling = await admin.toolingAccessStatus();
  console.log(tooling.enabled);
} catch (error) {
  if (!(error instanceof ApiError) || error.status !== 403) throw error;
  console.log("Tooling Access is unavailable for this account.");
}

Where to focus your review

Decisions & tradeoffs

  1. Existing unit-returning methods keep their transport behavior; seven response-returning siblings decode mutation payloads. This avoids changing existing return contracts. Replacing the siblings would require a separate return-type migration.
  2. Filter patterns and matchers use dynamic JSON; other responses remain typed. Summary metrics use floating-point values, while transaction heights stay strings to preserve precision. Changing these representations would require model and binding migrations.
  3. Omitted update fields remain omitted, and invalid requests retain API-side errors. Endpoint caps are optional to support flag-only updates. Requiring caps again would remove that capability.

Technical notes

All 33 new methods and 89 new types have matching public exports and declarations. Python preserves response class names and setters; summary dimensions distinguish omission from explicit null. Ruby responses retain IndifferentHash behavior. Python and Node declarations are regenerated, Ruby RBS is updated, and all four Admin E2E examples reflect the supported APIs.

How I verified it

AI: built, tested, and self-reviewed with Codex using john-pipeline and jreview.

Ran:

  • cargo check, just lint, and cargo fmt --all -- --check passed.
  • just test passed: 286 core tests, including 134 Admin tests.
  • just python-build, just node-build, and just ruby-build passed, including Node package tests.
  • Localhost native binding checks passed: 60 requests across Python, Node.js, and Ruby, including structured JSON, integer endpoint filters/response IDs, nullable ID lists and summary dimensions, cursor continuation, flag-only updates, explicit false, exact heights, and typed errors.
  • Both Rust Admin examples compiled; Python/Ruby syntax and isolated Node example type checks passed. Export parity and README contents links were checked.
  • All nine CI jobs passed on 8fea7b2.
  • All 62 npm method examples pass strict TypeScript checking against the public SDK declarations, with existing declaration errors isolated using skipLibCheck. Method coverage matches the PR inventory. These examples were not executed against the live API.

Not checked:

  • Live Admin examples and account mutations were not run. Certificate lifecycle, populated records, and real cursor continuation need account features and data; sparse response variants use fabricated fixtures.
  • Existing failures reproduced on the base branch remain: unrelated Streams/Webhooks Rust examples, Python log stubs with an invalid from property, and unresolved webhook/SQL names in TypeScript declarations. The isolated Node example check supplies ambient process typing and skips existing declaration errors.

Confidence assessment: how likely is this to affect or break prod

Overall: medium upgrade risk, limited to callers of the Admin SDK. Rolling back the package version restores prior SDK behavior; it does not undo API mutations. No data migration is required.

Concern Likelihood Why Containment
Rust and TypeScript source compatibility High for affected callers Rust field types and full struct literals change; TypeScript matcher arrays become unknown[]. Endpoint IDs and filters in the affected summary/transaction APIs change from strings to integers. Migration notes identify the required updates. Compile against the new version and apply the documented changes before upgrading; pin the previous version if needed.
Existing Admin request and response behavior Low Existing return contracts remain. Core tests, 60 native binding requests, and all nine CI jobs pass, including numeric IDs, nullable entries, omitted fields, and explicit false. Regression tests cover wire shapes and typed errors; revert the SDK version if a regression appears.
New API behavior with live account data Medium Public contracts and fabricated responses cover serialization, but live feature access and sparse response variants were not exercised. Run the included E2E examples on an approved test account before relying on these operations.

Not verifiable from here:

  • Live account permissions, certificate behavior, retained activity/transaction data, and sparse response variants beyond the public contracts.

@johnpmitsch
johnpmitsch marked this pull request as ready for review October 23, 2026 16:25
@johnpmitsch
johnpmitsch merged commit bda9019 into main Oct 29, 2026
9 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants