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

@beesolve/action-tokens

v0.3.0

Published

Generic one-time action token store backed by DynamoDB

Readme

@beesolve/action-tokens

A generic one-time action token store backed by AWS DynamoDB. Use it to implement email verification codes, magic login links, password reset tokens, OTP flows, or any other short-lived single-use credential.

Purpose

Action tokens associate a secret value with an owner and an action. Each token has a remaining-use counter and an expiry time. Calling use() validates the token and decrements the counter; once it reaches zero the token is blocked. DynamoDB's TTL attribute handles automatic cleanup after expiry — no cron job needed.

Tokens are looked up either by (owner, action) as a consistent primary-key read, or by (value, action) via a Global Secondary Index when the owner is unknown at call time (e.g. a magic-link click).

Installation

npm install @beesolve/action-tokens
# or
bun add @beesolve/action-tokens

Usage

1 — Infrastructure (CDK)

Import from @beesolve/action-tokens/cdk and add the construct to your stack. Call grantAccess on each Lambda that needs to interact with tokens — it sets the required IAM permissions and environment variables automatically.

import { ActionTokens } from "@beesolve/action-tokens/cdk";

const tokens = new ActionTokens(this, "ActionTokens", {
  // Enable point-in-time recovery in production
  pointInTimeRecoveryEnabled: isProd,
  // Protect the table from accidental deletion in production
  deletionProtection: isProd,
  removalPolicy: isProd ? RemovalPolicy.RETAIN : RemovalPolicy.DESTROY,
});

// Grants read/write access and injects env vars into the Lambda
tokens.grantAccess(myLambdaFunction);

2 — Lambda handler (SDK)

Import from @beesolve/action-tokens/sdk. The client reads the environment variables injected by grantAccess and creates a DynamoDB client automatically.

Prerequisite: the Lambda must have been granted access via grantAccess before deploying. The module-level env parse will throw at startup if BEESOLVE_ACTION_TOKENS_TABLE_NAME or BEESOLVE_ACTION_TOKENS_INDEX_NAME are missing.

import { ActionTokensClient } from "@beesolve/action-tokens/sdk";

const tokens = new ActionTokensClient();

3 — Token lifecycle

Create a token

import { ActionTokensClient, TokenAlreadyExistsError } from "@beesolve/action-tokens/sdk";
import { randomBytes } from "crypto";

const tokens = new ActionTokensClient();

const token = await tokens.createNew({
  owner: "user-123",
  action: "verify-email",
  value: String(Math.floor(100000 + Math.random() * 900000)), // 6-digit OTP
  remainingUses: 5,
  expiresAt: new Date(Date.now() + 10 * 60 * 1000), // 10 minutes
  data: { emailAddress: "[email protected]" },
  overwrite: false, // throw TokenAlreadyExistsError if a token already exists
});

Use a token

import {
  TokenDoesNotExistError,
  ExpiredTokenError,
  TokenAlreadyUsedUpError,
} from "@beesolve/action-tokens/model";

try {
  const used = await tokens.use({
    owner: "user-123",  // omit to look up by value alone (GSI path)
    action: "verify-email",
    value: submittedCode,
    drainWhenValid: false, // set true to zero out remaining uses in one step
  });

  // token.data contains whatever was stored at creation time
  console.log("verified email:", used.data?.emailAddress);
} catch (error) {
  if (error instanceof TokenDoesNotExistError) { /* token not found */ }
  if (error instanceof ExpiredTokenError)      { /* token has expired */ }
  if (error instanceof TokenAlreadyUsedUpError){ /* no uses remaining */ }
  if (error instanceof Error && error.message === "Token is invalid.") {
    /* value did not match */
  }
}

Drain a token

Deletes the token immediately, regardless of remaining uses. Useful for explicit invalidation (e.g. on sign-out or after a successful one-time action).

await tokens.drain({ owner: "user-123", action: "verify-email" });

4 — Direct model usage (advanced)

Import from @beesolve/action-tokens/model when you want to supply your own DynamoDB client or manage infrastructure yourself.

import { ActionTokens } from "@beesolve/action-tokens/model";
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";

const dynamo = DynamoDBDocumentClient.from(new DynamoDBClient({}));

const tokens = new ActionTokens({
  dynamo,
  tableName: process.env.MY_TABLE_NAME!,
  valueIndexName: process.env.MY_INDEX_NAME!,
});

API

createNew(props)

Creates a new token. Throws TokenAlreadyExistsError if a token with the same (owner, action) already exists and overwrite is false.

| Prop | Type | Description | |---|---|---| | owner | string | Identifies who owns the token (e.g. a user ID) | | action | string | Identifies the purpose of the token (e.g. "verify-email") | | value | string | The secret that must be presented when calling use() | | remainingUses | number | How many times the token can be used before being blocked | | expiresAt | Date | When the token expires | | data | Record<string, unknown> \| undefined | Arbitrary metadata stored with the token | | overwrite | boolean | Replace an existing (owner, action) token if one exists |

use(props)

Validates and consumes one use of the token. Throws if the token does not exist, is expired, or has no remaining uses. An incorrect value still decrements the use counter (see FAQ).

Returns the updated token.

| Prop | Type | Description | |---|---|---| | owner | string \| undefined | If provided, fetches by (owner, action) with a consistent read. If omitted, fetches by (value, action) via the GSI | | action | string | | | value | string | The secret to validate | | drainWhenValid | boolean | If true and the value is correct, sets remainingUses to 0 atomically |

drain(props)

Deletes the token unconditionally.

Error types (from @beesolve/action-tokens/model)

| Class | Thrown when | |---|---| | TokenDoesNotExistError | Token not found | | TokenAlreadyExistsError | createNew called with overwrite: false and a token already exists | | ExpiredTokenError | Token has passed its expiresAt | | TokenAlreadyUsedUpError | remainingUses is already 0 | | MalformedTokenError | Item in DynamoDB does not match the expected schema | | UnexpectedError | Unexpected state (e.g. index corruption) |

FAQ

Why does an incorrect value still consume a use?

Intentional. If guessing the wrong value did not cost a use, an attacker could enumerate values at no cost until finding the correct one. Charging a use for every attempt — regardless of correctness — limits brute-force attempts to remainingUses tries.

What happens when a token expires in DynamoDB?

DynamoDB's TTL attribute (expiresAt) automatically deletes the item within a few minutes of expiry. use() also checks expiresAt in application code before the TTL cleanup runs, so expired tokens are rejected immediately rather than waiting for DynamoDB to remove them.

What is drainWhenValid for?

It atomically sets remainingUses to 0 in the same UpdateCommand that records the use, ensuring no concurrent caller can squeeze in another use after you've validated. Use it for single-use flows (email verification, magic links) where you want to guarantee the token is used exactly once even under concurrent requests.

Can I look up a token without knowing the owner?

Yes — omit owner in use(). The call falls back to a GSI query by (value, action). This is useful for magic-link clicks where the token value arrives in the URL but the owner identity hasn't been established yet.

Why is there both a primary key lookup and a GSI lookup in use()?

The primary key lookup (owner + action) is a strongly consistent read, preferred when the caller already knows the owner. The GSI lookup (value + action) is eventually consistent but allows lookup without the owner — necessary for flows like magic links or QR code scans. Provide owner whenever you have it to get the stronger consistency guarantee.

Can I store the same action for multiple owners?

Yes. The table's primary key is (owner, action), so each owner can have an independent token per action.