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

@vtex/payment-templates-validator

v0.1.0

Published

Shared validation library for Payment Templates

Readme

payment-templates-validator

The shared validation library of the Dynamic Payment Templates project. It exposes exactly one function, which decides whether a submitted template bundle is safe and well-formed:

import { validate } from '@vtex/payment-templates-validator';

const result = await validate({ template, icon, displayName });
// { ok: false, errors: [{ rule: 'htmlSafety', severity: 'error', message: '…', ref: { file: 'index.html', line: 2 } }] }

ok is true iff no finding has severity: 'error'. Validation failures are always reported through errors[]validate() only throws for programmer errors, such as an input carrying none of template / icon / displayName.

Local runs are feedback only

This library is Layer 1 of a defense-in-depth model and it protects only the legitimate write path — a malicious template submitted through the pipeline. It does not protect against direct S3 tampering (that is Layers 2–4: handler wrap, host confinement, IAM/auditing).

The authoritative execution is the server-side one inside payment-templates-handler, which runs validate() before writing a single byte to S3. Running the validator locally from payment-mocker, or in the browser from the Admin UI, certifies nothing: it emits no trust flag, signature or attestation, and the handler always recomputes. A passing local run is a fast feedback loop, not a guarantee of publication.

Install

yarn add @vtex/payment-templates-validator

Requires Node >= 18. The same build runs in evergreen browsers: bytes are typed as Uint8Array (never Buffer), text is decoded with TextDecoder, and the package imports no Node built-in.

Input

Every file is a FileEntry { name, size, buffer: Uint8Array }, where size must equal buffer.byteLength. A ValidationInput carries any combination of the three parts, and at least one must be present:

interface ValidationInput {
  template?: {
    html: FileEntry;                    // index.html
    css: FileEntry;                     // style.css
    i18n: Record<string, FileEntry>;    // at least one locale
    assets?: FileEntry[];               // raster assets referenced by html/css
    defaultLocale: string;
  };
  icon?: FileEntry;
  displayName?: Record<string, string>; // per-locale plain text
}

The size / buffer.byteLength equality is the caller's responsibility: size is what maxFileSize caps, so an understated one would slip past the caps. A mismatch is therefore a programmer error and validate() throws a TypeError for it instead of returning a finding. A consumer that assembles FileEntry from an upload — multipart parts, say — must establish the invariant before calling, by deriving size from buffer.byteLength; otherwise a malformed upload surfaces as a server error rather than as a rejected request.

Which rules run depends on what the input carries — a full publish submits all three, while a metadata edit submits an icon and/or display name alongside the currently stored template:

| Input carries | Rules invoked | |---|---| | template | htmlSafety, noExternalRefs, i18nKeyConsistency, cssClassUsage, assetUsage, localeFormat, imageSafety (assets), and maxFileSize over every submitted file (the icon included) | | icon | imageSafety (icon bytes); maxFileSize when the icon is the whole submission | | displayName | localeFormat (keys), displayNameSafety (values), and displayNameConsistency when a template is also present |

maxFileSize is a whole-submission rule and is evaluated exactly once per call, so its findings never appear twice.

The ten rules

The catalog is frozen: adding, removing or renaming a rule — or changing any decision boundary, including the three lists — is a breaking change and a MAJOR bump.

| Rule | Decides | |---|---| | htmlSafety | Only the 39 allowListed tags may appear — no interactive form control (label, select, option, input, button) is among them; a blockList additionally rejects <script>/<iframe>/<object>/<embed>, every on* attribute and every javascript: URI. <html>/<head>/<body>/<frameset> are rejected too, attributes included — HTML fragment parsing discards them, so they are recovered from a document-mode parse rather than passing unseen | | noExternalRefs | Every URL resolves inside the bundle; fragment-only refs pass, any scheme (data: included) does not. Covers URLs written as CSS strings (@import "…", image-set("…" 1x)) as well as url() | | maxFileSize | Bundle ≤ 1 MB; HTML/CSS ≤ 128 KB, asset ≤ 256 KB, i18n ≤ 64 KB, icon ≤ 50 KB; file-type allowlist | | i18nKeyConsistency | Every data-i18n key exists in every locale, and all locales share one key set | | cssClassUsage | Every class selector in style.css is used by index.html (error); a class used by index.html that no selector defines is a warning, which never blocks publication | | assetUsage | Every asset is referenced by the HTML or CSS; the icon is exempt | | localeFormat | Every locale tag matches ^[a-z]{2}-[A-Z]{2}$ | | imageSafety | Icon and assets are PNG/JPEG/WebP by magic bytes; SVG files are never accepted; the icon fits a 160×160 box with its smallest side ≥ 60 px. Image weight is maxFileSize's cap, not this rule's | | displayNameSafety | Plain text, no control characters or bidi controls (U+202A–U+202E and U+2066–U+2069), ≤ 90 characters | | displayNameConsistency | Display-name locales are shipped by the template, and defaultLocale is among both |

Findings come back in a stable order for the same input, and validate() never mutates the input or its buffers.

Contributing and the release process: see CONTRIBUTING.md.