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

flowflex

v0.1.1

Published

Official FlowFlex SDK — fire custom-integration events and attach private files without handling presigned URLs yourself.

Readme

flowflex

Official Node/browser SDK for firing FlowFlex custom-integration events and attaching private files to them — without ever dealing with presigned URLs, Basic-auth headers, or the multi-step upload dance yourself.

npm install flowflex

Requires Node 18+ (for the built-in fetch) or any modern browser.


Quick start

import { FlowFlex } from "flowflex";

const ff = new FlowFlex({
  apiKey: "cik_xxx",            // from your custom integration
  apiSecret: "yyy",
  integrationCode: "ic_abc123", // the code in your event URL: /v1/events/<code>
  baseUrl: "https://api.flowflex.ai",
});

// Plain event, no file
await ff.sendEvent("order.placed", {
  payload: { name: "Ada", order_id: "ord_42" },
});

In your flow builder, the values land under trigger — e.g. {{trigger.name}}, {{trigger.order_id}}.


Attaching a private file

Wrap any file in ff.file(...) and drop it into the payload. The SDK uploads it through a presigned URL and replaces it with its opaque assetId before the event is sent. The file bytes go straight to storage — they never pass through the FlowFlex app server.

await ff.sendEvent("invoice.created", {
  payload: {
    assetId: ff.file("./invoice.pdf"),  // ← becomes "asset_..." on the wire
    customer_id: "cust_123",
  },
});

Then in your flow's Media Message node:

  1. Set File sourcePrivate file (assetId)
  2. Set the Asset ID field → {{trigger.assetId}}

The key you use in the payload is the key you reference in the flow. If you send { invoice: ff.file(...) }, reference it as {{trigger.invoice}}.

Multiple files

A flow can have as many media nodes as you like — put a file() anywhere in the payload (top-level, nested, or in arrays) and the SDK uploads them all in parallel and swaps each for its assetId. The shape is entirely up to you; reference each one by its path in the flow builder.

await ff.sendEvent("order.shipped", {
  payload: {
    invoice: ff.file("./invoice.pdf"),                 // {{trigger.invoice}}
    label:   ff.file("./label.png"),                   // {{trigger.label}}
    gallery: [ff.file("./a.jpg"), ff.file("./b.jpg")], // {{trigger.gallery[0]}}, {{trigger.gallery[1]}}
    order:   { receipt: ff.file("./receipt.pdf") },    // {{trigger.order.receipt}}
    note:    "non-file values pass through untouched",
  },
});
// → { invoice: "asset_a", label: "asset_b",
//     gallery: ["asset_c", "asset_d"],
//     order: { receipt: "asset_e" }, note: "..." }

result.uploadedAssets maps each payload path to its assetId, e.g. { "invoice": "asset_a", "gallery[0]": "asset_c", "order.receipt": "asset_e" }.

Reusing one file across nodes: if you pass the same file() instance in multiple places, it's uploaded only once and the same assetId is used everywhere:

const banner = ff.file("./banner.png");
await ff.sendEvent("promo.sent", {
  payload: { header: banner, footer: banner }, // one upload, same assetId in both
});

Supplying file bytes other ways

file() accepts a path (Node), a Buffer/Uint8Array, or a Blob/File (browser). When the type can't be inferred, pass filename and mime:

// Buffer / Uint8Array
ff.file(pdfBuffer, { filename: "invoice.pdf", mime: "application/pdf" });

// Browser File input
const f = document.querySelector("input[type=file]").files[0];
ff.file(f); // name + type read from the File

// Override the stored filename
ff.file("./tmp-7f3a.pdf", { filename: "Invoice-2026.pdf" });

Allowed types: PDF, DOC(X), XLS(X), PPT(X), TXT, JPEG, PNG, MP4, 3GPP, AAC, AMR, MP3, OGG. Max size: 25 MB.


API

new FlowFlex(options)

| Option | Type | Required | Notes | | ------------------------- | -------- | -------- | --------------------------------------------------------------------- | | apiKey | string | yes | Custom-integration key (cik_…). | | apiSecret | string | yes | Custom-integration secret. | | integrationCode | string | yes | The <code> in /v1/events/<code>. | | baseUrl | string | yes | FlowFlex host. Must be https (except localhost). Trailing /api stripped. | | timeoutMs | number | no | Per-request timeout. Default 30000. | | maxFileBytes | number | no | Client-side size cap. Default 26214400 (25 MB). | | fetch | function | no | Custom fetch for runtimes without a global one. |

Server-only. There is no option to run this in a browser — it throws if constructed in one, and browser bundlers resolve it to a stub that throws on import. See Security.

ff.sendEvent(event, { payload?, idempotencyKey? })

Uploads any file() in payload, then POSTs the event. Returns:

{
  response: unknown,                       // raw body from the events endpoint
  uploadedAssets: Record<string, string>,  // payload path → assetId
}

An idempotencyKey is auto-generated (UUID) if you don't pass one — safe to retry the same call.

ff.file(source, { filename?, mime?, size? })

Returns a lazy FileRef. Bytes are read only when the event is sent.

Lower-level helpers

const { assetId, uploadUrl } = await ff.createUploadUrl({ filename, mime });
const assetId = await ff.uploadFile(ff.file("./x.pdf")); // upload, get assetId

Errors

All errors extend FlowFlexError ({ message, status?, code?, details? }):

  • FlowFlexConfigError — bad/missing constructor options.
  • FlowFlexUploadError — a file couldn't be read or storage rejected it.
  • FlowFlexError — API or network failure (code is the backend error code, e.g. MIME_NOT_ALLOWED, ASSET_FILE_MISSING).
import { FlowFlexError } from "flowflex";

try {
  await ff.sendEvent("invoice.created", { payload: { assetId: ff.file("./big.pdf") } });
} catch (err) {
  if (err instanceof FlowFlexError) {
    console.error(err.code, err.status, err.message);
  }
}

File lifetime & storage cleanup

Important — read before using file attachments in production.

When you call ff.file(...), the file is uploaded to private Supabase storage that only the FlowFlex backend can read. It is not a public URL and the caller cannot access it after upload.

How long does the file stay?

| Phase | Duration | | ----- | -------- | | Presigned upload URL valid | 2 hours from createUploadUrl | | File kept in storage | 48 hours from upload | | After 48 hours | File deleted from storage + record removed |

After the flow delivers the WhatsApp message the file is no longer needed. The backend runs an automatic cleanup job every hour that:

  1. Finds assets whose 48-hour window has expired
  2. Deletes the file from Supabase storage first
  3. Then removes the database record

This means storage never accumulates — every file is cleaned up within ~1 hour of its expiry.

What this means for you

  • Do not store assetId long-term expecting to reuse it. It expires in 48h.
  • Each event send should get a fresh assetId by calling ff.sendEvent with a new ff.file(...). The SDK handles the upload automatically.
  • If you fire the event more than 48h after uploading, the file will be gone and the flow will fail with ASSET_FILE_MISSING. Keep your event send close to the upload.
  • Re-sending the same message to a different recipient after 48h requires a fresh upload — call sendEvent again with the file, don't reuse the old assetId.

Typical correct pattern

// ✅ Upload + fire in the same operation — always fresh
await ff.sendEvent("invoice.created", {
  payload: { assetId: ff.file("./invoice.pdf") },
});

// ❌ Don't store assetId and reuse it hours later
const { uploadedAssets } = await ff.sendEvent(...);
// ... 50 hours later ...
// uploadedAssets["assetId"] is now expired and deleted

Security

This SDK is server-only. Your apiKey/apiSecret are integration-wide credentials. Bundling them into front-end code exposes them to anyone who opens devtools — they could then fire events and upload files as you. The browser is hard-blocked, with no opt-out:

  • Runtime — the constructor always throws if it detects a browser.
  • Bundle time — browser bundlers (webpack / Vite / Next) resolve the package to a stub (via the "browser" export condition) that throws on import, so the SDK can't be built into client code in the first place.

Other protections built in:

  • HTTPS enforced. baseUrl must be https:// (only localhost may use http), so Basic-auth credentials are never sent in cleartext.
  • No credential leakage. The Authorization header is never included in error messages, logs, or FlowFlexError.details.
  • Header-injection safe. The event name and idempotencyKey are rejected if they contain control characters (CRLF).
  • Path-injection safe. integrationCode is URL-encoded into the request path.
  • Prototype-pollution safe. Payload keys like __proto__ can't pollute Object.prototype during the file-swap walk.
  • Per-request timeouts (default 30 s) on every network call.
  • Client-side size cap (default 25 MB) so oversized files fail before upload.

If you must call FlowFlex from a browser, proxy through your own backend: the browser talks to your server, your server holds the secret and calls this SDK.