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

@podtoo/cloud

v0.3.1

Published

Podtoo Cloud SDK for Node.js - Interact with api.cloud.podtoo.com using AWS SigV4 signing

Readme

@podtoo/cloud

Official Podtoo Cloud SDK for Node.js. Query and analyze your cloud storage data with AWS Signature V4 authentication.

npm version License: MIT

⚠️ Important: Server-Side Only

This package is for Node.js server environments only. It cannot be used directly in browser/client-side code because it:

  • Uses Node.js crypto module
  • Requires secret credentials that must never be exposed to browsers
  • Performs server-side AWS SigV4 signing

✅ Use in: Next.js API routes, Express servers, Node.js backends
❌ Don't use in: React components, browser JavaScript, client-side code

Installation

npm install @podtoo/cloud

Requirements

  • Node.js >= 18.0.0

Quick Start

import PodtooCloud from '@podtoo/cloud';

// Configure once with your credentials
PodtooCloud.conf({
  region: "us-east-1",
  credentials: {
    accessKeyId: process.env.PODTOO_ACCESS_KEY,
    secretAccessKey: process.env.PODTOO_SECRET_KEY,
  },
});

// Analytics with the DataFlow API
const analytics = await PodtooCloud
  .DataFlow("analytic")
  .query({ podcastid: "show-123" })
  .range("7d")
  .execute();

console.log(`Total downloads: ${analytics.analytics.totalDownloads}`);
console.log(`Unique listeners: ${analytics.analytics.uniqueDownloads}`);
console.log(`Bandwidth: ${analytics.analytics.bandwidthGB} GB`);

// Manage firewall rules with the Firewall API
await PodtooCloud
  .Firewall("waf")
  .bucket("myBucket")
  .matchType("ip")
  .matchValue("1.2.3.4")
  .action("block")
  .execute();

🛡️ Firewall API (NEW in v0.3.1)

The Firewall API provides a fluent interface for managing WAF (Web Application Firewall) and PWAF (Per-content WAF) rules on your CDN buckets.

Prerequisites

Before creating rules via the API, you must enable WAF/PWAF on the PodToo Cloud dashboard:

  1. Go to Firewall → Activation
  2. Toggle WAF on for each bucket you want to protect
  3. (Optional) Activate PWAF and select your metadata field — this is a one-time, permanent choice

The API can then create, list, update, and delete rules on those enabled buckets.

Available Operations

| Operation | What it does | HTTP | |-----------|-------------|------| | "waf" | Create a bucket-scoped WAF rule | POST /firewall/waf | | "pwaf" | Create a metadata-scoped PWAF rule | POST /firewall/pwaf | | "rules" | List WAF/PWAF rules | POST /firewall/rules | | "rule" | Update or delete a specific rule | PATCH or DELETE /firewall/rules/:id | | "report" | View block event reports | POST /firewall/report | | "quota" | Check daily block quota | POST /firewall/quota |

Create WAF Rules (Bucket-Scoped)

WAF rules apply to all files in a bucket.

// Block a specific IP
await PodtooCloud
  .Firewall("waf")
  .bucket("myBucket")
  .matchType("ip")
  .matchValue("1.2.3.4")
  .action("block")
  .execute();

// Block a CIDR range with a note
await PodtooCloud
  .Firewall("waf")
  .bucket("myBucket")
  .matchType("cidr")
  .matchValue("103.21.0.0/16")
  .action("block")
  .priority(50)
  .note("Known scraper network")
  .execute();

// Block by user-agent (supports * wildcards)
await PodtooCloud
  .Firewall("waf")
  .bucket("myBucket")
  .matchType("user-agent")
  .matchValue("*BadBot*")
  .action("block")
  .execute();

// Block by country code (ISO-3166)
await PodtooCloud
  .Firewall("waf")
  .bucket("myBucket")
  .matchType("country")
  .matchValue("CN")
  .action("block")
  .execute();

// Allow-list an IP (overrides blocks when both match)
await PodtooCloud
  .Firewall("waf")
  .bucket("myBucket")
  .matchType("ip")
  .matchValue("203.0.113.50")
  .action("allow")
  .priority(10)
  .note("Our monitoring service")
  .execute();

Create PWAF Rules (Metadata-Scoped)

PWAF rules apply only to files matching a specific metadata value. The metadata field (e.g. podcastid) is configured once on the dashboard — you only supply the value via the API.

// Block an IP for a specific podcast
await PodtooCloud
  .Firewall("pwaf")
  .bucket("myBucket")
  .metadataValue("6900006b2953d9f882c0d47d")
  .matchType("ip")
  .matchValue("1.2.3.4")
  .action("block")
  .execute();

// Block ChatGPT scraper for a specific podcast
await PodtooCloud
  .Firewall("pwaf")
  .bucket("myBucket")
  .metadataValue("6900006b2953d9f882c0d47d")
  .matchType("user-agent")
  .matchValue("*ChatGPT*")
  .action("block")
  .note("Block ChatGPT scraper on this podcast")
  .execute();

List Rules

// List all rules for a bucket
const result = await PodtooCloud
  .Firewall("rules")
  .bucket("myBucket")
  .execute();

console.log(`${result.total} rules found`);
result.rules.forEach(rule => {
  console.log(`${rule.scope} | ${rule.matchType}=${rule.matchValue} | ${rule.action}`);
});

// List only PWAF rules
const pwafRules = await PodtooCloud
  .Firewall("rules")
  .bucket("myBucket")
  .scope("metadata")
  .execute();

// List only enabled WAF rules
const activeWaf = await PodtooCloud
  .Firewall("rules")
  .bucket("myBucket")
  .scope("bucket")
  .limit(50)
  .execute();

Update Rules

// Disable a rule
await PodtooCloud
  .Firewall("rule")
  .ruleId("682fa4c0e8531fec569ef5fc")
  .enabled(false)
  .execute();

// Change action from block to allow
await PodtooCloud
  .Firewall("rule")
  .ruleId("682fa4c0e8531fec569ef5fc")
  .action("allow")
  .execute();

// Update priority and add a note
await PodtooCloud
  .Firewall("rule")
  .ruleId("682fa4c0e8531fec569ef5fc")
  .priority(10)
  .note("Moved to high priority")
  .execute();

Delete Rules

await PodtooCloud
  .Firewall("rule")
  .ruleId("682fa4c0e8531fec569ef5fc")
  .remove()
  .execute();

Block Event Reports

// Get blocks grouped by rule over the last 7 days
const report = await PodtooCloud
  .Firewall("report")
  .range("7d")
  .groupBy("rule")
  .execute();

// Response
{
  totalBlocks: 1523,
  quota: { used: 234, limit: 5000, exhausted: false },
  results: [
    { matchType: "user-agent", matchValue: "*ChatGPT*", scope: "metadata", blocks: 892 },
    { matchType: "ip", matchValue: "1.2.3.4", scope: "bucket", blocks: 631 },
  ]
}

// Get blocks for a specific bucket grouped by day
const daily = await PodtooCloud
  .Firewall("report")
  .bucket("myBucket")
  .range("30d")
  .groupBy("day")
  .execute();

// Get blocks for a specific date
const specific = await PodtooCloud
  .Firewall("report")
  .date("2026-04-16")
  .groupBy("ip")
  .execute();

// Get blocks for a date range
const ranged = await PodtooCloud
  .Firewall("report")
  .dateRange("2026-04-01", "2026-04-16")
  .groupBy("rule")
  .execute();

Check Quota

const quota = await PodtooCloud
  .Firewall("quota")
  .execute();

// Response
{
  date: "2026-04-16",
  quota: {
    used: 234,
    limit: 5000,
    remaining: 4766,
    exhausted: false,
    percentUsed: 4.7
  },
  activeRules: 12,
  history: [
    { day: "2026-04-10", blocks: 156 },
    { day: "2026-04-11", blocks: 203 },
    // ...
  ]
}

Firewall API Methods Reference

PodtooCloud
  .Firewall("operation")

  // Target a bucket (by name or ID)
  .bucket("myBucket")
  .bucketId("68f0b7100093f783e54045bf")

  // Rule creation (waf / pwaf)
  .matchType("ip" | "user-agent" | "cidr" | "country" | "asn")
  .matchValue("1.2.3.4")
  .action("block" | "allow")
  .priority(100)            // 0–10000, lower = evaluated first
  .note("Optional note")
  .enabled(true)

  // PWAF only
  .metadataValue("abc123")  // Field is locked on the dashboard

  // Rule targeting (rule operation)
  .ruleId("682fa4c0...")
  .remove()                 // Mark for deletion

  // List filters (rules operation)
  .scope("bucket" | "metadata")
  .limit(100)
  .skip(0)

  // Report options
  .range("24h" | "7d" | "30d")
  .groupBy("rule" | "ip" | "day" | "hour")
  .date("2026-04-16")
  .dateRange("2026-04-01", "2026-04-16")

  // Execute
  .execute()

WAF vs PWAF

| Feature | WAF | PWAF | |---------|-----|------| | Scope | Entire bucket | Specific metadata value | | Use case | Block bad actors from all your content | Block scrapers from a specific podcast/creator | | Pricing | $199/month per bucket, 100k blocks included | $499/month base, 100 metadata values included | | Setup | Toggle per bucket on dashboard | Activate + choose metadata field on dashboard | | API field | .bucket("name") | .bucket("name").metadataValue("id") |

Important Notes

  • Rules activate within 5 minutes — the CDN caches rules in Redis with a 5-minute TTL.
  • Allow rules override blocks when both match at the same priority level.
  • Disabling WAF on a bucket deletes all rules for that bucket (both WAF and PWAF).
  • Deactivating PWAF deletes all metadata-scoped rules across your account.
  • The metadata field is permanent — once chosen during PWAF activation, it cannot be changed.

🚀 DataFlow API (v0.2.4+)

The DataFlow API provides a powerful, fluent interface for querying analytics across 13 different endpoints with unified caching and type safety.

Available Endpoints

| Dimension | Endpoint | Description | |-----------|----------|-------------| | Dimension Endpoints (Group by specific dimensions) | | app | /dataflow/apps | Group by podcast application | | device | /dataflow/devices | Group by device type | | os | /dataflow/oss | Group by operating system | | browser | /dataflow/browsers | Group by browser | | referrer | /dataflow/referrers | Group by referrer | | country | /dataflow/countries | Group by country | | Analytics Endpoints (Specific metrics) | | analytic | /dataflow/analytics | Comprehensive analytics | | download | /dataflow/downloads | Total downloads | | uniquedownload | /dataflow/uniquedownloads | Unique downloads (24h IP dedup) | | bandwidth | /dataflow/bandwidths | Bandwidth usage with grouping | | consumption | /dataflow/consumptions | Media consumption analytics | | Custom Endpoint (Advanced queries) | | custom | /dataflow/custom | Custom queries with MongoDB joins |

Basic Usage

All DataFlow endpoints use the same fluent API:

const result = await PodtooCloud
  .DataFlow("dimension")      // Choose endpoint
  .query({ metadata })        // Filter by metadata
  .range("timeRange")         // Set time range
  .execute();                 // Run query

Dimension Endpoints

Query analytics grouped by specific dimensions:

// Get downloads by podcast app
const apps = await PodtooCloud
  .DataFlow("app")
  .query({ podcastid: "show-123" })
  .range("30d")
  .limit(50)
  .execute();

// Get downloads by device type
const devices = await PodtooCloud
  .DataFlow("device")
  .query({ episodeid: "ep-456" })
  .range("7d")
  .execute();

// Get downloads by country
const countries = await PodtooCloud
  .DataFlow("country")
  .query({ season: "2" })
  .range("90d")
  .limit(25)
  .execute();

Available dimension endpoints:

  • .DataFlow("app") - Group by podcast application
  • .DataFlow("device") - Group by device type (mobile, desktop, tablet)
  • .DataFlow("os") - Group by operating system
  • .DataFlow("browser") - Group by browser
  • .DataFlow("referrer") - Group by referrer URL
  • .DataFlow("country") - Group by country

Analytics Endpoints

Get specific metrics without grouping:

// Comprehensive analytics
const analytics = await PodtooCloud
  .DataFlow("analytic")
  .query({ podcastid: "show-123" })
  .range("currentMonth")
  .execute();

// Total downloads
const downloads = await PodtooCloud
  .DataFlow("download")
  .query({ episodeid: "ep-456" })
  .range("7d")
  .execute();

// Unique downloads (24-hour IP deduplication)
const unique = await PodtooCloud
  .DataFlow("uniquedownload")
  .query({ season: "1" })
  .range("30d")
  .execute();

// Bandwidth with time grouping
const bandwidth = await PodtooCloud
  .DataFlow("bandwidth")
  .query({ podcastid: "show-123" })
  .range("30d")
  .groupBy("day")
  .execute();

// Media consumption (session-based)
const consumption = await PodtooCloud
  .DataFlow("consumption")
  .query({ episodeid: "ep-456" })
  .range("allTime")
  .execute();

Custom Endpoint (Advanced)

Create powerful custom queries with MongoDB joins and flexible grouping:

// Get downloads by country (with automatic IP enrichment)
const custom = await PodtooCloud
  .DataFlow("custom")
  .query({ podcastid: "show-123" })
  .range("30d")
  .execute({
    join: [{ collection: 'ipLocation' }],
    groupBy: ['countryName'],
    aggregations: [
      { metric: 'totalDownloads', field: 'status', function: 'count' },
      { metric: 'totalBytes', field: 'bytes_sent', function: 'sum' }
    ],
    orderBy: { field: 'totalDownloads', direction: 'desc' },
    limit: 25
  });

// Get app usage by device type
const appsByDevice = await PodtooCloud
  .DataFlow("custom")
  .query({ season: "2" })
  .range("90d")
  .execute({
    join: [{ collection: 'dataflow_user_agent' }],
    groupBy: ['app_name', 'device_type'],
    aggregations: [
      { metric: 'sessions', field: 'status', function: 'count' },
      { metric: 'uniqueIPs', field: 'ip', function: 'countDistinct' }
    ],
    orderBy: { field: 'sessions', direction: 'desc' }
  });

DataFlow API Methods

PodtooCloud
  .DataFlow("dimension")
  .query({ podcastid: "show-123", episodeid: "ep-456" })
  .range("24h" | "7d" | "30d" | "currentMonth" | "lastMonth" | "allTime")
  .since("2024-01-01T00:00:00Z")
  .groupBy("day" | "hour" | "month" | "year")  // bandwidth only
  .limit(50)
  .orderBy("metric", "desc")
  .metrics(["downloads", "bandwidth", "uniqueDownloads"])
  .key("audio/podcasts/abc/episode.mp3")        // consumption disambiguation
  .forceRefresh(true)
  .execute()
  .execute({ join, groupBy, aggregations, where, orderBy })  // custom only

🔑 Metadata System

What is Metadata?

Metadata are custom key-value pairs you attach to your uploads to organize and query them later. Think of them as tags or labels for your files.

Examples:

  • podcastId: 'my-podcast' and episodeId: 'ep-001'
  • userId: 'user-123' and category: 'premium'
  • season: '2' and episode: '5'
  • creatorId: 'creator-789' and contentType: 'audio'

✨ Automatic Timestamp Conversion

The SDK automatically converts date/timestamp values to Unix timestamps:

const results = await PodtooCloud
  .DataFlow("download")
  .query({
    podcastid: "show-123",
    uploadedAt: '2024-01-15T10:30:00Z'   // Automatically converted
  })
  .range("30d")
  .execute();

Metadata Rules

✅ DO: Use descriptive keys, be consistent, use standard date formats
❌ DON'T: Use colons in non-timestamp values, use &, =, ? in keys/values


API Reference

Configuration

conf(config)

Initialize the SDK with your credentials.

PodtooCloud.conf({
  region: "us-east-1",
  credentials: {
    accessKeyId: "YOUR_ACCESS_KEY",
    secretAccessKey: "YOUR_SECRET_KEY",
  },
});

setCredentials(accessKeyId, secretAccessKey)

Update credentials at runtime (useful for multi-tenant applications).

PodtooCloud.setCredentials("NEW_ACCESS_KEY", "NEW_SECRET_KEY");

Legacy Analytics Methods (Still Supported)

These methods are still available but we recommend using the DataFlow API:

// Legacy
const analytics = await PodtooCloud.getAnalytics(
  { podcastid: "show-123" },
  { range: "7d" }
);

// DataFlow API (Recommended)
const analytics = await PodtooCloud
  .DataFlow("analytic")
  .query({ podcastid: "show-123" })
  .range("7d")
  .execute();

Legacy methods: getAnalytics, getUniqueDownloads, getTotalDownloads, getBandwidth, getConsumption, queryByMetadata


Usage Examples

Next.js API Route with DataFlow + Firewall

// pages/api/podcast-stats.ts
import type { NextApiRequest, NextApiResponse } from 'next';
import PodtooCloud from '@podtoo/cloud';

PodtooCloud.conf({
  region: "us-east-1",
  credentials: {
    accessKeyId: process.env.PODTOO_ACCESS_KEY!,
    secretAccessKey: process.env.PODTOO_SECRET_KEY!,
  },
});

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
  try {
    const { podcastid, range = '30d' } = req.query;

    // Parallel queries: analytics + firewall status
    const [analytics, apps, firewallRules, quota] = await Promise.all([
      PodtooCloud.DataFlow("analytic")
        .query({ podcastid })
        .range(range)
        .execute(),

      PodtooCloud.DataFlow("app")
        .query({ podcastid })
        .range(range)
        .limit(10)
        .execute(),

      PodtooCloud.Firewall("rules")
        .bucket("myBucket")
        .execute(),

      PodtooCloud.Firewall("quota")
        .execute(),
    ]);

    return res.status(200).json({ analytics, apps, firewallRules, quota });
  } catch (error: any) {
    return res.status(500).json({ error: error.message });
  }
}

Express: Firewall Dashboard API

import express from 'express';
import PodtooCloud from '@podtoo/cloud';

const app = express();
app.use(express.json());

PodtooCloud.conf({
  region: "us-east-1",
  credentials: {
    accessKeyId: process.env.PODTOO_ACCESS_KEY,
    secretAccessKey: process.env.PODTOO_SECRET_KEY,
  },
});

// Get firewall overview for a bucket
app.get('/api/firewall/:bucket', async (req, res) => {
  try {
    const [rules, quota, report] = await Promise.all([
      PodtooCloud.Firewall("rules")
        .bucket(req.params.bucket)
        .execute(),

      PodtooCloud.Firewall("quota")
        .execute(),

      PodtooCloud.Firewall("report")
        .bucket(req.params.bucket)
        .range("7d")
        .groupBy("day")
        .execute(),
    ]);

    res.json({ rules, quota, report });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

// Create a rule from a user request
app.post('/api/firewall/:bucket/block', async (req, res) => {
  try {
    const { matchType, matchValue, metadataValue } = req.body;

    const builder = metadataValue
      ? PodtooCloud.Firewall("pwaf")
          .bucket(req.params.bucket)
          .metadataValue(metadataValue)
      : PodtooCloud.Firewall("waf")
          .bucket(req.params.bucket);

    const result = await builder
      .matchType(matchType)
      .matchValue(matchValue)
      .action("block")
      .execute();

    res.json(result);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

app.listen(3000);

TypeScript Support

Full TypeScript definitions included:

import PodtooCloud, {
  type FirewallOperation,
  type FirewallMatchType,
  type FirewallAction,
  type DataFlowDimension,
} from '@podtoo/cloud';

Security Best Practices

  1. Never expose credentials in client-side code
  2. Use environment variables for credentials
  3. Keep credentials server-side (API routes, backends)
  4. Use HTTPS in production

Error Handling

try {
  await PodtooCloud
    .Firewall("waf")
    .bucket("myBucket")
    .matchType("ip")
    .matchValue("1.2.3.4")
    .action("block")
    .execute();
} catch (error) {
  if (error.message.includes('status: 403')) {
    // WAF not enabled on this bucket — enable it on the dashboard
    console.error('WAF not activated for this bucket');
  } else if (error.message.includes('status: 409')) {
    console.error('Rule already exists');
  } else {
    console.error('Error:', error.message);
  }
}

Support


License

MIT © Podtoo


Changelog

0.3.0 (Current)

Added

  • 🛡️ Firewall API: Fluent interface for WAF and PWAF rule management
  • 6 Firewall operations: waf, pwaf, rules, rule, report, quota
  • WAF rules: Block/allow by IP, CIDR, user-agent, country code, ASN
  • PWAF rules: Metadata-scoped rules for per-content protection
  • Subscription gates: API enforces WAF/PWAF activation via dashboard
  • Locked metadata field: PWAF metadata field set once on dashboard, API reads from subscription
  • Rule lifecycle: Create, list, update, enable/disable, delete rules
  • Block reports: View block events grouped by rule, IP, day, or hour
  • Quota monitoring: Check daily block quota and 7-day history

Changed

  • Firewall routes use /firewall/* prefix (not /dataflow/waf*)
  • PWAF rules no longer accept metadataField — read from org subscription
  • WAF/PWAF activation and bucket enrollment managed exclusively via dashboard

0.2.11

  • Added Key to consumption for multiple files.

0.2.10

  • Improved Where query for custom route.
  • Removed AXIOS as no longer required.

0.2.9

  • Error with AXIOS - trying https

0.2.8

  • Error with AXIOS request method

0.2.7

  • Removed Fetch and Added AXIOS due to error with Fetch

0.2.6

  • Force IPv4 connection.

0.2.5

  • Fix GroupBy issue.

0.2.4

Added

  • 🚀 Complete DataFlow API: Unified fluent interface for all analytics
  • 13 DataFlow endpoints: 7 dimensions + 5 analytics + 1 custom
  • Custom endpoint: Flexible queries with MongoDB joins
  • IP enrichment: Automatic IP geolocation from DB-IP API
  • 4-hour caching: Intelligent cache with request deduplication
  • Full TypeScript support for all DataFlow methods

0.2.0

Added

  • 🎉 Dual-format support: ESM and CommonJS

0.1.0

  • Initial public release