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

@providame/core

v0.0.15

Published

Providame authentication SDK — headless primitives, prebuilt-component foundation, and backend token verification.

Downloads

672

Readme

@providame/core

The framework-agnostic TypeScript SDK for Providame — headless auth primitives and the foundation @providame/vue and @providame/react are built on.

Use this directly if you're not on Vue or React (Svelte, Angular, Solid, plain TypeScript), or if you want the flow objects rather than a framework binding.

For a browser bundle with zero dependencies, see @providame/sdk-js. For server-side code, see @providame/backend — it takes your secret key and must never run in a browser.

Install

npm install @providame/core

Quickstart

import { createProvidame } from '@providame/core'

const providame = createProvidame({
  publishableKey: 'pk_test_...',
  apiBase: 'https://<your-providame-host>',
})

// Sign up — the backend always requires email verification next
const signUp = providame.createSignUp()
await signUp.create({ email, password, firstName, lastName })
await signUp.attemptVerification({ code })
if (signUp.isComplete) {
  // signUp.session has { userId, accessToken, expiresAt }
}

// Sign in
const signIn = providame.createSignIn()
await signIn.create({ identifier: email })
await signIn.attemptFirstFactor({ strategy: 'password', password })
if (signIn.status === 'needs_second_factor') {
  await signIn.attemptSecondFactor({ strategy: 'totp', code })
}

// Restore a session on page load
const session = await providame.session()

await providame.signOut()

providame.onAuthStateChange((event, { session }) => {
  if (event === 'signed-in') { /* ... */ }
})

session() is the authoritative check: it tries GET /me, and — since access tokens live ~60 seconds — falls back to a silent refresh before reporting signed-out. You don't manage refresh timing yourself; the client runs a background refresh loop once a session exists.

Other sign-in strategies

await providame.signInWithIdp('google', { returnUrl: window.location.href })

const flow = providame.createSignIn()
await flow.createWithOTPEmail({ identifier: email })   // emailed code
await flow.resendOTPEmail()                             // if it didn't arrive
await flow.createWithOTPSMS({ identifier: email })     // texted to the phone on file
await flow.resendOTPSMS()
await flow.createWithMagicLink({ identifier: email, returnUrl })
await flow.createWithPasskey({ identifier: email, domain: location.hostname })
await flow.completeWithPasskey()   // drives navigator.credentials.get() for you

Password reset

Starting a reset always succeeds from the caller's point of view — no account enumeration. A code is only emailed if the address exists.

const flow = await providame.resetPassword({ email })
await flow.attempt({ code, password: newPassword })

// An MFA-protected account still has to prove factor possession — resetting
// the password doesn't bypass MFA
if (flow.status === 'needs_second_factor') {
  await flow.attemptSecondFactor({ strategy: 'totp', code: mfaCode })
}

await flow.resend()   // re-send the code if it expired or never arrived

Mid-flow second factor and forced TOTP enrollment

attemptSecondFactor/enrollTotp/verifyTotpEnrollment are identical across SignInFlow, SignUpFlow, and ResetPasswordFlow — all three share one backend flow-state machine keyed by flowId, not by which route created it.

if (flow.status === 'needs_second_factor') {
  await flow.attemptSecondFactor({ strategy: 'totp', code })
}

// Forced-MFA environment, account has no second factor yet:
if (flow.status === 'needs_mfa_enrollment') {
  const { secret, otpauthUri } = await flow.enrollTotp()
  await flow.verifyTotpEnrollment(code)   // activates the factor AND completes the flow
  flow.recoveryCodes                      // one-time backup codes — shown once, never retrievable again
}

Branding & enabled sign-in providers

Every prebuilt component fetches these automatically on mount — use them directly only if you're rendering your own sign-in screen.

const branding = await providame.branding()          // never throws — {} on failure
const idps = await providame.listEnabledIdps()        // never throws — [] on failure
// [{ provider: 'google', displayName: 'Google' }, ...]

branding() is memoized per client instance, so mounting several components on one page costs one request, not one per component. listEnabledIdps() deliberately isn't — the enabled-provider set can change between mounts (e.g. an admin edits it) and the fetch is cheap.

Account management

providame.user is what <UserProfile> in the framework packages renders a UI over — use it directly for a custom account page.

await providame.user.update({ givenName, familyName })
await providame.user.changePassword({ currentPassword, newPassword })
await providame.user.changeEmail('[email protected]')   // code goes to the NEW address
await providame.user.verifyEmailChange(code)
await providame.user.setPhone('+15551234567')

const { avatarUrl } = await providame.user.uploadAvatar(file)   // a Blob/File
await providame.user.removeAvatar()

// hasPassword is false for a social-login account — gate any password
// prompt on it, or it's an unsatisfiable dead end for those users
const { hasTotp, hasPassword } = await providame.user.mfaState()
await providame.user.hasPassword()   // same data, just the one flag

if (!(await providame.user.totp.isEnrolled())) {
  const { secret, otpauthUri } = await providame.user.totp.enroll()
  const recoveryCodes = await providame.user.totp.verify(code)
}
await providame.user.totp.remove()

await providame.user.passkeys.list()
const { passkeyId, creationOptions } = await providame.user.passkeys.register(location.hostname)
// credential = navigator.credentials.create(toCreationOptions(creationOptions))
await providame.user.passkeys.verify(passkeyId, credential, name)
await providame.user.passkeys.registerWithBrowser(location.hostname, name)   // drives that whole ceremony for you
await providame.user.passkeys.remove(passkeyId)

await providame.user.sessions.list()
await providame.user.sessions.revoke(sessionId)

const myData = await providame.user.exportData()   // self-service GDPR export — never private_metadata

// deleteAccount() takes proof of a recent re-authentication and calls
// reauth() internally, so most callers only need to pass it straight in:
await providame.user.deleteAccount(currentPassword)
// A social-login account has no password — request an emailed code instead:
await providame.user.requestReauthCode()
await providame.user.deleteAccount({ code })

Admin-issued passkey registration link

For a user who is not signed in — an admin generates a registration link from the dashboard, and the emailed code is itself the credential:

const { passkeyId, creationOptions } = await providame.user.passkeys.registerWithCode(
  identifier, codeId, code, domain,
)
const credential = await navigator.credentials.create(toCreationOptions(creationOptions))

// identifier must be the SAME email passed to registerWithCode()
await providame.user.passkeys.verifyWithCode(passkeyId, identifier, credentialToJSON(credential), name)

Every failure mode — unknown identifier, wrong/expired code, a bad credential — collapses into the same generic invalid_code rejection, so don't rely on the error to distinguish "no such account" from "wrong code".

Organizations

Providame's B2B tenancy layer: your customers are companies with members and roles. Every environment has organizations — there's no feature switch to enable.

const orgs = await providame.organizations.list()
await providame.organizations.setActive(orgs[0].id)   // null clears it
await providame.organizations.setActiveBySlug('acme')

const org = await providame.organizations.get(orgs[0].id)
await providame.organizations.create({ name: 'Acme Inc', slug: 'acme' })
await providame.organizations.update(org.id, { name: 'Acme Corp' })
await providame.organizations.delete(org.id)

await providame.organizations.members(org.id)
await providame.organizations.setMemberRoles(org.id, userId, [
  { key: 'org:admin', scope: 'environment' },
])
await providame.organizations.removeMember(org.id, userId)   // a member may always remove themselves

// Org-local roles draw their permissions from the ENVIRONMENT's catalog
await providame.organizations.roles(org.id)
await providame.organizations.createRole(org.id, {
  roleKey: 'billing-owner', displayName: 'Billing Owner', permissionKeys: ['billing:manage'],
})
await providame.organizations.updateRole(org.id, 'billing-owner', {
  permissionKeys: ['billing:manage', 'billing:view'],   // omit to leave unchanged, [] revokes all
})
await providame.organizations.deleteRole(org.id, 'billing-owner')

// Invitations — the token itself is never returned here, only emailed
await providame.organizations.invite(org.id, { email: '[email protected]' })
await providame.organizations.invitations(org.id)          // this org's invitations
await providame.organizations.revokeInvitation(org.id, invitationId)
await providame.organizations.myInvitations()               // addressed to the signed-in user
await providame.organizations.acceptInvitation(token)

// Verified domains + domain-based suggestions
await providame.organizations.domains(org.id)
await providame.organizations.addDomain(org.id, { domain: 'acme.com' })
await providame.organizations.setDomainEnrollmentMode(org.id, domainId, 'automatic_invitation')
await providame.organizations.verifyDomain(org.id, domainId)
await providame.organizations.removeDomain(org.id, domainId)
await providame.organizations.suggestions()
await providame.organizations.joinSuggested(suggestedOrgId)

setActive() returns a freshly minted token already carrying the new org_* claims, so you don't wait out the previous token's lifetime. You don't need to call it after every sign-in: the choice is remembered, so a returning user is put back in the organization they last used, a user who has never chosen starts in the one they joined first, and clearing it is remembered too. A member's role carries a scope: "environment" roles come from the environment's shared catalog, "organization" roles exist only inside one org, and setMemberRoles replaces across both tiers at once.

Reading roles and permissions

These read the access token's claims and make no request:

import {
  hasRole, hasPermission, hasOrgRole, hasOrgPermission, activeOrganization,
} from '@providame/core'

const claims = await providame.getClaims()

hasRole(claims, 'admin')                    // environment-level grant
hasPermission(claims, 'posts:write')
activeOrganization(claims)                  // { id, slug, name } | null
hasOrgRole(claims, 'org:admin')             // ACTIVE organization only
hasOrgPermission(claims, 'billing:manage')

All four return true for an undefined argument, so an omitted constraint is not a constraint. This is display gating only — real enforcement is your backend verifying the token.

Object-scoped access

The claim helpers above answer "is this user an admin" for free, from the token. providame.authz asks about one specific resource — a real network call, for "can this user edit this document":

const allowed = await providame.authz.can({ permission: 'edit', resource: 'document:42' })
const [access] = await providame.authz.listResources({ permission: 'edit', resourceType: 'document' })
// access.scopes is a UNION of shapes: [{ kind: 'all' }] means every document;
// [{ kind: 'instances', instances: [...] }] is the complete, enumerable list, e.g. ['document:42', 'document:107']
const editableDocumentIds = access.scopes.some((s) => s.kind === 'all')
  ? allDocumentIds
  : access.scopes.flatMap((s) => s.instances ?? [])

// The cache already clears itself on sign-in/out and organization switch —
// call this yourself only after something YOUR app did could have changed an
// answer (e.g. your backend just recorded a new grant).
providame.authz.invalidate()

There is no user parameter on either call — the subject is always the signed-in caller, forced server-side; a customer-supplied user id would turn this into an authorization oracle readable by anyone holding the publishable key. can() never resolves false on a network failure — it rejects, so a dropped connection can't be mistaken for a real denial; handle that in your own error boundary rather than treating a caught error as "not allowed" and moving on silently. Answers are cached per-question for the life of the session (cleared on sign-in/out and organization switch); sdk/vue/sdk/react wrap this in a <Can> component with a loading state built in.

Getting a token for your backend

const token = await providame.getToken()   // cached — a network call only
                                            // happens once it's actually expired
if (token) {
  await fetch('/api/whatever', { headers: { Authorization: `Bearer ${token}` } })
}

await providame.getToken({ skipCache: true })   // force a live round trip

Like Clerk's own getToken(), the cached token is decoded from its own exp claim, so it's safe to call before every request — a network round trip only happens once it's actually expired.

Verifying tokens

import { verifyToken } from '@providame/core'

const config = await providame.configuration()
const { userId, claims } = await verifyToken(accessToken, config)

A local JWKS check that never calls Providame. If your server also needs to read end-users or manage organizations, use @providame/backend instead — it wraps this plus the Backend API.

Native & hybrid apps

For React Native/Expo, or a Capacitor/Ionic hybrid app — a nk_ (native) publishable key instead of pk_, and an options.native block instead of cookies. createProvidame detects native mode from the key prefix.

import "react-native-get-random-values"; // React Native only, before @providame/core
import * as SecureStore from "expo-secure-store";
import { createProvidame } from "@providame/core";

export const providame = createProvidame({
  publishableKey: "nk_live_...",
  native: {
    storage: {
      getItem: (k) => SecureStore.getItemAsync(k),
      setItem: (k, v) => SecureStore.setItemAsync(k, v),
      removeItem: (k) => SecureStore.deleteItemAsync(k),
    },
    redirectUrl: "myapp://auth-callback",
  },
});

options.native.storage is required — the SDK stores, refreshes and sends the session handle for you from there. Sign-in flows, session(), getToken(), and signOut() all work exactly as in a browser. Only a 401 discards the stored handle; offline or a transient server error keeps it and retries, so a dropped connection never signs anyone out.

A 401 with code invalid_key is the exception: the server refused your app's key (revoked, rotated, or a WebView origin not on the environment's native app origins), not the user's session. The SDK keeps the handle, retries with backoff (15s, doubling to every 5 minutes), and emits key-rejected once through onAuthStateChange — the place to show a "please update the app" prompt:

providame.onAuthStateChange((event) => {
  if (event === "key-rejected") showUpdatePrompt()
})

Social sign-in runs through the system browser, not an embedded WebView:

const { authorizeUrl } = await providame.startIdpSignIn("google", {
  returnUrl: "myapp://auth-callback",
})
// open authorizeUrl with expo-web-browser's openAuthSessionAsync, or @capacitor/browser

// on the deep link your app receives back:
const session = await providame.completeIdpSignIn(callbackUrl)

options.native.openAuthUrl (an (url: string) => void | Promise<void>) lets signInWithIdp() and the prebuilt components' own social buttons drive this for you instead of calling startIdpSignIn/completeIdpSignIn by hand.

React Native's Hermes engine has no crypto.subtle, so the PKCE challenge this uses is computed by a dependency-free SHA-256 implementation in this package — but it still needs crypto.getRandomValues, which React Native requires react-native-get-random-values for (imported once, before this package, as shown above).

Device attestation

If the environment requires device attestation, a native app has to prove it is a genuine build on a genuine device before FAPI will serve it. The flow is nonce, platform proof, verify:

const { nonce } = await providame.attestation.nonce()

// Produced by YOUR native code, not by this package:
//   iOS     — App Attest: pass SHA256(UTF-8 bytes of the nonce string) as
//             clientDataHash to attestKey (first time) or generateAssertion.
//   Android — a classic Play Integrity request with the nonce string as its nonce.
const result = await providame.attestation.verify({
  platform: "ios",
  nonce,
  deviceId,            // a stable id for this install, chosen by your app
  keyId,               // App Attest key id (base64)
  attestation,         // first time; later calls send `assertion` instead
})

if (!result.valid) console.warn(result.reasons)

JavaScript cannot produce these proofs itself — obtain them through your own native module or a React Native / Capacitor plugin that wraps App Attest and Play Integrity. A passing verification returns a short-lived attestation token, which the SDK keeps in options.native.storage and sends as X-Attestation-Token on every request until it expires (providame.attestation.token reads it; providame.attestation.clear() forgets it). A purpose: "check_in" verification — for reporting a device your app decided to block — is recorded but never earns a token.

When the token is missing or expired, requests fail with a 401 attestation_required. Like invalid_key, that does not sign the user out: the SDK keeps the session, backs off, and emits attestation-required once. Re-attest, then call session() again (a sleeping refresh retries at once):

providame.onAuthStateChange(async (event) => {
  if (event === "attestation-required") await attestAgain()
})

Forced updates

Pass your build's version as native.appVersion; the SDK sends it on every request as X-App-Version. When the environment sets a minimum app version in the dashboard, requests from an older build fail with 426 upgrade_required. The session is kept; the SDK backs off and emits upgrade-required once. appConfig() is always reachable, so check it at launch and on returning to the foreground:

const providame = createProvidame({
  publishableKey: "nk_live_...",
  native: { storage, appVersion: "4.2.1" },
})

const policy = await providame.appConfig()
// { minClientVersion, recommendedVersion, iosStoreUrl, androidStoreUrl }

providame.onAuthStateChange((event) => {
  if (event === "upgrade-required") showUpdateScreen()
})

Audit events

sendAuditEvents(events) posts up to 100 of your app's audit events for the signed-in user. Every event comes back in exactly one of accepted or rejected — both are final, so delete it from your local buffer once it appears in either — and an event the same user resends is not stored twice. Anything past the first 100 is in neither list; send it again. Each user may send 30 batches a minute; past that the call throws a ProvidameError with status 429 and nothing is stored — keep the events and retry after err.retryAfterSeconds.

const { accepted, rejected, errors } = await providame.sendAuditEvents([
  { eventId: crypto.randomUUID(), action: "trip.viewed", timestamp: new Date().toISOString(), resourceId: "trip_981" },
])

The vanilla, zero-dependency @providame/sdk-js package does not support native mode.

Full walkthrough, including the raw HTTP calls for Swift/Kotlin and how to allow a Capacitor/Ionic WebView to present a native key: the Native apps guide in the Providame docs.

Also exported

  • resolveAppearance, ELEMENT_KEYS — the theming vocabulary the framework packages' appearance prop is built on.
  • generateQRMatrix, qrDarkCells — a dependency-free QR encoder, for rendering otpauthUri during TOTP enrollment.
  • toCreationOptions, toRequestOptions, credentialToJSON, isWebAuthnSupported — WebAuthn plumbing for custom passkey UI.
  • renderTurnstile — bot-check widget mounting.
  • safeHref, safeAbsoluteHttpUrl, safeMailto — URL guards to run before rendering any developer-supplied link.
  • ProvidameError, messageForIdpErrorCode, messageForWebAuthnError.

Docs

Full reference: /docs/sdk/core and /docs/api/fapi in your Providame dashboard.

Build

npm run build

License

MIT