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

@quickflo/app-sdk

v0.7.2

Published

Framework-agnostic browser SDK for QuickFlo micro-apps: identity-aware onboarding, entitlement checks, and typed webhook + form trigger clients.

Readme

@quickflo/app-sdk

Framework-agnostic browser SDK for QuickFlo micro-apps.

Centralizes the three things every QF-backed app re-implements from scratch:

  • identity-aware onboarding (POST /api/onboarding/apps/:appId/install)
  • per-app entitlement reads + paywall guard helpers
  • typed webhook and form trigger clients (/w/@:orgSuid/:name, /f/@:orgSuid/:name/*)

No Vue, React, Astro, or Quasar coupling. The SDK uses fetch and optional browser storage (sessionStorage / localStorage); falls back to in-memory storage in SSR / Node.

Install

pnpm add @quickflo/app-sdk zod

zod is a peer dependency. @quickflo/entitlement-schema is bundled and re-exported.

Quick start

import { createQuickFloClient } from '@quickflo/app-sdk';
import { supabase } from './supabase';

const qf = createQuickFloClient({
  appId: 'list-scrub',
  // Resolver MUST return null when no session is active.
  getAuthToken: async () => {
    const { data } = await supabase.auth.getSession();
    return data.session?.access_token ?? null;
  },
  onUnauthenticated: () => {
    window.location.href = '/login';
  },
  onPaywall: () => {
    window.location.href = '/register?paywall=1';
  },
});

Return pattern

Every HTTP method on the client returns { data, error, response }. No exceptions, no try/catch:

const { data, error, response } = await qf.ensureOnboarded();
if (error) {
  // error.code   → 'network' | 'http_4xx' | 'http_5xx' | 'parse' | 'auth_missing' | 'aborted' | 'validation' | 'inactive_entitlement'
  // error.status → HTTP status when applicable, else null
  // error.body   → server-provided error payload when available
  // response     → raw Response when the request reached the server
  console.error(error.message);
  return;
}
// data is non-null here
console.log(data.entitlement.tier);

Sync helpers (cache reads, hash parsers) return their values directly : only the methods that hit the network use the result shape.

Onboarding + entitlement

// Idempotent: caches the result in sessionStorage by default.
const { data, error } = await qf.ensureOnboarded();
if (data) console.log(data.orgId, data.orgSuid, data.entitlement);

// Page-guard for paywalled routes. Fires onUnauthenticated / onPaywall as a
// side effect and surfaces the same condition via error.code.
const guard = await qf.requireActiveEntitlement();
if (guard.error?.code === 'inactive_entitlement') {
  // onPaywall has already fired; render the upgrade state here too if needed.
}

// Synchronous reads for inline UI gating.
const cached = qf.getCachedEntitlement();
qf.clearCache();

ensureOnboarded({ force: true }) skips the cache.

Org scoping

Trigger calls target an org by its orgSuid. You don't pass it explicitly: after ensureOnboarded(), the SDK caches the onboarding result and uses its orgSuid as the default for every forms.* / webhooks.* call. Pass { orgSuid } in the options only to override (e.g. credentials-flow forms for users who aren't onboarded QF identities). A call made before ensureOnboarded() with no explicit orgSuid returns a validation error.

Webhook triggers

const { data, error } = await qf.webhooks.run('start-scan', {
  url: 'https://example.com',
});
if (error) return;
console.log(data.status, data.body);

Auth modes for webhooks

Webhook trigger configs declare an authType, but there's no public probe endpoint to discover it from the browser. Callers pass authType per call:

// 'qf-identity' (default): SDK attaches getAuthToken() as a Bearer.
await qf.webhooks.run('private-hook', body);

// 'token': caller passes the static secret. The token is visible to anyone
// who can read your bundle : only use this for non-browser callers or for
// genuinely public webhooks where leakage is acceptable.
await qf.webhooks.run('public-hook', body, {
  authType: 'token',
  token: process.env['PUBLIC_QF_AUTH_HOOK'],
});

// 'none': no Authorization header.
await qf.webhooks.run('open-hook', body, { authType: 'none' });

// Override the target org explicitly:
await qf.webhooks.run('private-hook', body, { orgSuid: 'otherOrg' });

Typed seam (bd-xh7a codegen target)

type ScanIn = { url: string };
type ScanOut = { scanId: string };
const { data } = await qf.webhooks.runTyped<ScanIn, ScanOut>('start-scan', {
  url: 'https://example.com',
});

Form triggers

The full structured form surface (org defaults to the onboarding result):

// 1. Schema + auth metadata
const { data: schema } = await qf.forms.getFormSchema('start-scan');
// → { schema, formMeta, auth: { provider, ... }, hasPrefill, hasConfirmation, ... }

// 2. Prefill (optional, when hasPrefill)
const { data: prefill } = await qf.forms.getFormPrefill('start-scan');
// prefill?.warning carries X-Prefill-Warning when the backend fails open.

// 3. File upload (signed-URL handshake, direct browser → GCS, no proxy)
const { data: uploaded } = await qf.forms.uploadFormFile('start-scan', file);

// 4. Confirmation (optional, when hasConfirmation)
const { data: confirmation } = await qf.forms.getFormConfirmation('start-scan', {
  agents: [1, 2, 3],
});
// confirmation?.warning carries X-Confirmation-Warning when fail-open.

// 5. Submit
const { data: submitted } = await qf.forms.submitForm('start-scan', {
  url: 'https://example.com',
  upload: uploaded,
});
if (submitted && !submitted.success) {
  console.error(submitted.message, submitted.supportReference);
}

Auth modes for forms

Forms expose their auth provider via getFormSchema().auth.provider. The SDK caches that value per (orgSuid, name) and uses it to decide how to authenticate subsequent calls:

  • 'none' : no Authorization header.
  • 'qf-identity' : SDK attaches the current QF identity token from getAuthToken. This is the default for SDK-driven micro-apps and what closes the "second login" friction.
  • 'credentials' : separate session-token flow for external-user forms. The SDK provides four endpoints + a hash-parsing helper. These forms are often for users who aren't onboarded QF identities, so pass { orgSuid } explicitly:
// Password
const { data: session } = await qf.forms.submitFormCredentials(
  'name',
  { username: '[email protected]', password: 'hunter2' },
  { orgSuid: 'abc123' },
);

// Magic link request (always resolves { ok: true } regardless of match)
await qf.forms.requestMagicLink('name', { email: '[email protected]' }, { orgSuid: 'abc123' });

// Magic link redemption : the backend redirects to
//   <formUrl>#auth_token=<jwt>&username=<email>
// Parse that fragment with parseMagicLinkHash, then remember the token:
const parsed = qf.forms.parseMagicLinkHash(window.location.hash);
if (parsed) {
  qf.forms.rememberFormAuthToken('name', parsed.authToken, { orgSuid: 'abc123' });
}

// OTP
await qf.forms.requestOtp('name', { email: '[email protected]' }, { orgSuid: 'abc123' });
const { data: otpSession } = await qf.forms.verifyOtp(
  'name',
  { email: '[email protected]', code: '123456' },
  { orgSuid: 'abc123' },
);

The credentials-flow session token is cached per (orgSuid, name) in the configured storage and attached to every subsequent form-scoped call.

Typed seam

type SubmitIn = { url: string };
type SubmitOut = { success: true; executionId: string };
const { data } = await qf.forms.submitFormTyped<SubmitIn, SubmitOut>('start-scan', {
  url: 'https://example.com',
});

Billing

qf.billing wraps the centralized platform Stripe workflows. Subscription billing with zero per-app billing code — the app calls two methods; the platform org owns the Stripe secret and the customer→org mapping.

// Start a subscription Checkout Session, then redirect.
const { data } = await qf.billing.checkout({ tier: 'pro' });
if (data?.url) window.location.href = data.url;

// Manage / cancel via the Stripe billing portal.
const { data: portal } = await qf.billing.billingPortal();
if (portal?.url) window.location.href = portal.url;

Address the price by tier (+ optional interval, default 'month', resolved via the stripe-sync lookup key <appId>_<tier>_<interval>) or by an explicit priceId (from VITE_QF_PRICE_*):

await qf.billing.checkout({ priceId: 'price_123' });
await qf.billing.checkout({ tier: 'pro', interval: 'year' });
await qf.billing.checkout({ tier: 'pro', metadata: { campaign: 'launch' } });

What the SDK fills in for you:

  • organizationId — the user's own org, auto-filled from cached ensureOnboarded() (override via { organizationId }). The platform workflow validates it against the caller's token, so checkout requires onboarding; with no cached org it returns a validation error rather than firing.
  • Routing — every call targets the platform org (config.platformOrgSuid, default 'platform'), not the user's org. The user's org rides in the body. Override the routed org per call with { orgSuid } (for future user-owned billing entrypoints).
  • Redirect URLssuccessUrl / cancelUrl / returnUrl default to window.location.href.
// Point the client at a non-default platform org (dev/staging):
createQuickFloClient({ appId, getAuthToken, platformOrgSuid: 'platform-staging' });

Subscription status is not a separate billing call — it's the entitlement, already exposed by ensureOnboarded() / requireActiveEntitlement().

Cache + storage

Default cache keys:

  • ${appId}_onboarding_v1 for the onboarding result
  • ${appId}_form_auth_v1:<orgSuid>:<name> for credentials-flow session tokens

Override either via cache.onboardingKey / cache.formAuthKeyPrefix.

Default backend is sessionStorage in a browser, in-memory otherwise. Override via cache.storage:

import { createMemoryStorage } from '@quickflo/app-sdk';

createQuickFloClient({
  // ...
  cache: { storage: 'local' },               // localStorage
  // or
  cache: { storage: createMemoryStorage() }, // explicit memory
  // or
  cache: { storage: myRedisBackedAdapter },  // custom { getItem, setItem, removeItem }
});

Error handling

Every HTTP method's result has the same shape:

interface QuickFloResult<T> {
  data: T | null;
  error: QuickFloError | null;
  response: Response | null;
}

interface QuickFloError {
  message: string;
  code: QuickFloErrorCode;
  status: number | null;
  body?: unknown;
}

type QuickFloErrorCode =
  | 'network'              // fetch threw: connection lost, DNS, CORS, etc.
  | 'http_4xx'             // server returned 4xx
  | 'http_5xx'             // server returned 5xx
  | 'parse'                // response body could not be parsed
  | 'auth_missing'         // getAuthToken returned null when auth was required
  | 'inactive_entitlement' // requireActiveEntitlement: signed in but no active entitlement
  | 'aborted'              // request was aborted via signal
  | 'validation';          // caller passed invalid arguments before any request fired

Form submissions are a special case: submitForm returns the { success: false, message, supportReference, errors? } payload as data even when the backend returned a 4xx. Workflow validation failures shouldn't force consumers to branch on HTTP status : the structured body is the contract. Only network failures and non-structured non-2xx responses come back as error.

unwrap — throw instead of branch

The result pattern is the default, but for call sites that prefer a throw, unwrap(result) returns data or throws a QuickFloThrownError carrying the code / status / body / response:

import { unwrap, QuickFloThrownError } from '@quickflo/app-sdk';

try {
  const { url } = unwrap(await qf.billing.checkout({ tier: 'pro' }));
  window.location.href = url;
} catch (err) {
  if (err instanceof QuickFloThrownError) console.error(err.code, err.status);
}

License

Apache-2.0