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

@relintio/svelte-agent

v1.0.0

Published

Official Svelte and SvelteKit WAF protection agent for Relintio.

Readme


One function, createRelintio, returns a client made of two Svelte-shaped things: a readable store carrying challenge state, and an action you put on the challenge <iframe>. It wraps fetch, so a 403 from your own API carrying an X-Relintio-Challenge header becomes a challenge the visitor solves and a request that is replayed once, instead of an error your UI has to explain. Under SvelteKit it goes in the root layout, src/routes/+layout.svelte, and it is inert during server rendering.

<!-- src/routes/+layout.svelte -->
<script lang="ts">
  import { onDestroy } from 'svelte';
  import { createRelintio } from '@relintio/svelte-agent';

  const relintio = createRelintio({ publishableKey: 'pk_live_...' });
  const { state: challenge, challengeFrame, frameAttrs } = relintio;

  onDestroy(() => relintio.destroy());
</script>

{#if $challenge.isChallenging}
  <iframe
    use:challengeFrame
    src={$challenge.challengeUrl}
    {...frameAttrs}
    class="relintio-challenge"
  />
{/if}

<slot />

Installation

npm install @relintio/svelte-agent

Svelte 4 or 5, as a peer dependency. The package imports nothing from svelte at runtime — not even writable. The store contract it returns is a four-line hand-declared Readable<T> interface, which is why one build works across both major versions and why this package cannot pin your Svelte version. @relintio/browser-core is the only runtime dependency.

Registration

Call createRelintio once, from src/routes/+layout.svelte. That component wraps every route and survives client-side navigation, which is what the agent needs: fetch is wrapped for the lifetime of the client, and the challenge iframe has to be able to render over whatever page is currently mounted.

A page component is the wrong place. +page.svelte is destroyed and recreated on every navigation, so each one wraps fetch again over the previous wrapper, and a challenge open at the moment of navigation is torn down with the request it was holding.

A load function is also the wrong place. +layout.ts runs on the server as well as the client, and the client this returns holds timers, listeners and a class instance — it cannot cross the SSR serialisation boundary. Create it in the component.

Pair it with onDestroy(() => relintio.destroy()). Nothing else calls it: destroy() restores the original fetch and disposes the agent, which rejects any open challenge with Relintio agent unmounted.

Server rendering

createRelintio is safe to run during SSR because it does almost nothing there. It checks typeof window, and on the server it skips fetch interception entirely — wrapping fetch in a SvelteKit server render would wrap the framework's own request plumbing — skips verifyOnMount, and gives the store a subscribe that emits the initial state once and returns a no-op unsubscribe. $challenge.isChallenging is therefore false in the server-rendered HTML, and the client comes alive at hydration.

Configuration

RelintioOptions is RelintioConfig from the core plus interceptFetch.

| Option | Default | Meaning | | --- | --- | --- | | publishableKey | — | Required. Must begin pk_. Checked in the browser only; see below. | | apiUrl | https://api.relintio.com/v1 | Override for staging or self-hosting. Trailing slashes are trimmed once. | | challengeTimeoutMs | 120000 | How long a challenge stays open before it fails. Floored at 10000, whatever you pass. | | verifyOnMount | false | Request a verdict as soon as the client is created in a browser. | | fallbackUrl | — | Declared on RelintioConfig for a visitor whose challenge cannot be presented. No code path in the current core reads it. | | interceptFetch | true | Wrap window.fetch. Pass exactly false to opt out. |

Which key goes here

A publishable key (pk_live_…) and nothing else. It is public by design — it ships in your client bundle and every visitor can read it — and it can do exactly one thing: ask Relintio for a verdict on a request. It cannot read your rules, write telemetry, or cause a challenge pass to be issued.

Your licence key (UP_LIVE_…) is a different credential with a different job. It is the HMAC key that signs challenge passports and outbound agent requests, so anyone holding it can walk through your WAF unchallenged. It belongs in $env/static/private or your process environment, used by a server-side agent — never in $env/static/public, never in a +layout.svelte, never anywhere Vite can inline it into the browser bundle.

Given a licence key, the agent refuses to start and stays refused. The key check requires a string beginning pk_; UP_LIVE_… fails it, so the agent logs an error saying a licence key must never appear in browser code and reports itself unusable. In this binding that means interceptFetch is never installed, verifyOnMount never fires, verify() resolves to null and challenge() resolves immediately — and the key is never put on the wire. The core's tests assert exactly that, with a fetch stub that fails if it is called at all. A blank key, sk_live_…, or a bare pk are refused the same way.

The store and the action

state is a store in the ordinary Svelte sense: subscribe with $, and it carries isChallenging, challengeUrl, resolvedCount and the last verdict. Subscribing emits the current value immediately, so there is no undefined first frame.

challengeFrame is an action, not a component. Relintio does not inject a fixed-position element into your layout — that is a dependency putting markup into your design system. You render the iframe, style it, and place it; use:challengeFrame supplies the behaviour.

frameAttrs is what the iframe must carry: sandbox="allow-forms allow-scripts allow-same-origin", referrerpolicy="no-referrer", title="Security check". There is no allow-top-navigation, so a challenge page cannot move the visitor off the site it is protecting.

The action adds a message listener and removes it on destroy. A message counts as a pass only if all three of these hold: its origin matches the origin of the challenge URL currently on screen, its source is that iframe's own contentWindow, and its data is the exact string relintio_challenge_success. Origin alone would let any frame served from the challenge origin resolve a challenge on the visitor's behalf, and a startsWith or a JSON parse would hand the decision to a string the visitor can influence.

What happens on a request

The wrapped fetch is a pass-through for everything except a 403 carrying X-Relintio-Challenge. When one arrives, the URL from that header goes into the store, your layout renders the iframe, and the awaiting request is held. A pass replays the original request exactly once — one retry, never a loop, so a second challenge is returned to your code rather than looped on. A timeout or a teardown hands back the original 403 response object untouched, so response.status and the body are what your server actually sent.

verify() is the other call and it is advisory. It POSTs to ${apiUrl}/agent/decision with an X-Agent-Version header and a body carrying the domain, path, referrer, return URL, any up_token from the query string, agent_kind: "svelte", and telemetry. The body field is named license_key for wire compatibility; the value is your publishable key. It is abandoned after 5 seconds. A verdict of challenge with a challenge_url presents a challenge; the other actions land in the store as information. Nothing here enforces a verdict, because a browser that could enforce one could be told to enforce a different one. Enforcement belongs to the agent at your origin.

Telemetry comes from the collector shared with the challenge page — screen and hardware shape, timezone, languages, resolved fonts, canvas, WebGL and audio digests, network hints, the navigator.webdriver flag, and behaviour counters. The counters are counts and a dwell time, never content: how many pointer moves, keystrokes, scrolls and touches happened, not what was typed or where. Each probe is guarded individually and the audio probe is raced against a 120 ms budget, so a hung audio stack costs one signal rather than a rendered page.

One engine, five bindings

There is no security decision in this package. The key refusal, the http/https restriction on a challenge URL, the three-part postMessage check, the ten-second timeout floor and every fail-open path live in @relintio/browser-core. What you install here is the framework-shaped surface over that protocol: subscribe() presented as a store, a listener presented as an action, teardown presented as something onDestroy can call.

The alternative was five hand-written implementations of one protocol for React, Vue, Svelte, Angular and Expo, differing in exactly the places where they must not. Keeping the engine in one package means a fix to challenge handling — a tightened origin check, a corrected timeout — ships once and reaches every framework at the same time, rather than being true on the day each SDK was written.

When Relintio is unreachable

Nothing stops. verify() returns null on a refused connection, a timeout, a non-2xx status or an unreadable body, with no throw for your code to catch. The interceptor returns your server's own response. A challenge that cannot be presented or solved releases what it was holding. Every failure path in this agent fails open on purpose: a security agent that blanks a page because it could not reach its control plane has converted our outage into yours, which is the worse of the two failures.

Edge cases

On the server, the key is never checked. Usability is evaluated as "in a browser and the key is valid", and the short-circuit means a licence key produces no message during SSR. The error appears in the visitor's browser console at hydration — not in your build output, not in your server logs, not in a CI check. If you want that caught earlier, assert the pk_ prefix in your own config loading.

A refused key gives you a client that quietly does nothing. No exception is thrown and every method still exists: $state emits its initial value once and never changes, verify() resolves null, challenge() resolves immediately, and fetch is left alone. A store that never moves and a console error are the only symptoms, so check the console before you go looking at the store.

Do not call the store state in a Svelte 5 component. $state is a rune in Svelte 5, and in a runes-mode component the compiler reads $state as the rune rather than as a store auto-subscription. Rename it on destructure, as the example above does — any name but state works, and the store is the same object either way.

The action must be on the iframe that shows challengeUrl. The pass check compares the message's source against node.contentWindow for the node the action was attached to. Put use:challengeFrame on a wrapper <div>, or on a second iframe, and every success message is rejected: the challenge stays on screen until challengeTimeoutMs runs out — two minutes by default — and the held request then resolves with the original 403, silently.

destroy() is yours to call and nothing calls it for you. Without the onDestroy line, an unmounted layout leaves fetch wrapped by an agent no store is watching. In a single-layout SvelteKit app that layout lives as long as the tab, so the practical cost is nil; in tests, in an embedded widget, or anywhere the root is mounted repeatedly, the wrappers stack.

Simultaneous failures share one challenge. Several requests refused at once join a single pending challenge rather than stacking iframes. The first URL is the one presented; later ones are discarded, and all the held requests release together.

A repeated verify() sends stale behaviour counters. The behaviour watcher starts when the client is created and detaches its listeners the first time it is read. A second verdict request carries the counts frozen at the first read, alongside a dwell time that keeps rising. It is accurate for the mount-time call and increasingly stale for anything after it.

Readable<T> here is not svelte/store's type. It declares only subscribe, which is all the auto-subscription contract needs. It satisfies $-prefixed usage in a component, but it is not a full Svelte store: there is no set, no update, and passing it to a helper whose signature demands svelte/store's Readable will not type-check.

Links

Security reports go to [email protected], not to a public issue.

License

MIT. See LICENSE.