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

@tangle-network/hub-sdk

v1.0.9

Published

Typed SDK for Tangle Hub tool discovery and execution

Readme

@tangle-network/hub-sdk

Typed SDK for the Tangle Hub /v1/hub/* surface. Status, connections, tools discovery and invocation, capability tokens, policies, approvals, audit, inbound channels, and product event subscriptions.

Install

pnpm add @tangle-network/hub-sdk

Quick start

import { HubClient } from "@tangle-network/hub-sdk";

const hub = HubClient.fromEnv();
const { principal, connections } = await hub.status();

HubClient.fromEnv() reads everything it needs from environment variables. SaaS vs on-prem flips via env only — there is no per-client default URL.

Environment variables

| Variable | Required | Meaning | | ------------------------------ | -------- | --------------------------------------------------------------------------------------------- | | TANGLE_HUB_URL | Yes | Hub root URL. Either the platform root (https://api.tangle.tools) or the full Hub URL including /v1/hub — both are accepted; the SDK normalizes. | | TANGLE_API_KEY | One of | Long-lived API key (Bearer). User / API key principal. | | TANGLE_HUB_CAPABILITY_TOKEN | One of | Short-lived capability token (Bearer). Sandbox-runtime principal. Auto-minted server-side per action. |

The two auth credentials are mutually exclusive — set exactly one.

Examples

SaaS:

export TANGLE_HUB_URL=https://api.tangle.tools
export TANGLE_API_KEY=sk-tan-...

On-prem:

export TANGLE_HUB_URL=https://hub.acme.internal
export TANGLE_API_KEY=sk-tan-...

Inside a sandbox (capability-token principal):

export TANGLE_HUB_URL=https://api.tangle.tools/v1/hub
export TANGLE_HUB_CAPABILITY_TOKEN=hubcap_...

Fail-loud configuration

HubClient.fromEnv() (and the standalone resolvers resolveHubBaseUrl() / resolveHubAuth()) never default. Missing or malformed configuration throws HubSdkError synchronously, before any request is issued:

| Code | Cause | | --------------------- | -------------------------------------------------------------------------------- | | HUB_CONFIG_MISSING | TANGLE_HUB_URL unset/empty, or neither auth credential is set. | | HUB_CONFIG_INVALID | TANGLE_HUB_URL is not a valid URL, or both auth credentials are set. |

try {
  const hub = HubClient.fromEnv();
} catch (error) {
  if (error instanceof HubSdkError && error.code === "HUB_CONFIG_MISSING") {
    // Surface the missing env var to the operator. Do not retry, do not default.
  }
  throw error;
}

This matches the repository's "No fallbacks. Fail loud." doctrine: required config has no silent default.

Bypassing env auth resolve

Pass apiKey or authHeaders explicitly to skip the auth-env-var check (useful when credentials come from a vault, OIDC exchange, or a custom session token). TANGLE_HUB_URL is still required.

const hub = HubClient.fromEnv({
  apiKey: await vault.read("hub/api-key"),
});

const hub = HubClient.fromEnv({
  authHeaders: async () => ({ Authorization: `Bearer ${await sessionToken()}` }),
});

Constructing without env vars

new HubClient({...}) remains supported for callers that resolve config themselves (CLI flags, web app config, tests). fromEnv is sugar with a fail-loud contract on top.

const hub = new HubClient({
  baseUrl: "https://api.tangle.tools",
  apiKey: "sk-tan-...",
});

Browsers, Workers, edge runtimes

fromEnv() reads globalThis.process?.env — undefined in browsers and many edge runtimes. Pass an explicit env map there:

const hub = HubClient.fromEnv({
  env: {
    TANGLE_HUB_URL: import.meta.env.VITE_TANGLE_HUB_URL,
    TANGLE_API_KEY: await session.apiKey(),
  },
  fetch: globalThis.fetch,
});

Tool approvals

tools.invoke() preserves an existing HUB_APPROVAL_REQUIRED error instead of requesting another token. The error retains the original approval ID and the server's action details. Present that approval through the product's decision flow. Set approve: true only after the caller has authorized that action. Automatic capability-token minting remains available when the server returns no concrete approval.

OAuth scopes

requestedScopes is a provider-generic, non-empty unique subset of the connector manifest and the host's optional narrowing. Platform rejects empty, duplicate, unknown, or widening requests. The exact set is bound to OAuth state; narrowed flows persist a connection only when the provider grants that same set, with no missing, narrower, or additional grants. Hosts that do not narrow retain their existing defaults and provider-grant fallback.

The SDK refuses a start response with different scopes, including responses from an older Platform deployment. GitHub retains its special contract: the exact read:user, repo, and read:org set. Gmail can request manifest-declared read-only access when the host permits it:

const hub = HubClient.fromEnv();
await hub.connections.start("gmail", {
  requestedScopes: ["https://www.googleapis.com/auth/gmail.readonly"],
});
await hub.connections.start("github", {
  requestedScopes: ["read:user", "repo", "read:org"],
});

Omitting requestedScopes retains the host-selected or manifest default.

Workflows

hub.workflows covers the /v1/workflows surface — the same resource the platform web UI and the tangle workflows CLI drive. It authenticates with the sk-tan-* API key (or a session), not a hub capability token. Alongside CRUD (list/get/create/update/delete), setEnabled, validate, and schema, it can trigger and observe runs:

const hub = HubClient.fromEnv();

// Trigger a run with the trigger fields the workflow reads, then wait for it.
const { runId } = await hub.workflows.run("wf_123", {
  "pull_request.number": "123",
});
const run = await hub.workflows.waitForRun("wf_123", runId, {
  timeoutMs: 300_000,
});
console.log(run.status, run.actionResults);

// Or tail live progress as it executes.
for await (const event of hub.workflows.watchRun("wf_123", runId)) {
  if (event.type === "token") process.stdout.write(event.delta);
  if (event.type === "run.done") console.log("\n", event.status);
}

// One page of run history + a single run's full detail.
const { runs, nextCursor } = await hub.workflows.listRuns("wf_123");
const detail = await hub.workflows.getRun("wf_123", runs[0].id);

run throws HubSdkError with MISSING_RUN_INPUTS (the workflow reads trigger fields none were supplied for — details.missing names them). A paused workflow still runs once on request; pausing only stops its triggers. waitForRun throws WORKFLOW_RUN_TIMEOUT if the run has not finished within timeoutMs. Live watchRun ticks require the worker executing the run to share the API process; across instances only snapshot + run.done arrive, and the persisted record read via getRun stays the source of truth.

Product event subscriptions

Products can bind a managed channel or a hosted provider connection to a signed server callback without authoring workflow YAML:

OAuth readiness and inbound-event readiness are separate. Check provider.eventIngressConfigured === true before presenting an inbound channel as available. An absent value means the server predates readiness discovery and should be treated as unavailable.

const callbackSecret = await deriveHubEventCallbackSecret({
  rootSecret: env.HUB_EVENT_CALLBACK_ROOT_SECRET,
  productId: "relationships",
  ownerId: tangleUserId,
  bindingId: channel.id,
});

const { subscription } = await hub.eventSubscriptions.create({
  clientReference: `relationships:${channel.id}`,
  label: "Relationship workspace inbound",
  source: { type: "channel", channelId: channel.id },
  event: "email.received",
  callback: {
    url: "https://relationships.example/api/events",
    secret: callbackSecret,
  },
});

clientReference is the product-owned idempotency key. The callback URL and secret are encrypted by the platform and never returned. The derivation scopes one callback secret to one product, owner, and binding, so the product stores only those existing ids. On receipt, authenticate and parse the exact raw request in one call:

const authenticated = await authenticateHubEventRequest({
  request,
  secret: callbackSecret,
});
if (!authenticated.ok) return authenticated.response;

const { delivery } = authenticated;
const email = delivery.providerEvent.payload;

The signed delivery includes the verified provider event directly, so the product does not need a second workflow-run request. Callbacks may be retried; use delivery.runId as the durable idempotency key before starting product work.

Allowances and the paywall decision

A product that bills its end users sets a free allowance, and optionally a paid one, on a meter. Hub admits every turn against it, counts turns per member and per line, and reads each payer's dollars from the ledger. The product keeps only its checkout: after a payment it records the member as paid until the period ends.

await hub.allowances.setPlan(`agent:${agentId}`, {
  free: { turnsPerDay: 20, usdPerDay: 2 },
  paid: { turnsPerDay: 200, usdPerDay: 5, priceUsdMonthly: 10 },
  spendResourceType: "hosted_enrollment",
});

const turn = await hub.allowances.admit(`agent:${agentId}`, {
  member: enrollmentId,
  turnId: messageId,
  line: lineId,
});
if (turn.decision === "paywall") return sendCheckoutLink();
if (turn.decision === "allowance_reached") return sendComeBackTomorrow();

// From the checkout webhook:
await hub.allowances.grant(`agent:${agentId}`, enrollmentId, {
  expiresAt: periodEnd.toISOString(),
  reference: subscriptionId,
});

admit is idempotent by turnId, so a retried turn is never counted twice. A member's dollars are the sum of today's ledger charges on keys bound to (spendResourceType, member) and on every key delegated below them, so delegate each member's key with that resource. A paid member also has a funded capacity for the period: the price less Tangle's 10% fee, plus the free daily dollars for the period's 31 days. With the plan above, that is $9 + $62 = $71 against the $155 that $5 a day would allow. Past it, admit answers allowance_reached, and turn.period shows spent, funded and remaining. Only the meter's owner may call these, signed in or with a root key of their own. A delegated or resource-bound key is refused, so an end user's sandbox cannot grant itself the paid tier.

Shared-Line application dispatch

Before admitting a shared-Line callback, check its current Hub authority:

const current = await hub.lines.applicationDispatchCurrent(
  lineId,
  messageId,
  { attachmentId, memberId, memberRevision, senderAddress, textSha256, binding, dispatchFence, dispatchDeadline },
  callbackBearer,
);

textSha256 is the lowercase SHA-256 hex digest of the exact callback text UTF-8 bytes, with no normalization. Compute it from the received callback text, not from a caller-supplied digest. Hub compares it with the persisted inbound message text before returning current: true. This uses the callback bearer alone; account credentials, cookies, and redirects are excluded. It returns a boolean only from a successful, valid Hub response and makes no retry. HTTP failures retain their status and code without response bodies or credential details. Configure the client's injected fetch to enforce the caller's request deadline. A current result authorizes this observation; native admission must still enforce its own expiry and deduplication contract.

Exports

  • HubClient, HubClient.fromEnv(options?)
  • HubConnectionsClient, HubPermissionsClient, HubTokensClient, HubChannelsClient, HubEventSubscriptionsClient, HubToolsClient, HubApprovalsClient, HubAuditClient, HubWorkflowsClient, HubAllowancesClient
  • deriveHubEventCallbackSecret, authenticateHubEventRequest, verifyHubEventSignature, parseHubEventDelivery, HubEventDeliveryError
  • HubSdkError — typed code: HubErrorCode, redacted details, optional HTTP status
  • HUB_URL_ENV_VAR, HUB_API_KEY_ENV_VAR, HUB_CAPABILITY_TOKEN_ENV_VAR — string constants for the env-var names
  • resolveHubBaseUrl(env?), resolveHubAuth(env?) — standalone resolvers with the same fail-loud contract as fromEnv
  • All request/response types from the /v1/hub/* contract

Link application login to explicit Hub permissions

Use the same HubClient for application management; do not add a product-local HTTP client. These operations require the authenticated developer account, not an app's OIDC access token. Customer grants are created only by the existing session-authenticated /cross-site/app-consent flow.

import { HubClient } from "@tangle-network/hub-sdk";

const hub = HubClient.fromEnv(); // developer account for app administration
const { app, clientSecret } = await hub.apps.create({
  name: "Support assistant",
  redirectUris: ["https://assistant.example/hub/callback"],
  allowedScopes: ["github.issues.create"],
});
// Save clientSecret once in the application's server-side secret store.
await hub.apps.linkOAuthClient(app.id, oidcClientId);
const apps = await hub.apps.list(); // includes oauthClientId; never the secret

oidcClientId is the application's already-registered Sign in with Tangle client. Linking requires the same owner on both records and is immutable and one-to-one. It does not reuse the two clients' secrets, change their IDs, create a user grant, or authorize spending. Use separate app/client pairs for separate deployments. Existing unlinked broker apps remain compatible. There is no automatic link by name, callback domain, email, or ownership alone.

With a customer's authenticated Hub client, hub.apps.grants.list() returns only that customer's active resource grants and hub.apps.grants.revoke(grantId) revokes one idempotently. App administrators use hub.apps.revoke(appId) to suspend the app, revoke all its customer grants, and disable its owned linked OIDC client. An app's broker credential cannot call these management methods.

An app broker can create new HubLineGrantsClient(options) for a line approved by its owner. Pass an authHeaders provider that mints a fresh scoped broker token for each call. Pass the exact workspaceId and workspaceAgentId binding to every method. The current response includes intakeMode, priorAttachmentId, and priorAttachmentActive. An owner_only grant attaches one declared owner sender and rejects unknown senders. The app cannot choose its own sender or change the approved intake mode in attach.

An owner can instead consent with execution=application (and owner_test=1). Then current().execution is "application" and attach is refused. The owner's existing attachment stays on the Line. memberApplication() reads the declared owner member, its memberRevision, eligible, and the Hub-derived binding. selectApplication() routes that member's texts to an HTTPS callback on the approved origin, with the app's own bearer secret. clearApplication() returns them to the incumbent agent. When another app answers, selection.own is false and selection.app.name and selection.selectedAt say which app and since when; its callback URL stays private. Show that to the owner with an explicit choice to answer here instead. Only on that choice, pass the selection id you read as both replaceSelectionId and expectedSelectionId to selectApplication(). Without replaceSelectionId, or with a stale id, Hub refuses with 409 and the other app keeps answering. The app cannot clear another app's selection, and revoking the grant revokes its selection. Each callback carries binding; verify currentness with lines.applicationDispatchCurrent.

Broker execution keeps the one approved connection even when the invocation omits connectionId; it never substitutes another account. Each one-shot broker token is checked against current app policy and the exact active grant when minted and consumed. Changing consent revokes the old binding, tokens and codes; the fresh consent response supplies a new grant ID. Login disconnect revokes the linked app's existing grants for that customer, not other customers or apps. An operation already admitted to a provider may finish; revocation blocks new admissions and minting, not external side effects already underway.

This is identity and resource delegation, not generalized spending delegation. Existing product billing checks and action approvals continue to apply.

Select an application for one verified line member

The account owner can select an app without changing the Line attachment. Unselected members keep the existing default agent. The member must already be active, declared, verified, and consented. An owner without a customer subject can select only their own Platform subject. Other members must retain their existing customer subject. The app authorizes and funds its own agent execution.

Use hub.lines.memberApplications.get(lineId, memberId) to read the current selection. Its app.name is Hub's name for the app that answers: the registered app of a consented grant, or the trusted app that serves the callback origin, else the callback host. Bindings starting with line-grant: belong to consented app grants and are refused here. Use select(lineId, memberId, input) with expectedSelectionId, expectedMemberRevision, subjectId, and application. The application supplies its approved HTTPS callback, binding, and callback secret. Use clear(lineId, memberId, { expectedSelectionId, expectedMemberRevision }) to restore the default for future messages. Selection changes use compare-and-swap and refuse while this member has an active callback or provider delivery. Each inbound message pins its selected target. Revoked selections refuse pending work instead of redirecting it. Existing messages and execution receipts retain their original identity. This path uses the existing shared-application flag and remains disabled by default. Account-owner channel authority is separate from the app's agent workspace key.

Customer grants with separate sponsorship

A server registers an application with accessPayer: "app-owner" when its owner sponsors platform eligibility. This is immutable: Hub uses the authenticated stored application owner, never a payer ID from the customer. Connections and grants still belong to the customer. The customer's native consent names the exact connection and actions. Existing explicit deny or approval policies still take precedence.

Keep the application secret on your server. After same-session callback and state validation, use the maintained broker client:

import { HubAppBrokerClient } from "@tangle-network/hub-sdk";

const broker = new HubAppBrokerClient({ baseUrl, clientId, clientSecret });
const grant = await broker.exchange({ code, redirectUri, connectionId });
const result = await broker.invoke(grant.grantId, connectionId, "github.issues.list", { owner: "example", repo: "project" });

invoke mints a fresh one-use, 60-second broker token and checks its selected scope before execution. The host application must recheck its own current customer enrollment on every call and revoke the app grant when access ends. Do not return broker tokens or application secrets to a browser or persist them in agent profiles.

Embedded Node MCP hosts

@tangle-network/hub-sdk/mcp owns HubMcpScope, connection resolution, createHubMcpRoutes, the HTTP bridge and lowerProfileConnections. The root client export remains browser/Worker-safe.

Create one scope for the host's lifetime with its existing canonical hash and credential-renewal function. Mount createHubMcpRoutes(scope) on the host's existing listener. scope.resolveConnectionsToMcp(client, refs, serverForGrant) validates the connections, provider and tool allowlists, rolls back partial allocation, and gives the host its native MCP representation. Only a pure attach-all request can return an explicitly degraded empty result; required connections remain errors. The host surfaces degradation and recovery.

Revoke each resolution's grantTokens when its run settles. scope.close() revokes the remaining grants of that instance and prevents new allocation. Other scopes and their borrowed clients remain independent.

The published @tangle-network/hub-sdk/mcp/stdio entry reads only TANGLE_HUB_MCP_ENDPOINT. Launch it through Node with the opaque callback endpoint; do not forward the Hub credential. lowerProfileConnections keeps the authored adapter profile separate from the ephemeral workspace profile. Grant endpoints belong to runtime materialization, never persisted authored profiles or trace metadata.