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

@codeany-inc/hubs-client-sdk

v0.1.0-alpha.1

Published

TypeScript SDK for the CodeAny hub client APIs.

Readme

@codeany-inc/hubs-client-sdk

TypeScript SDK for the CodeAny hub client APIs.

Status: pre-release. Version 0.1.0-alpha.1 is prepared for public publication under the alpha dist-tag and is not published yet. Its entry point — src/index.ts, the subject of the claim above — carries the client foundation: the factory, the client's shape, the credential and language configuration, the escape hatch and the error model. Three of the sixteen module names have endpoints behind them — auth, added by CODANYP-489, competitions, added by CODANYP-490, and participation, added by CODANYP-491 — and the typed resource methods behind the other thirteen are added by later work, so those facades are still named placeholders.

Usage

Import from the package root only:

import { createCodeAnyHubsClient } from '@codeany-inc/hubs-client-sdk';

const client = createCodeAnyHubsClient({
  baseUrl: 'https://codeany.example',
  auth: { jwt: { token: accessToken }, session: 'same-origin' },
  interfaceLanguage: 'en',
});

// The escape hatch, which is on the root client and nowhere else.
const competitions = await client.raw.request({
  method: 'GET',
  path: '/api/competitions/list',
});

// Two scoped facades. Both are cheap, and both are a new object every call.
const acme = client.forHub('acme');
const round = client.forCompetition(42);

Signing in

auth is on the root client, and the hub is an argument: a client calling these has no identity yet, and three of the four routes name a hub in their path. Nothing here changes the client — a token is returned and never stored — so applying one is a line the consumer writes.

const result = await client.auth.login('acme', {
  username: 'ada',
  password: 'secret',
});

if (result.outcome === 'authenticated') {
  client.setJwtToken(result.token);
} else {
  // A 200 as well: this account exists and is not a member of that hub.
  showRegistrationForm();
}

// Later, when the access token has expired. `storedRefreshToken` is the
// consumer's own: the credential store has no slot for a refresh token.
const refreshed = await client.auth.refreshAccessToken({
  refreshToken: storedRefreshToken,
});
client.setJwtToken(refreshed.access);

if (refreshed.refresh !== null) {
  // A rotated token, which replaces the one just sent.
  storedRefreshToken = refreshed.refresh;
}

That write is conditional because refreshed.refresh is null on every deployment that leaves Simple JWT's ROTATE_REFRESH_TOKENS off — the default. Such a deployment answers no new refresh token because the stored one is still the valid one, and overwriting it with null would leave the consumer with nothing to refresh with: worse than not writing at all. Where rotation is on, the response carries a new refresh token and the deployment may blacklist the one just sent, so a consumer that keeps the old one fails on the next refresh rather than this one.

Registration takes either form the backend branches between — a password with its confirmation, or a Google OAuth token — and the two are exclusive in the type system. client.auth.verifyOauthToken('acme', { oauthToken }) answers what the backend knows about the identity behind a token before anything is created, and signs the caller in when that identity is already a member.

Input fields are camelCase and response fields keep the backend's own spelling, csrf_token and sessionid included: renaming a field a consumer reads off backend data would make this SDK's documentation and the backend's disagree about what the same value is called.

Competitions, and what a participant may do in one

competitions is on a hub-scoped facade, because the participant listing route reads the hub from its hub query parameter. The other two methods take the competition as an argument, because their routes name no hub.

const competitions = client.forHub('acme').competitions;

const page = await competitions.list({ page: 1, perPage: 20 });
for (const competition of page.list_competitions) {
  render(competition.id, competition.titles);
}

const { competition, tasks } = await competitions.getContent(42);

page.list_competitions is an array even when the hub has none: the backend sends the string 'empty' in place of an empty list, on this route and on the tasks of a content response, and both arrive here as []. A body carrying some other string is refused, because that is not a document either endpoint produces.

Render the workflow; do not recompute it. getWorkflow answers with the document build_workflow_state produces, which is the same one the backend decides submission scope and question permissions from — so a client that works out for itself whether a competition is open, from its start time and length, is computing a second opinion the backend will not honour.

const workflow = await competitions.getWorkflow(42);

if (workflow.can_submit_official) {
  showSubmitButton();
} else if (workflow.can_start) {
  showStartButton();
}

Every flag on it is a boolean rather than a boolean | null, and a 2xx missing one is refused rather than returned with a hole: a nullable permission invites ?? false at the call site, and false on a permission the response said nothing about would close a competition the backend was accepting submissions for. The times, counts and status strings beside them are nullable, because a caller displays those rather than branching on them.

All three calls are reachable unauthenticated. An anonymous workflow read is not an error: it answers participation_state: 'anonymous' with every flag false, which is what a "sign in to take part" screen renders from.

Taking part in one

participation is on the competition-scoped facade, and the competition is injected into every call: all five routes name one, so none of these methods takes a competition argument.

const participation = client.forCompetition(42).participation;

// The questions come off the workflow document; the answers go back by id.
const { workflow } = await participation.register({
  answers: [{ questionId: 7, response: 'Baku' }],
});

if (workflow.can_start) {
  await participation.start();
}

A mutation is the workflow read. Each of register, start, finish and submitFeedback answers a message and a workflow, and that workflow is the same document getWorkflow returns, recomputed after the change — so following one of these with getWorkflow asks a question already answered.

register is the exception, and only for the tasks. Its view is the one of the four that never attaches competition_tasks, so a registration's workflow carries null there whatever it says beside it.

What decides whether the follow-up getWorkflow is worth a call is a backend condition with five arms rather than a list of cases, and any one arm is enough: can_access_problems, finished_participation, feedback_required, feedback_submitted, or registered together with competition_finished. Registering into a competition already under way satisfies the first in the same request. A COURSE satisfies it whether or not the course has begun, because COURSE task access does not depend on the start time at all. And an already_registered retry can satisfy any of the other four — including on a competition that merely expired, with the participant never having finished it and with upsolving and feedback both off, where the last arm is the only one that fires. That last case is the reason this is written as the rule and not as an enumeration: the arms are the backend's, they do not correspond one-to-one to situations a consumer would think to name, and a list of cases goes stale the first time one is added. src/participation.ts names each arm and the backend function that reads them, _with_accessible_competition_tasks.

Nothing is read before a mutation either: the state a mutation decides on is mostly built inside the mutation's own transaction, so a check made here would be a second opinion the backend will not honour, and a refusal arrives as a 400 or 403 carrying the backend's own sentence.

Mostly, because three of the four decide something from a snapshot taken before the transaction that carries it out, and none of it is something a client can compensate for. The shape is the same each time: the view loads the competition, and the decision is made from that instance rather than from one the service locked or refetched. start runs its proctor gate that way and returns its 403 without entering the service at all. register tests whether registration is open once before taking the row lock and once after, so a registration racing a reopen can be refused on the earlier read. submitFeedback is the widest: submit_feedback never locks and never refetches, so whether feedback is accepted at all is decided from the row as the view found it. All three are the backend acting on its own stale snapshot, which is a different thing from this SDK acting on one — and the reason the rule above is about what a client must not do. src/participation.ts states which of the four take the competition's row lock, which one does not, and where each unlocked read sits.

const done = await participation.finish();

if (done.workflow.feedback_required) {
  await participation.submitFeedback({ text: userTypedFeedback });
}

const me = await participation.getCurrentParticipant();
render(me.score, me.user.full_name);

me is whichever account the backend resolved the request to, which is not always the one the token names: this route authenticates in the opposite order to the four mutations above, so a client sending a session cookie and a bearer token for two different accounts reads one person here and changes the other's participation there. It is the concrete case behind the client options; src/participation.ts and this package's backend-contract TODO index carry the evidence and the backend issue.

me.score is a string, and stays one: the backend's column is a decimal with ten places, so parsing it into a JavaScript number is a lossy step this SDK does not take for you. me.tasks is unknown for a related reason — the column is schemaless, and for anything that is not an ICPC competition the backend returns it verbatim, so it is frequently not an array at all.

Two constraints are documented and not enforced here, and they do not cut the same way. Registration answer question ids must be unique across the array, and feedback text must contain something other than whitespace. This SDK neither refuses nor repairs either: your array is forwarded unchanged and unmutated, and your feedback text is sent with its interior — newlines, runs of spaces, indentation — exactly as written. Blank feedback is then refused by the deployment, whose 400 is the report. A repeated question id is refused by nobody as a duplicate: the backend keeps whichever row came last and never mentions the earlier one. What happens next depends entirely on that surviving value. If it answers the question, the registration succeeds and the first row is gone without a word. If it is blank — a form emitting an empty second row for a question the user did answer — the registration is refused with 400 "You must answer all questions!", which names no question, because the check that runs afterwards asks only whether each one ended up with a value. Either way a form that can emit two rows for one question is a form that must not, and the failure is silent in one direction and unattributable in the other — the backend-contract TODO index carries that one. Both of those outcomes are a first registration's: a caller who is registered already answers already_registered from a branch the backend reaches before it looks at the array at all, so a retry's rows are neither checked nor stored whatever is in them. Refusing a caller value here would need an error code that is part of the shared core's public contract, and this package repairs no caller value anywhere.

The client options

These configure the client itself, so they apply to every call above rather than to any one module.

auth is optional and its default is no primary credential, which is what a sign-in page holds. A JWT and an LTI launch token are mutually exclusive — the backend's LTI middleware authenticates allowlisted paths before DRF reads the bearer token, so one client carrying both would be two identities — and the union refuses the pair at compile time as well as at run time. session is independent of either and coexists with both: it is the Fetch credentials mode, which decides whether Django's session cookie goes with a request.

A cookie and a token for two different accounts are two identities, and this one the SDK cannot refuse. The cookie is attached by the browser and never passes through this package, so there is nothing here to compare — and which of the two authenticated a request is decided route by route, by the backend, not uniformly. Hold one identity at a time: sign the session out before applying a token for another account, or configure session: 'omit' and carry the token alone. src/options.ts states what the choice costs and src/participation.ts names the route where two of this SDK's own calls currently disagree.

session: 'omit' is that answer for a JWT client and is not one for an LTI client. Omitting the cookie omits the csrftoken cookie too, and Django compares the X-CSRFToken header against that cookie rather than accepting the header alone. A bearer token never meets the check; a launch token authenticates through Django's session authentication, so on an unsafe method it does — with no cookie left to satisfy it. An LTI client that omits the cookie therefore gets a 403 from all four of register, start, finish and submitFeedback, and one that sends it gets that same 403 until it configures an auth.csrf token for the header half of the comparison — and the identity ambiguity above once it does. 'same-origin' is not a third answer between them: no cookie goes on a cross-origin request under that mode, whatever SameSite the deployment sets, so in the cross-origin iframe an LTI launch normally runs in it sends nothing and lands in the first case. Only 'include' sends the cookie cross-origin. Reading is unaffected under any of the three. That is a backend blocker rather than a configuration to pick between, filed as CODANYP-601; src/participation.ts and the backend-contract TODO index carry the chain.

baseUrl is the deployment root, not an API prefix: hub routes live under /api/hubs/<hub_name>/…, competition routes under /api/competitions/… and sign-in under /api/users/…, and no narrower base reaches all three. Every request path is confined to the configured base.

Deep imports (for example @codeany-inc/hubs-client-sdk/dist/...) are not part of the package contract and may break in any release. The only other entry in the manifest's exports map is ./package.json, which resolves a file rather than a module surface.

Packaging

  • ESM and CommonJS builds with TypeScript declarations, published from dist/.
  • Targets Node.js >=18.14.1 <19 || >=19.6.1 and evergreen browsers, using the platform fetch.
  • No runtime dependencies; the shared core is bundled in.
  • sideEffects: false, so unused exports can be tree-shaken.
  • JavaScript source maps are published, and they map this package's own source only. Declaration maps also name private-core declaration sources that do not ship, so those locations fall back to the bundled declaration. The shared core is bundled in but its source is not shipped, so a debugger stepping into a core frame shows the bundled JavaScript rather than the original. That is deliberate: the core is private, and a map carrying its text would publish it. The rule and how it is enforced are in package boundaries.

Development happens in the SDK workspace; this package is versioned and released independently of @codeany-inc/hubs-manage-sdk.

License

Licensed under the Apache License 2.0.