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/backend

v0.0.15

Published

Providame backend SDK — token verification, webhook signature verification, and typed access to the Providame Backend API from your own server.

Downloads

555

Readme

@providame/backend

Server-side SDK for Providame. Verify end-user access tokens, verify webhook signatures, and read your environment's end-users from your own backend.

This package must never run in a browser — it takes a secret key. For frontend code use @providame/sdk-js, @providame/react, or @providame/vue.

Install

npm install @providame/backend

Setup

import { createProvidameBackend } from '@providame/backend'

export const providame = createProvidameBackend({
  secretKey: process.env.PROVIDAME_SECRET_KEY!,
})

Config discovery works from the secret key alone. Pass publishableKey only if you would rather discovery use the public endpoint, or supply jwksUri, issuer, and audience explicitly to skip discovery entirely.

Authenticating requests

export async function GET(request: Request) {
  const { isAuthenticated, userId } = await providame.authenticateRequest(request)

  if (!isAuthenticated) {
    return Response.json({ error: 'Unauthorized' }, { status: 401 })
  }

  return Response.json({ userId })
}

authenticateRequest never throws — every failure returns isAuthenticated: false plus a machine-readable reason (no-authorization-header, malformed-authorization-header, token-invalid, token-expired, config-unavailable).

It reads Authorization: Bearer only, never cookies.

Use providame.verifyToken(token) instead when you already hold a token and want it validated or an error thrown.

Authorizing on roles, permissions, and organizations

The result carries the caller's active organization and a has() gate:

const auth = await providame.authenticateRequest(request)

auth.orgId          // active organization id, or null
auth.orgSlug
auth.orgRoles       // roles in the ACTIVE organization, both tiers
auth.orgPermissions // permissions resolved from those roles

if (!auth.has({ orgPermission: 'billing:manage' })) {
  return Response.json({ error: 'Forbidden' }, { status: 403 })
}

has() ANDs every constraint you supply — role, permission, orgRole, orgPermission — and ignores the ones you omit. has({}) therefore means "any authenticated session", not deny-all.

Two distinctions worth getting right:

  • orgRoles/orgPermissions cover the active organization only. Environment-level grants live in permission/role and do not appear there. A user who is an admin of organization B sees no trace of that while organization A is active.
  • Every field is read from the verified claims. Nothing here decodes the token unverified, which is what stops the bearer forging their own organization.

Only the active organization is in the token. To ask about a user's other memberships, use providame.organizations.forUser(userId) below.

Verifying without a client

verifyToken is also exported standalone, for edge middleware, API gateways, or anything that authenticates requests but never calls the Backend API.

import { verifyToken } from '@providame/backend'

const { userId, claims } = await verifyToken(token, {
  secretKey: process.env.PROVIDAME_SECRET_KEY,
})

How verification works, and what a key is actually for

Tokens are RS256. Providame signs with a private key; you verify against the matching public key from the deployment's JWKS, which is served unauthenticated. Nothing is verified "against" your secret key — it is an API credential, not a signing key.

A key is needed only to discover one value: the audience (your environment's project ID). Issuer and JWKS URI are the same for the whole deployment, but the audience is per-environment, and checking it is what stops a token minted for one environment from validating against another.

So any of these work:

// Secret key — discovery via the Backend API. One env var.
await verifyToken(token, { secretKey: process.env.PROVIDAME_SECRET_KEY })

// Publishable key — discovery via the public FAPI endpoint. Preferred for
// edge/gateway deployments, which should not hold a secret at all.
await verifyToken(token, { publishableKey: process.env.PROVIDAME_PUBLISHABLE_KEY })

// No credential at all — supply the three values yourself, no discovery call.
await verifyToken(token, {
  jwksUri: 'https://api.providame.com/fapi/v1/.well-known/jwks.json',
  issuer: 'https://api.providame.com',
  audience: 'your-project-id',
})

If you pass both keys, the publishable one is used, so the secret never goes on the wire.

This throws on an invalid, expired, or wrongly-audienced token. It resolves config per call — construct a client with createProvidameBackend() instead if you want discovery memoized across many verifications.

Verifying webhooks

import { verifyWebhook } from '@providame/backend/webhooks'

export async function POST(request: Request) {
  try {
    const event = await verifyWebhook(request, {
      secret: process.env.PROVIDAME_WEBHOOK_SECRET!,
    })
    // event: { id, type, environment_id, data }
    return Response.json({ received: true })
  } catch {
    return Response.json({ error: 'Invalid signature' }, { status: 400 })
  }
}

⚠️ The raw body is required

The signature covers the exact bytes Providame sent. If you parse the JSON and re-serialize it, key order, whitespace, and unicode escaping all change, and verification will fail every time.

  • Next.js App Router — verifyWebhook(request, …) reads the body itself. Do not call request.json() first.
  • Express — mount express.raw({ type: 'application/json' }) on the route and pass the resulting Buffer as a string to verifyWebhookPayload.
  • Already read the body? Use verifyWebhookPayload(rawBody, signatureHeader, options).
import { verifyWebhookPayload, SIGNATURE_HEADER } from '@providame/backend/webhooks'

const event = await verifyWebhookPayload(
  rawBodyString,
  req.header(SIGNATURE_HEADER)!,
  { secret: process.env.PROVIDAME_WEBHOOK_SECRET! },
)

Signatures older than 5 minutes are rejected (replay protection). Override with toleranceSeconds. Deliveries retry with a stable id, so dedupe on it.

Failures throw WebhookVerificationError, which carries a machine-readable reason (missing-signature, malformed-signature, timestamp-out-of-tolerance, signature-mismatch, invalid-payload) so you can distinguish a misconfiguration from a forgery attempt.

Note: the webhook event is returned exactly as signed, so its fields are snake_case (environment_id). Every other response in this package is camelCase. This is deliberate — it lets you compare the event byte-for-byte against the signed payload and the dashboard's event log.

End-users

const { users, total } = await providame.users.list({
  limit: 25,
  search: 'ada@',
  sort: '-created_at',   // an unrecognised sort is rejected, not ignored
})
const user = await providame.users.get('u_123')          // includes `active` (ban status)
const metadata = await providame.users.getMetadata('u_123') // all 3 tiers, incl. private
const { roleKeys } = await providame.users.getRoles('u_123')

getMetadata returns privateMetadata — this is a trusted server-to-server surface, unlike the frontend SDKs, which never expose that tier.

Note that list() enriches each row with avatarUrl and scimProvisioned, while get() does not return those two fields.

Writing

The same operations the dashboard performs, so anything you can do by clicking you can also do from a deploy script or an admin tool of your own.

const { id } = await providame.users.create({
  email: '[email protected]',
  givenName: 'Ada',
  password: process.env.TEMP_PASSWORD,
  username: 'ada_lovelace',    // optional sign-in handle, for migrations
})

// Partial: an omitted field is left unchanged, names included.
await providame.users.update(id, { givenName: 'Ada King', username: 'alovelace' })

await providame.users.ban(id)     // blocks sign-in, keeps the account
await providame.users.unban(id)   // also how you approve a waitlisted sign-up
await providame.users.delete(id)  // permanent, and clears all our records of them

await providame.users.setMetadata(id, { publicMetadata: { tier: 'gold' } })
await providame.users.deleteMetadataKey(id, 'public', 'tier')
await providame.users.setRoles(id, ['editor'])   // replaces, does not merge

await providame.users.resetPassword(id)              // generates one, emails it
await providame.users.resendVerification(id)         // unverified accounts only
await providame.users.createPasskeyRegistrationLink(id)

const { permissionKeys } = await providame.users.getPermissions(id)

Three things worth knowing before you use these:

  • update({ password }) revokes every live session, so a stolen refresh token stops working immediately rather than continuing to mint access tokens. resetPassword() does the same, but generates the password itself and emails it to the account owner — the plaintext never comes back to you.
  • setRoles() and setMetadata() differ. setRoles replaces the whole grant (an empty array revokes everything); setMetadata replaces only the tiers you supply, and within one, only the keys you name.
  • create() defaults to emailVerified: true. Pass false if you want the user to verify — and note resendVerification() only works for an account that has not verified yet.

Sessions and export

const { sessions } = await providame.users.listSessions(id)
await providame.users.revokeSession(sessions[0].id)   // a SESSION id, not a user id

const csv = await providame.users.exportCsv()

revokeSession() signs one device out. That is a different action from ban() (blocks every future sign-in) and from changing the password (revokes every session at once).

exportCsv() returns the whole directory as a string, one <tier>_metadata.<key> column per metadata key. Your data is always yours to take out, and from a backend you can put that on a schedule.

Sign-up invitations

An invitation admits one specific address into your environment's user pool. It is an allowance, honoured under every sign-up policy mode rather than only invite_only: naming one exact address is a more specific decision than a general rule, so it also overrides a blocklist and skips a waitlist.

const invite = await providame.signupInvites.create({
  email: '[email protected]',
  sendEmail: false,   // you are delivering it yourself
})
invite.token   // single-use, expires in 7 days — treat it as a credential

const { invites, total } = await providame.signupInvites.list()
await providame.signupInvites.revoke(invite.id)

Creation returns the plaintext token so you can deliver the invitation through your own email, Slack, or SMS. The dashboard's equivalent never returns it, because that response lands in a browser. Pair it with sendEmail: false so we don't send a second invitation alongside yours.

Creating one returns 409 if the address is already a user here, or already holds a live invitation — revoke the old one rather than issuing a second.

App audit events

Audit events your apps send through FAPI's POST /me/audit-events (for example with @providame/core's sendAuditEvents()), read back for export or review. Read-only; newest first.

const { events, total } = await providame.auditEvents.list({
  filter: { action: "trip.viewed", occurred_at: ">=2026-09-01" },
  sort: "-occurred_at",
  limit: 100,
})
// events[i].submittedBy — the verified user whose session sent it
// events[i].userId      — the user your app recorded (can differ on a shared device)

Organizations

Organizations are Providame's B2B tenancy layer: your customers are companies with members and roles. Unlike end-users, this surface is full read/write.

const { organizations, total } = await providame.organizations.list({ limit: 25 })

const org = await providame.organizations.create({
  slug: 'acme',
  name: 'Acme Inc',
  createdBy: 'u_123', // optional: seat this user as the first member
})

await providame.organizations.update(org.id, { name: 'Acme Corp', maxMembers: 50 })
await providame.organizations.delete(org.id)

// Organizations a user belongs to, with roles and resolved permissions.
// Useful when you hold a user id but no token to decode.
const memberships = await providame.organizations.forUser('u_123')

Members

const { members } = await providame.organizations.memberships.list(orgId)

// Omit `roles` to grant the environment's configured default member roles.
await providame.organizations.memberships.add(orgId, { userId: 'u_123' })

// Replaces the member's FULL role set across both tiers. `[]` revokes all.
await providame.organizations.memberships.setRoles(orgId, 'u_123', [
  { key: 'org:admin', scope: 'environment' },
  { key: 'billing-owner', scope: 'organization' },
])

await providame.organizations.memberships.remove(orgId, 'u_123')

A role carries a scope. "environment" roles come from the environment's shared catalog and are reusable across every organization; "organization" roles exist only inside one. Both may be granted to the same member, which is why setRoles replaces across both tiers at once — a partial update would silently strip the other tier.

add() is idempotent, so re-adding an existing member is safe.

Org-local roles

const roles = await providame.organizations.roles.list(orgId)

await providame.organizations.roles.create(orgId, {
  roleKey: 'billing-owner',
  displayName: 'Billing Owner',
  permissionKeys: ['billing:manage'],
})

// Omitted fields are left unchanged; an explicit [] revokes every permission.
await providame.organizations.roles.update(orgId, 'billing-owner', {
  permissionKeys: ['billing:manage', 'invoices:read'],
})

await providame.organizations.roles.delete(orgId, 'billing-owner')

Org-local roles draw their permissions from the environment's catalog, so permission keys stay flat and enumerable per environment.

Invitations and verified domains

const invite = await providame.organizations.invitations.create('org_1', {
  email: '[email protected]',
  roles: [{ key: 'org:admin', scope: 'environment' }],
  sendEmail: false,
})
invite.token   // as above: returned only here, never to a browser

await providame.organizations.invitations.list('org_1')
await providame.organizations.invitations.revoke('org_1', invite.id)

const domain = await providame.organizations.domains.create('org_1', {
  domain: 'acme.com',
})
domain.challengeRecord   // publish this DNS TXT record to verify ownership

enrollmentMode defaults to manual_invitation. The automatic modes start admitting people the moment DNS resolves, which is not a thing to switch on by accident — so you have to ask for it.

This surface is fully trusted

Unlike the end-user-facing API in @providame/core, nothing here checks org:sys_* permissions, and creation ignores the self-serve caps (allow_end_user_creation, max_orgs_per_user) — those bound what an end-user may do, not a server-to-server call. That is exactly what lets you run an invite-only model: self-serve off, your own backend creating organizations. Tenant isolation still applies in full; the secret key scopes every call to its own environment.

Authorization

Fine-grained, per-object authorization on top of the roles/permissions above — "can this user edit this specific document", not just "does this user have the posts:write permission". Every question is a user, a permission (e.g. "edit"), and an optional resource ("type:id", e.g. "document:42"). Omit resource to check the environment's global permission catalog instead — the same posts:write-style keys your roles already grant.

This is the trusted, server-side surface — you name the user. The client-side packages (@providame/core/vue/react/sdk-js) have a narrower counterpart, providame.authz.can()/listResources() (plus a <Can> component in Vue/React), which answers only for the signed-in caller and never accepts a user id — appropriate for a frontend authenticated by a publishable key, which is public by design.

Use forRequest() inside a request handler, not can() directly — every check issued in the same tick collapses into one HTTP call instead of one round trip per check:

app.get('/documents', async (req, res) => {
  const authz = providame.authz.forRequest()
  const documents = await loadDocuments()
  const withAccess = await Promise.all(
    documents.map(async (doc) => ({
      ...doc,
      canEdit: await authz.can({ user: req.userId, permission: 'edit', resource: `document:${doc.id}` }),
    })),
  )
  res.json(withAccess)
})

Reach for a single can() only when you genuinely have just one question to ask:

const allowed = await providame.authz.can({
  user: userId,
  permission: 'edit',
  resource: 'document:42',
})

Filter a list without checking every row — one round trip instead of N. listResources returns one ResourceAccess per resource type asked about, whose scopes is a UNION of shapes a caller compiles into SQL: "all" (every record), "organization" (every record in one named organization), "instances" (a specific enumerable set of ids), or "subtree" (a self-linked hierarchy's root — "this and everything beneath it", never expanded, at any size). A subject can hold several at once:

const [access] = await providame.authz.listResources({
  user: userId,
  permission: 'edit',
  resourceType: 'document',
})
const editableDocumentIds = access.scopes.some((s) => s.kind === 'all')
  ? allDocumentIds
  : access.scopes.flatMap((s) => s.instances ?? [])

See the Authorization Lists guide (in the dashboard docs) for compiling every scope kind into SQL and for materialising access into your own database via the sync feed.

An organization can also be named, the same way can() accepts one — it ADDS a place to look, never narrows what an environment-wide role/permission already grants:

await providame.authz.can({
  user: userId,
  permission: 'edit',
  resource: 'document:42',
  organizationId: orgId,
})

Conditions

A relation's subject can be authored (in the dashboard, or via authz.conditions below) to require a named CEL condition — "editor, but only during business hours." A check against a conditioned relation needs the parameter values that condition's expression references, passed as context:

const allowed = await providame.authz.can({
  user: userId,
  permission: 'edit',
  resource: 'document:42',
  context: { current_hour: new Date().getHours() },
})

context is checked, not merely accepted — omitting a parameter the condition's expression actually needs is an error, not a silent false. That's deliberate: a bug that drops a context value should fail loudly rather than look identical to "access denied." can(), canEach() (via forRequest()), and listResources() all accept context; on a batch, each check's own context applies only to that check — one shared context is not spread across the batch.

Resource types, their relations, and the permissions computed from them can be authored from the dashboard's Authorization section, or from your own backend — see Authoring the schema below. Once a type exists, grant and remove access on its records at any time — there's no separate registration step per record. A grant gives a role (or a single permission) on one resource, to a user or to everyone holding a role:

// Alice can edit document 42.
await providame.authz.assign({ subject: { type: 'user', id: 'alice' }, resource: 'document:42', role: 'editor' })

// Everyone who holds the "reviewer" role — now or later — can view it.
await providame.authz.assign({ subject: { type: 'role', key: 'reviewer' }, resource: 'document:42', permission: 'view' })

await providame.authz.unassign({ subject: { type: 'user', id: 'alice' }, resource: 'document:42', role: 'editor' })

// What was granted directly on this record (not what anyone inherits —
// use can() for an access decision).
const grants = await providame.authz.assignments({ resource: 'document:42' })

// Every member of an organization, or holders of one organization's own role.
await providame.authz.assign({ subject: { type: 'organization', id: orgId }, resource: 'document:42', role: 'viewer' })
await providame.authz.assign({ subject: { type: 'role', key: 'billing', organizationId: orgId }, resource: 'document:42', permission: 'view' })

// A permission handed straight to a user, no role in between
// (environment-wide, or within one organization with organizationId).
await providame.authz.grantPermission({ user: 'alice', permission: 'invoices:export' })
await providame.authz.revokePermission({ user: 'alice', permission: 'invoices:export' })

A link places one record under another of the same type (nested folders), so a grant on the parent reaches its children. It is structural, never a grant itself:

await providame.authz.link({ resource: 'folder:child', link: 'parent', target: 'folder:root' })
await providame.authz.unlink({ resource: 'folder:child', link: 'parent', target: 'folder:root' })
const parents = await providame.authz.links({ resource: 'folder:child' })

The type names user, group, role, permission, organization, environment and resource are reserved by Providame and cannot be used for your own resource types.

Authoring the schema

Resource types, permissions, roles, and conditions can all be authored from your own backend via authz.resourceTypes/authz.permissions/authz.roles/ authz.conditions — the same model the dashboard's Authorization section edits, so a CI pipeline can apply it the same way it applies a database migration.

Resource types — your own nouns. hasInstances defaults to true and is fixed at creation; a resource with no records of its own (billing, settings) sets it false, and can carry permissions but never links. get returns the type's permissions, links, and a read-only preview of what it compiles to; list returns names and descriptions only.

const types = await providame.authz.resourceTypes.list()
const document = await providame.authz.resourceTypes.get('document')

await providame.authz.resourceTypes.create({
  name: 'document',
  displayName: 'Document',
  description: 'A document in the workspace',
  // hasInstances: false,  // a resource with no records of its own
})

await providame.authz.resourceTypes.update('document', { displayName: 'Doc' })
await providame.authz.resourceTypes.delete('document')

Links are declared per type, named from the argument rather than a body field. A self-link — a type linking to ITSELF — is the only shape that carries inherited access (nested folders, any parent/child hierarchy of one type). A link between two DIFFERENT types is structural only, useful for your own navigation, since a permission can never be shared across two types under this design.

await providame.authz.resourceTypes.upsertLink('document', 'folder', { targetType: 'document' })
await providame.authz.resourceTypes.deleteLink('document', 'folder')

Permissions — a permission belongs to exactly ONE resource type, fixed at creation. Omit resourceType for the built-in app-wide resource. The key and resource type are both immutable afterwards; only name and description update.

const permissions = await providame.authz.permissions.list()

await providame.authz.permissions.create({
  permissionKey: 'document:edit',
  name: 'Edit',
  resourceType: 'document',
})

await providame.authz.permissions.update('document:edit', { name: 'Edit document' })
await providame.authz.permissions.delete('document:edit')

Roles — a named bundle of permissions from the catalog. Only a permission that's also applied to a resource type turns into a real relation in the compiled model, so a role holding only catalog-scoped permissions is valid but inert. update replaces permissionKeys wholesale when the field is present (including [], which clears it) — omit it to leave the assignment untouched.

const roles = await providame.authz.roles.list()

await providame.authz.roles.create({
  roleKey: 'editor',
  displayName: 'Editor',
  permissionKeys: ['document:edit'],
})

await providame.authz.roles.update('editor', { displayName: 'Content editor' }) // permissions unchanged
await providame.authz.roles.update('editor', { permissionKeys: [] })            // revokes every permission
await providame.authz.roles.delete('editor')

Conditions — named CEL expressions a role or link grant may carry. params declares the TYPES the expression references; the values are supplied per-check, as context on can(). Writing one recompiles the model, and the compiler is the only authority on validity — an invalid expression is rejected and never stored.

const conditions = await providame.authz.conditions.list()

await providame.authz.conditions.upsert('office_hours', {
  expression: 'current_hour >= 9 && current_hour < 17',
  params: { current_hour: 'int' },
})

await providame.authz.conditions.delete('office_hours')

A permission or role Providame seeds itself (the six org:sys_* permissions, org:admin/org:member) cannot be deleted through this surface, same as the dashboard. Deleting a resource type is refused while another type links to it or permissions still belong to it; deleting a condition is refused while a grant still references it.

JWKS

const { keys } = await providame.jwks.get()

You rarely need this. verifyToken and authenticateRequest already fetch and cache these keys. It exists for tooling that wants to inspect or pre-warm them.

Runtime support

Node 18+, Vercel Edge, and Cloudflare Workers. Uses Web Crypto and fetch only — no Node built-ins.

Not yet supported

Impersonation. Minting an access token that acts as one of your users is deliberately dashboard-only, behind the users:impersonate role. A secret key carries no roles at all, so exposing it here would mean a leaked key can act as any end-user in your environment.

License

MIT