@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.
⚠️ 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
cryptomodule - 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/cloudRequirements
- 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:
- Go to Firewall → Activation
- Toggle WAF on for each bucket you want to protect
- (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 queryDimension 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'andepisodeId: 'ep-001'userId: 'user-123'andcategory: 'premium'season: '2'andepisode: '5'creatorId: 'creator-789'andcontentType: '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
- Never expose credentials in client-side code
- Use environment variables for credentials
- Keep credentials server-side (API routes, backends)
- 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
- Documentation: https://docs.podtoo.com
- Email: [email protected]
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
