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/sdk-js

v0.0.15

Published

Providame auth SDK — vanilla JS, zero runtime deps, drop-in components and headless auth primitives (for plain <script> tag / non-TypeScript use; see @providame/core in sdk/core for the TypeScript SDK sdk/vue builds on)

Readme

@providame/sdk-js

Vanilla JS auth SDK for Providame — zero runtime dependencies, ships ESM, CJS, and UMD, and works from a plain <script> tag.

On Vue or React? Use @providame/vue or @providame/react. On another framework, or want TypeScript types? Use @providame/core. On a server? Use @providame/backend.

Building a native or hybrid app (React Native, Expo, Capacitor, Ionic)? This package is browser-only — use @providame/core with a native key (nk_) instead. See the Native apps guide.

Install

npm install @providame/sdk-js

Usage

Drop-in components

<div id="sign-in"></div>

<script type="module">
  import { ProvidameClient, SignIn } from '@providame/sdk-js'

  const providame = new ProvidameClient({ publishableKey: 'pk_test_...' })

  SignIn.mount({
    client: providame,
    target: '#sign-in',
    signUpUrl: '/sign-up',
    onSuccess: () => window.location.href = '/app',
  })
</script>

Component styles are injected automatically on mount — no CSS import is needed. (@providame/vue is the exception: it ships a real stylesheet you must import.)

Modal vs. page usage

The footer link behaves differently depending on how you mount these:

  • Page — you mount SignIn into your own /sign-in page. Pass signUpUrl and the footer renders a normal link that navigates.
  • Modal — client.openSignIn() handles this for you: the footer switches to sign-up in place, because navigating would tear the modal down. openSignUp() opens the same modal on the sign-up view.
providame.openSignIn()  // footer switches to sign-up in place
providame.openSignUp()  // same modal, opens on sign-up
providame.closeSignIn() // closes it — closeSignUp() is equivalent

Mounting into your own modal? Pass the switch callback instead of a URL and swap the views yourself:

SignIn.mount({
  client: providame,
  target: content,
  onSwitchToSignUp: () => {
    SignIn.unmount(content)
    SignUp.mount({ client: providame, target: content, onSwitchToSignIn: /* … */ })
  },
})

Unmount the outgoing view before mounting the next one — that is what releases its auth-state listener. The callback takes precedence when both it and a URL are supplied.

Headless

import { ProvidameClient } from '@providame/sdk-js'

const providame = new ProvidameClient({ publishableKey: 'pk_test_...' })

// Sign up
await providame.signUp({ email, password, fullName })

// Sign in
await providame.signIn({ email, password })

// Sign out
await providame.signOut()

// Check session
providame.isSignedIn() // → boolean

// Listen for state changes
providame.onAuthStateChange((event, data) => {
  // event: 'signed-in' | 'signed-out'
})

signUp() only completes a session when the backend says so. Email verification is always required, so the usual sequence is signUp() then verifySignUp({ flowId, code, password }) — all three fields are required, and omitting any of them throws. Note the case flip: signUp() resolves to the raw response { flow_id, status }, while verifySignUp takes flowId.

const { flow_id } = await providame.signUp({ email, password })
await providame.verifySignUp({ flowId: flow_id, code, password })

The prebuilt SignUp component already handles that two-step flow for you.

Other sign-in strategies

import {
  signInWithOTPEmail, verifyOTPEmailSignIn,
  signInWithOTPSMS,   verifyOTPSMSSignIn,
  signInWithMagicLink, signInWithPasskey,
  resendOTPEmail, resendOTPSMS, resendMagicLink,
} from '@providame/sdk-js'

Multi-factor sign-in

signIn() can return needs_second_factor (an already-enrolled account) or needs_mfa_enrollment (a forced-MFA environment, no second factor yet) instead of completing. The prebuilt <SignIn> already renders both follow-up steps; these are the headless primitives underneath it:

const res = await providame.signIn({ email, password })

if (res.status === 'needs_second_factor') {
  await providame.attemptSignInSecondFactor({
    flowId: res.flow_id,
    strategy: 'totp', // or 'backup_code'
    code,
  })
}

if (res.status === 'needs_mfa_enrollment') {
  const { secret, otpauth_uri } = await providame.enrollSignInTotp({ flowId: res.flow_id })
  // ...render otpauth_uri as a QR code, or show `secret` for manual entry...
  await providame.verifySignInTotpEnrollment({ flowId: res.flow_id, code })
  // A 'complete' response's session carries recovery_codes — display them
  // once; they're never retrievable again.
}

Password reset

<div id="reset-password"></div>
<script type="module">
  import { ProvidameClient, ResetPassword } from '@providame/sdk-js'

  const providame = new ProvidameClient({ publishableKey: 'pk_test_...' })

  ResetPassword.mount({
    client: providame,
    target: '#reset-password',
    onSuccess: () => window.location.href = '/app',
    signInUrl: '/login',
  })
</script>

Headless, or as a modal via providame.openResetPassword({ onSuccess, signInUrl }) / providame.closeResetPassword():

// Always resolves — no account enumeration — a code is only emailed if the
// address exists.
const { flow_id } = await providame.resetPassword({ email })

// The password IS reset by this call regardless of status — only the
// automatic sign-in that follows is gated for an MFA-enrolled account.
const res = await providame.attemptResetPassword({ flowId: flow_id, code, password })

if (res.status === 'needs_second_factor') {
  await providame.attemptResetPasswordSecondFactor({ flowId: flow_id, strategy: 'totp', code: totpCode })
}
if (res.status === 'needs_mfa_enrollment') {
  const { secret, otpauth_uri } = await providame.enrollResetPasswordTotp({ flowId: flow_id })
  await providame.verifyResetPasswordTotpEnrollment({ flowId: flow_id, code: totpCode })
}

await providame.resendResetPassword({ flowId: flow_id })

Social login

import { messageForIdpErrorCode } from '@providame/sdk-js'

const idps = await providame.listEnabledIdps()
// -> [{ provider: 'google', displayName: 'Google' }, ...] — never throws,
// resolves to [] on a network/CORS failure so you just render no buttons

await providame.signInWithIdp('google', { returnUrl: '/app' }) // navigates away

// ProvidameClient strips a providame_error query param on return and
// replays it as a 'social-login-error' event to the first subscriber.
providame.onAuthStateChange((event, data) => {
  if (event === 'social-login-error') alert(messageForIdpErrorCode(data.socialLoginError))
})

Session, tokens, and environment info

// Your own backend's Authorization header — cached, decoded from the
// token's own exp claim; a real round trip only happens once it's actually
// expired, or you pass { skipCache: true }.
const token = await providame.getToken()

const config = await providame.configuration()  // issuer, JWKS URI, Turnstile site key
const branding = await providame.branding()      // this environment's colors/logo — never rejects
const env = await providame.getEnvironment()     // OIDC client ID, kind (dev/staging/prod), status

ProvidameClient already constructs and drives a SessionManager internally (its self-rescheduling silent-refresh loop) — you don't need your own for ordinary use. It's exported for a custom framework integration that needs to control that loop directly: new SessionManager(providame), then .start() (session already known live), .resume() (page-load case — asks the server via providame.me(), since there's no synchronous way to tell), .stop().

Account management

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

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

const passkeys = await providame.user.passkeys.list()
const { passkeyId, creationOptions } = await providame.user.passkeys.register(domain)
await providame.user.passkeys.verify(passkeyId, credential, name)
await providame.user.passkeys.remove(passkeyId)
await providame.user.passkeys.registerWithBrowser(domain, name) // register + browser prompt + verify, one call

// Admin-issued passkey registration link (user is NOT signed in — the
// emailed code is the credential):
await providame.user.passkeys.registerWithCode(identifier, codeId, code, domain)
await providame.user.passkeys.verifyWithCode(passkeyId, identifier, credential, name)

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

// Auth-method + step-up state
const { hasTotp, hasPassword } = await providame.user.mfaState()
await providame.user.hasPassword()

// Step-up re-auth, required by deleteAccount() and any endpoint the backend
// guards with X-Step-Up-Token. Takes { password } or { code } (from
// requestReauthCode(), the only step-up path for an account with no
// password at all — social-login sign-up).
const { stepUpToken, expiresAt } = await providame.user.reauth(currentPassword)
await providame.user.requestReauthCode()
await providame.user.deleteAccount(currentPassword) // takes the same step-up proof, internally re-auths

const { avatarUrl } = await providame.user.uploadAvatar(file)
await providame.user.removeAvatar()
const data = await providame.user.exportData() // GDPR export — everything FAPI
                                                 // already shows this end-user,
                                                 // never private_metadata

// Claims (display/gating only — never verified client-side)
const claims = await providame.getClaims()

mountUserButton and mountUserProfile render full drop-in UI over the same API (each with a matching unmount*), or as modals:

providame.openUserProfile()
providame.closeUserProfile()

Organizations

Providame's B2B tenancy layer: your customers are companies with members and roles. The API matches @providame/core method for method.

import { mountOrganizationSwitcher } from '@providame/sdk-js'

const orgs = await providame.organizations.list()
const org = await providame.organizations.get(orgs[0].id)
await providame.organizations.setActive(orgs[0].id)   // null clears it
await providame.organizations.setActiveBySlug('acme')
// setActive is remembered across sign-ins -- you don't call it on every login

// CRUD
const created = await providame.organizations.create({ name: 'Acme Inc', slug: 'acme' })
await providame.organizations.update(created.id, { name: 'Acme Corp' })
await providame.organizations.delete(created.id)

// Members — setMemberRoles REPLACES across both tiers; [] revokes all
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 (permissions come 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', { displayName: 'Billing Lead' }) // permissionKeys omitted leaves it unchanged
await providame.organizations.deleteRole(org.id, 'billing-owner')

// Invitations — the token itself is never returned here, only emailed
await providame.organizations.invitations(org.id)
await providame.organizations.invite(orgId, { email: '[email protected]' })
await providame.organizations.revokeInvitation(org.id, invitationId)
await providame.organizations.myInvitations()
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)

mountOrganizationSwitcher({
  client: providame,
  target: '#org-switcher',
  onChange: (org) => console.log('switched to', org),
  hideCreate: false,     // hide the "Create organization" action
  hidePersonal: false,   // hide the "no active organization" option
  personalLabel: 'Personal account',
})

Only the switcher is prebuilt here — there's no vanilla equivalent of <OrganizationProfile> or <CreateOrganization> yet. Build those over the API above.

Roles and permissions

import {
  hasRole, hasPermission, hasOrgRole, hasOrgPermission, activeOrganization,
} from '@providame/sdk-js'

await hasRole(providame, 'admin')                    // environment-level grant
await hasOrgPermission(providame, 'billing:manage')  // ACTIVE organization only
await activeOrganization(providame)                  // { id, slug, name } | null

Each has an ...InClaims variant (hasRoleInClaims, activeOrganizationFromClaims, …) taking decoded claims directly, for when you already hold them and don't want the client to fetch. This is display gating only — real enforcement is your backend verifying the token, via @providame/backend.

Object-scoped access

hasRole/hasPermission read claims already in the token — free, and answer "is this user an admin". providame.authz.can() 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' })
if (allowed) {
  // show the Edit button
}

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 ?? [])

No prebuilt <Can>-equivalent component here — call can() directly and render accordingly. Fails closed: a network failure rejects rather than resolving true, so check for that in your own error handling. Answers are cached per-question for the life of the session (cleared on sign-in/out and organization switch) via providame.authz.invalidate().

App audit events

sendAuditEvents(events) posts up to 100 of your app's own audit events for the signed-in user. Every event comes back in exactly one of accepted or rejected — both are final, so delete it locally once it appears in either. An event the same user resends is not stored twice; anything past the first 100 is in neither list, so send it again. Each user may send 30 batches a minute; past that the call throws an error 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: 'record.viewed', timestamp: new Date().toISOString(), resourceId: 'rec_42' },
])

Utilities

import { hasSessionCookie, isWebAuthnSupported, renderTurnstile } from '@providame/sdk-js'

hasSessionCookie() // always false in a real browser — providame_eu_session is
                    // HttpOnly, so document.cookie can never see it. Kept for
                    // backwards compatibility; use isSignedIn()/me() instead.

if (isWebAuthnSupported()) {
  // show "Sign in with a passkey" — gate on real browser support first
}

// Renders a Cloudflare Turnstile widget; onToken fires with the challenge
// token once the user passes it (often invisibly). Returns a cleanup
// function — call it on unmount so a re-mounted form gets a fresh widget.
const cleanup = await renderTurnstile(containerEl, siteKey, (token) => {
  // include token in your own signup/signin request body
})
cleanup()

Docs

Full reference: /docs/sdk/vanilla in your Providame dashboard.

Build

npm run build