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

@blotout/qinto-sdk-app

v0.1.12

Published

Qinto app SDK: the authoring surfaces for Qinto platform apps — defineApp (server) and defineBrowserApp (browser) plus their hook, context, and action types. Zero runtime dependencies.

Readme

@blotout/qinto-sdk-app

The authoring surface for Qinto platform apps: defineApp for the server, defineBrowserApp for the page, createAppUi for the settings panel, and the types each one hands your handlers. Zero runtime dependencies.

Why we built it

A Qinto app is code that runs on the platform, inside the site's own edge. A site sends each visitor action once, and every app installed on it receives that action server-side: a transform app rewrites, enriches, or drops the event before destinations see it, and a destination app forwards it onward. The site's own tracking code never changes when a site adds your app.

That is a very different place to write code than either end you are used to:

  • It is not a server you host. There is no origin to run, scale, or pay for. You declare what your app needs in qinto.app.jsonc — a KV namespace, a D1 database, a workflow, an HTTP endpoint, a cron schedule — and the platform builds the Worker, provisions the bindings, and dispatches into your handlers.
  • It is not a script in the customer's page. The event reaches you one hop from where it was recorded, with the visitor already resolved to an identity, the site's consent already applied, and the site's settings already filled in. You never ask a customer to paste anything.
  • Everything your app can do is declared. Capabilities, captured attribution names, published topics, and settings a panel may write are what a site manager reads and accepts before installing — and again whenever a later version widens them.

This package is the contract between your handlers and that runtime. It exists as a package, rather than as a shape you guess at, for three reasons:

  • The types are the documentation. What a transform hook may return, what rides on ctx in a scheduled run versus a visitor action, which context is a value and which is a call — all of it is on hover.
  • It ships app.schema.json. Point your manifest's $schema at it and your editor completes and validates qinto.app.jsonc offline, against the exact SDK version you installed.
  • The definitions are checked before you publish. defineApp records which hooks you actually declared, and the CLI compares that against the manifest at publish and at dev — so a hook the manifest promises and the bundle does not export fails on your machine, not at dispatch on a customer's site.

Install

npm i @blotout/qinto-sdk-app
npm i -g @blotout/qinto-cli

Usually you do not: qinto app create my-app scaffolds a manifest, a server entry, and this dependency for you, and offers to register the app under your organization.

What you import

| Entry | Exports | Runs | | --- | --- | --- | | @blotout/qinto-sdk-app/server | defineApp, defineWorkflow, runAi, runAiBytes | In your app's Worker, on the platform. | | @blotout/qinto-sdk-app/browser | defineBrowserApp | In the visitor's page, inside the site's tag. | | @blotout/qinto-sdk-app/ui | createAppUi | In your settings panel, framed by the Qinto platform. |

The package also ships app.schema.json for your manifest's $schema.

The server app

import { defineApp } from '@blotout/qinto-sdk-app/server'

type Env = { PURCHASES: D1Database }

export default defineApp<Env>({
  transform: {
    track: async (action, ctx) => {
      if (action.payload.event === 'Blocked Event') {
        return ctx.drop()
      }

      return ctx.replace({ ...action.payload, properties: { ...action.payload.properties, siteTag: ctx.variables.SITE_TAG } })
    }
  },
  onEvent: {
    track: async (action, ctx) => {
      const response = await fetch(String(ctx.variables.ENDPOINT_URL), {
        method: 'POST',
        headers: { 'content-type': 'application/json', authorization: `Bearer ${ctx.secrets.API_KEY}` },
        body: JSON.stringify({ event: action.payload.event, properties: action.payload.properties, userId: action.userId })
      })
      if (!response.ok) {
        ctx.observability.error('delivery refused', undefined, { status: response.status })

        return
      }

      ctx.observability.log('track delivered', { event: action.payload.event })
    }
  }
})

defineApp takes one object. Every key below is optional, and an unknown key is refused rather than ignored.

Every visitor action includes receivedAt; action.context additionally carries occurredAt, source/page/network data, and a browser session as { id, isNew, startedAt } when available. startedAt is fixed when Qinto creates the session id, so destinations can make stable session-cutoff decisions.

| Key | What it is for | | --- | --- | | transform | track and identify hooks that run before destinations, and return ctx.pass(), ctx.drop(), or ctx.replace(payload). | | onEvent | track, identify, and consent hooks that run after the transform chain — where a destination does its work. | | onConnect | Runs once when a visitor's page connects, before any action. | | onLifecycle | Advisory notices that the install changed: installed, configurationChanged, enabled, disabled, versionChanged, uninstalled. They observe; they never approve. | | schedules | Cron handlers, keyed by the ids the manifest declares. | | endpoints | HTTP handlers served at https://<site>/app/<slug><path>. Webhooks have a 10-second total deadline; API handlers must return headers within 10 seconds and may then stream for up to five minutes, with a 150-second idle deadline. | | rpc | Functions other apps on the same site may call. | | ui | The server side of your settings panel, reached at <siteOrigin>/apps/<slug>/ui<path>. | | workflows | Durable runs built with defineWorkflow, keyed by the class name the manifest declares. | | migrations | Resumable backfill handlers named by a D1 migration. |

What rides on ctx

| | | | --- | --- | | ctx.env | The bindings your manifest's resources declared — typed by the Env you pass to defineApp<Env>. A pipeline binding's send is retried for you on a transient Cloudflare failure; the manifest reference says what that means for a sink. | | ctx.variables, ctx.secrets | This install's settings, filled in by the site manager. | | ctx.appVariables, ctx.appSecrets | Your own settings, one value for every site that installs the app. | | ctx.install, ctx.site | Which install this invocation is for, and which site it is on. | | ctx.geo | The current request's complete Cloudflare cf object when the app declares the geo data capability. On an endpoint this describes the endpoint caller, not a stored visitor. | | ctx.user | The visitor: anonId, globalId, stored properties, and geo when granted. | | ctx.consent | { enabled, categories, apps }, when consentState is granted. Your manifest's category is already enforced before you are called. | | ctx.emit | Emit a track of your own back into the pipeline. Your app never receives its own emission. | | ctx.push | Send a message down the visitor's live connection, to your browser code. | | ctx.attribution | Read the click ids and cookies your manifest captures for this visitor, and write derived ones. | | ctx.facts | Read stable, paginated retained visitor facts with list(filters) and opaque continuation cursors; requires the facts data capability. | | ctx.context | Per-visitor state your app keeps: a private level only you read, and a shared level every app on the site can. | | ctx.apps | Call another installed app's exposed rpc functions through the site's broker. | | ctx.startWorkflow | Start one of your declared workflows, carrying this install — and the visitor — into the run. | | ctx.observability | log, error, and span. Everything you record shows up in the site's real-time console, next to the platform's own spans; a plain fetch is traced for you. |

A scheduled run, MCP tool, endpoint, and workflow step carry a narrower context — they have no visitor, so there is no user, consent, or emit there, and push names its subject instead of inheriting one. An endpoint still carries the current caller's cf object in ctx.geo when the app declares the geo data capability. Scheduled runs and MCP tools can query a catalog-enabled R2 resource with ctx.query.catalog(resourceIdentifier, sql) and an Analytics Engine dataset with ctx.query.analytics(resourceIdentifier, sql) — name the dataset by its resource identifier in FROM; Qinto keeps the physical bucket, the dataset name, and the credential out of the app worker. The types say which capability is available where.

Durable work

import { defineApp, defineWorkflow } from '@blotout/qinto-sdk-app/server'

export default defineApp<Env>({
  endpoints: {
    '/webhook': async (request, ctx) => {
      await ctx.startWorkflow(ctx.env.ORDER_SYNC, { orderId: '123' }, { id: 'order-123' })

      return new Response('queued', { status: 202 })
    }
  },
  workflows: {
    OrderSync: defineWorkflow<Env, { orderId: string }>(async (event, step, ctx) => {
      const order = await step.do('fetch', async () => {
        return fetchOrder(event.payload.orderId, ctx.secrets.API_KEY)
      })

      await step.sleep('settle', '1 hour')
      await step.do('notify', async () => {
        await ctx.push({ topic: 'order:synced', data: order })
      })
    })
  }
})

Cloudflare owns the retries, the sleeps, and the per-step persistence, so a destination does not hand-roll its own retry loop. Passing options.id names the instance, which is how a retried vendor webhook avoids starting a second run.

Workers AI

Qinto hands out no AI binding: a binding would spend the hosting account's inference allowance with no way to attribute or cap it. runAi calls Cloudflare's /ai/run endpoint with a token you supply, so the run bills your account.

import { runAi } from '@blotout/qinto-sdk-app/server'

const { response } = await runAi<{ response: string }>({
  accountId: ctx.appVariables.CF_ACCOUNT_ID,
  apiToken: ctx.appSecrets.CF_AI_TOKEN,
  model: '@cf/meta/llama-3.1-8b-instruct',
  input: { prompt: 'Summarise this basket' }
})

The token needs Account → Workers AI → Read; one holding only an AI Gateway permission is refused, and runAi says so rather than passing on Cloudflare's bare Authentication error. It also unwraps the response envelope and raises the refusals Cloudflare returns under HTTP 200.

Some models answer bytes rather than an envelope — @cf/myshell-ai/melotts documents both application/json and a raw audio/mpeg body, and picks per run. runAiBytes reads those, returning the content type alongside the bytes:

const { contentType, bytes } = await runAiBytes({
  accountId: ctx.appVariables.CF_ACCOUNT_ID,
  apiToken: ctx.appSecrets.CF_AI_TOKEN,
  model: '@cf/myshell-ai/melotts',
  input: { prompt: 'Your order has shipped', lang: 'en' }
})

The two read the same run and disagree only about the body, so each says which one the answer wanted: runAi raises ai_invalid_response naming the content type it got, and runAiBytes raises ai_json_response when the run answered an envelope after all. Refusals map to the same errors from either.

Both read one buffered answer, so both refuse input.stream: true rather than sending a run whose server-sent events neither can parse — the refusal comes before the request, so a stream you meant to consume is never billed. Call /ai/run yourself when you want to stream.

Cloudflare refuses a @cf/ model that names no gateway, so runAi sends default for one — the single gateway id Cloudflare creates on first use, with logging on, caching off, and Standard billing. Pass gatewayId to route through one of your own instead, which is where spend limits and per-model settings live; any id other than default must exist before you use it. A third-party model (openai/…, anthropic/…) needs no gateway and gets no header.

Declare both values in the manifest's appVariables so they are yours and set once, not asked of every site that installs the app — the token with qinto app secret set, the account id with qinto app var set.

For chat completions, point the OpenAI SDK at the account's /ai/v1/chat/completions instead; that path is OpenAI-compatible and needs nothing from us. The gateway rule still applies there, so a @cf/ model needs the header set on the client:

const client = new OpenAI({
  apiKey: ctx.appSecrets.CF_AI_TOKEN,
  baseURL: `https://api.cloudflare.com/client/v4/accounts/${ctx.appVariables.CF_ACCOUNT_ID}/ai/v1`,
  defaultHeaders: { 'cf-aig-gateway-id': 'default' }
})

The browser app

An app may also ship code that runs in the page, inside the site's tag. The factory runs once per active install, and only for a visitor whose consent covers your app.

import { defineBrowserApp } from '@blotout/qinto-sdk-app/browser'

export default defineBrowserApp((app) => {
  app.onEvent((event) => {
    return { observedAt: Date.now(), siteTag: app.variables.SITE_TAG }
  })

  app.onPush((message) => {
    console.info('server push', message.from, message.topic, message.data)
  })
})

| | | | --- | --- | | app.variables, app.install | This install's browser-exposed plain settings, and which install it is. Secrets never reach the page. | | app.getSession | The current anonId and linked globalId. Apps declaring the geo data capability also receive the server-observed ip and geo: { country, region, state, isEU }; otherwise both location fields are null. | | app.onEvent | Every action on the page. What you return reaches your own server hook for that same action as action.browser, when the manifest allows it — treat it as untrusted page input. | | app.onConsent | The visitor's consent, on connect and on every change. | | app.onPush | Messages your server code sent with ctx.push. A page that was closed missed them; pushes are never replayed. | | app.onSharedContext | What every contributing app knows about this visitor, where both sides declared the capability. | | app.track | Send an event of your own. |

The settings panel

A panel is a static page the Qinto platform frames when a site manager configures your app. createAppUi is the bridge to the page around it.

import { createAppUi } from '@blotout/qinto-sdk-app/ui'

const ui = createAppUi()
const context = await ui.getContext()

const response = await ui.fetch('/analytics', {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ installId: context.appInstallId })
})

const events = await ui.query.catalog(
  'lake',
  'SELECT event_name, event_timestamp FROM lake.events ORDER BY event_timestamp DESC LIMIT 100'
)

const usage = await ui.query.analytics(
  'events',
  'SELECT blob1 AS name, SUM(_sample_interval) AS count FROM events WHERE timestamp > NOW() - INTERVAL \'1\' DAY GROUP BY name'
)

| | | | --- | --- | | getContext() | The team, the site, your app and version, the install the panel was opened for, every live install with its plain variables, and the theme. | | getToken() | The current panel token, renewed before it expires. | | fetch(path, init?) | Calls your own ui server handler with that token attached, and returns the raw Response. Parse it, and decide yourself what a 429 or a 5xx means. | | query.catalog(resourceIdentifier, sql) | Runs R2 SQL against a catalog-enabled R2 resource in the version serving this panel. The same method is available on scheduled-run and MCP-tool contexts. Qinto keeps the physical bucket and host credential server-side and returns { rows, meta }. | | query.analytics(resourceIdentifier, sql) | Runs one single-table SELECT against an analytics_engine resource in the version serving this panel; name the dataset by its resource identifier in FROM. Also on scheduled-run and MCP-tool contexts. Qinto substitutes the real dataset name server-side and returns { rows }; a never-written dataset returns none. |

The platform gives the panel the whole area under the site's header and nothing else — size your page to it and let it scroll inside, the way any page in a window does.

A link to another site opens a new tab: give it target="_blank", or call window.open. The tab is a normal one, outside the sandbox. The frame itself stays on your panel's own origin — the platform refuses any other origin inside it, and it never leaves the page on the panel's behalf except through connect.

Your panel runs in the viewer's browser, so it never receives a secret. The install id it posts is input, not authority — resolve it against ctx.installs in the server handler, which the platform built from the site's own registry.

The manifest

qinto.app.jsonc is the app: it names it, says where it plugs into the pipeline, and declares everything it may reach. Point $schema at the file this package ships and your editor does the rest:

{
  "$schema": "./node_modules/@blotout/qinto-sdk-app/app.schema.json",
  "slug": "my-app",
  "name": "My App",
  "flows": ["destination"],
  "consent": { "category": "analytics" },
  "legal": { "termsUrl": "https://example.com/terms", "privacyUrl": "https://example.com/privacy" },
  "server": {
    "entry": "src/server.ts",
    "hooks": { "transform": [], "onEvent": ["track"], "onConnect": false }
  }
}

The manifest is authoritative: server.hooks must match the hooks your entry exports, every declared schedule, workflow class, and backfill handler must exist, and a ui block needs a ui handler. The CLI checks all of it before anything is uploaded.

The loop

qinto app create my-app      # scaffold, install, register
qinto app dev                # run it against a local Qinto — real platform bundles, no account, no login
qinto app publish            # build, upload, and publish an immutable version
qinto app publish --site example.com
qinto app status my-app --site example.com

qinto app dev boots the released site worker and app runtime in workerd, with a console, a demo site, and a live trace of every request — so the transform chain, the consent gate, and the dispatch into your hooks are the real ones.

Documentation

The manifest reference, the app limits, the standard events, and the CLI are at docs.qinto.io.