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

@suluk/cloudflare

v0.1.3

Published

Butter-smooth, API-driven provisioning + deployment for a Suluk app on Cloudflare — no wrangler CLI. A typed REST client + idempotent provisioners (D1, KV, R2, secrets) + the Workers module-script + static-assets upload flow, orchestrated into one deploy(

Readme


CANDIDATE tooling — not official OpenAPI. Suluk is a single-contributor candidate for OpenAPI Specification v4.0 ("Moonwalk"), unaffiliated with the OpenAPI Initiative and unable to ratify anything on the SIG's behalf.

Install

bun add @suluk/cloudflare

What it does

A typed wrapper over the Cloudflare REST API that ships a Worker + its dependencies in dependency order, with no wrangler binary and no wrangler.toml.

  • CloudflareClient — a thin, typed client over https://api.cloudflare.com/client/v4 with Bearer auth, the standard { success, errors, result } envelope unwrapped, and a CloudflareError that carries the API's own error codes (so a failed deploy says why). fetch is injectable, so the whole library is unit-testable without a network.
  • Idempotent provisionersprovisionD1 / provisionKvNamespace / provisionR2Bucket are create-or-get (re-running never errors with "already exists"), applyMigrations keeps a _suluk_migrations ledger so each migration runs at most once (and baselines a DB migrated before the ledger existed), and putSecrets skips empty values.
  • Workers static-assets uploaduploadAssets builds the manifest, opens an upload session, and pushes only the buckets the API asks for (so a redeploy only sends changed files). _headers / _redirects are routed into the worker's assets.config as rules, not uploaded as serveable blobs.
  • One-call deploy() — provision → migrate → upload assets → deploy the module worker (with bindings + vars) → push secrets → set cron triggers, in the order where each step's output feeds the next. keep_bindings preserves secrets across redeploys, so you set them once.
  • Durable Object agents — pass durableObjects: [{ binding, className }] and deploy() binds each as a durable_object_namespace and creates the same-script classes via an inline script migration (new_sqlite_classes, the Agents SDK + free-plan backend). The migration rides the same script-upload PUT (no versions API) and uses the API field new_tag (≠ wrangler's tag); it's omitted entirely when there's nothing to create (an empty block can reset DO state). nodejs_compat is auto-injected for any DO deploy so the worker can't ship missing the Agents-SDK runtime flag. Safe for first-deploy + redeploy; additive evolution (oldTag/new_tag to add a class later) is plumbed but caller-driven.
  • kvRateLimitStore — the production, KV-backed RateLimitStore for @suluk/hono's enforceRateLimit (a fixed-window counter that fails open on a KV blip). Structurally typed, so it plugs straight in with no @suluk/hono dependency.

When to reach for it

Reach for @suluk/cloudflare when you want to deploy a Suluk app (or any static/worker site) to Cloudflare programmatically — from a deploy.ts script or CI — rather than via the wrangler CLI. It is the executing adapter: it holds your token and actually calls the API.

  • vs @suluk/deploy@suluk/deploy is pure planning behind a swappable provider interface; it computes the files + ordered steps to ship but never executes anything or touches credentials. @suluk/cloudflare is the concrete Cloudflare doer. Use @suluk/deploy when you want a provider-agnostic plan; use this when you're committing to Cloudflare and want it to run.

Usage

The one-call deploy

deployWith builds a client from your token/account and runs a full deploy. Pass it a DeployPlan of bytes, not paths — the disk-reading lives in your app script (see Boundary).

import { deployWith, type AssetFile } from "@suluk/cloudflare";
import { readFileSync } from "node:fs";

const assets: AssetFile[] = [
  { path: "/index.html", bytes: new Uint8Array(readFileSync("dist/index.html")), contentType: "text/html" },
  // …walk your dist/ dir and map each file to { path, bytes, contentType }
];

const res = await deployWith(
  { apiToken: process.env.CLOUDFLARE_API_TOKEN!, accountId: process.env.CLOUDFLARE_ACCOUNT_ID },
  {
    scriptName: "saasuluk",
    module: readFileSync("worker/dist/worker.js", "utf8"), // the bundled ES module
    compatibilityDate: "2026-06-01",
    compatibilityFlags: ["nodejs_compat"],
    d1: { binding: "DB", databaseName: "saasuluk-db", migrations: [{ name: "0000_init.sql", sql: "CREATE TABLE t (id INTEGER);" }] },
    kv: [{ binding: "RATE_LIMIT_KV", title: "saasuluk-ratelimit" }],
    r2: [{ binding: "MEDIA", bucketName: "saasuluk-media" }],
    assets,
    assetsConfig: { html_handling: "auto-trailing-slash" },
    vars: { STRIPE_METER_EVENT_NAME: "saasuluk_cost" },     // plain-text bindings
    secrets: { BETTER_AUTH_SECRET: process.env.BETTER_AUTH_SECRET }, // encrypted; empty/undefined skipped
    crons: ["0 * * * *"],
    observability: true,
  },
  (msg) => console.log("  " + msg), // optional DeployLog — narrates each step
);

console.log(`Deployed "${res.scriptName}" to ${res.accountId} — D1 ${res.d1?.id}, ${res.assetsUploaded} assets, secrets: ${res.secretsSet.join(", ")}`);

The token must be Account-scoped: Workers Scripts (Edit), D1 (Edit) (+ KV/R2 Edit if used), and Account Settings (Read). Omit accountId and it resolves to the token's first account.

deploy(cf, plan, log?) is the same orchestration over an existing CloudflareClient if you already hold one.

Driving the client / resources directly

Every step deploy() runs is also exported. Use them for one-off work the plan doesn't cover (e.g. seeding a DB after migrations, or attaching a route via raw request):

import { CloudflareClient, queryD1, provisionKvNamespace } from "@suluk/cloudflare";

const cf = new CloudflareClient({ apiToken: process.env.CLOUDFLARE_API_TOKEN! });

const db = await provisionKvNamespace(cf, "my-cache"); // create-or-get, idempotent
await queryD1(cf, databaseId, "INSERT OR REPLACE INTO setting (k, v) VALUES ('seeded', '1');");

// anything not in the plan yet (routes, custom domains, …) → the raw typed request:
await cf.request("PUT", `/accounts/${await cf.resolveAccountId()}/workers/scripts/saasuluk/routes`, {
  json: { pattern: "example.com/*", zone_name: "example.com" },
});

KV-backed rate limiting (inside the Worker)

kvRateLimitStore is the production store for @suluk/hono's enforceRateLimit. The KV binding isn't available at module init on Workers, so pass a lazy getter; it falls open to an in-memory fallback when KV is absent or errors.

import { kvRateLimitStore } from "@suluk/cloudflare";
import { enforceRateLimit } from "@suluk/hono";

let kv: KVNamespace | undefined;
const store = kvRateLimitStore(() => kv); // captures the binding on first request

app.use("*", async (c, next) => { if (!kv && c.env.RATE_LIMIT_KV) kv = c.env.RATE_LIMIT_KV; return next(); });
app.use("*", enforceRateLimit({ store, /* …operationOf, rateLimitOf, keyOf… */ }));

memoryRateLimitStore() is the dev-only / fail-open fallback — per-isolate, not coordinated across Workers instances.

API

| Export | What it does | | --- | --- | | deploy(cf, plan, log?) / deployWith(opts, plan, log?) | the full orchestration over a DeployPlan; returns a DeployResult | | CloudflareClient | typed REST client; request(method, path, opts) unwraps result, resolveAccountId() caches the account | | CloudflareError | thrown on success:false/non-2xx, carrying status + the API's errors[] | | provisionD1 / provisionKvNamespace / provisionR2Bucket | idempotent create-or-get for D1 / KV / R2 | | applyMigrations / queryD1 | ledgered, run-once D1 migrations / raw D1 SQL | | putSecret / putSecrets | encrypted Worker secrets (putSecrets skips empty values, returns the names set) | | uploadAssets / assetHash / extractAssetRuleFiles | the Workers static-assets upload flow | | deployWorker / putCronTriggers | upload a module worker (incl. DO bindings + inline migrations) / set its cron schedule | | DeployPlan.durableObjects / DurableObjectBinding / WorkerMigration | bind + migrate SQLite-backed Durable Object agents (Cloudflare Agents SDK) | | kvRateLimitStore / memoryRateLimitStore | KV-backed (prod) / in-memory (dev) RateLimitStore for @suluk/hono |

Boundary

This package executes — it holds your token and calls Cloudflare. But it is pure over its inputs: deploy() takes a DeployPlan of bytes, not paths (the module source, the AssetFile[], the migration SQL), so the whole library is unit-testable without disk or network. The disk-reading wrapper is the app's seam — walking dist/, reading the worker bundle and migration files lives in your scripts/deploy.ts, not in here (see saasuluk's scripts/deploy.ts). Inject the bytes; inject the fetch.

What stays out of the plan today: routes / custom domains aren't modeled — attach them via CloudflareClient.request (the typed escape hatch). If that pattern recurs, that's the candidate to fold into DeployPlan.

License

Apache-2.0.