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-core

v1.0.0

Published

Shared validation and wrap-runtime library for Payment Templates

Readme

payment-templates-core

The shared validation and wrap-runtime library of the Dynamic Payment Templates project. It exposes validate(), which decides whether a submitted template bundle is safe and well-formed, and wrapTemplate(), which turns that bundle into the HTML document the sandboxed iframe loads:

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

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.

A local run is advisory: it emits no trust flag, signature, or attestation, and the authoritative validation runs server-side.

Install

yarn add @vtex/payment-templates-core

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 is a MAJOR bump, because the catalog is the RuleName enum and that enum is a frozen contract (constitution Principle IV). Changing any decision boundary — including the rules' allowLists and blockLists — is a MINOR bump at any version, because what moves is one rule's threshold and not the contract. Both need an RFC amendment first; a change to a finding's message is a PATCH, as is dropping a finding that can only accompany another error and cannot change whether a bundle passes.

| 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>/<frame> are rejected too, attributes included — HTML fragment parsing discards them, so they are recovered from a document-mode parse rather than passing unseen. Tags must also balance: every non-void element needs its own explicit end tag, and a stray end tag, an element left open at end of input (including mid-file, the moment a breakout tag exits an <svg> an authored </svg> never got to — specs/003-html-tag-balance/spec.md, rule 3), an end tag that closes across an open element, or a run of the file a parse5 recovery swallowed rather than tokenized (seventeen codes, eleven partner-facing messages — an unterminated comment among them) is an error — one finding per offending token or occurrence, balance findings in source order among themselves, with every swallowed-markup finding reported after all of them regardless of its own position in the file. /> closes an element only in foreign content, so <svg><path/></svg> passes while <div/> leaves the div open. An end tag HTML makes optional (</p>, </li>, …) is required here, and the finding says so | | 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. Locale files are nested JSON objects: an empty or dotted key name is an error, as is a value that is not nested content or text. A data-i18n reference with an empty segment, or one that is a segment-wise ancestor of another, is an error | | 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.

Wrap runtime

wrapTemplate is exported from the package root next to validate(). It is not a second validator: it does not decide whether a bundle is safe to publish. resolveLocale stays on @vtex/payment-templates-core/wrap because it is a require() of the verbatim CJS file. Full argument: docs/rfcs/rfc-wrap-runtime.md.

import { wrapTemplate, DIAGNOSTIC_CODES, buildThemeTokenStyle } from '@vtex/payment-templates-core';
import { resolveLocale } from '@vtex/payment-templates-core/wrap';

const html = wrapTemplate(bundle, 'en-US', 'http://localhost:8080', themeTokens);

The two iframe scripts are published verbatim (no transpile). Resolve them with require.resolve and serve the bytes as-is:

require.resolve('@vtex/payment-templates-core/wrap/template-runtime.js')
require.resolve('@vtex/payment-templates-core/wrap/resolve-locale.js')

wrapTemplate emits <script src="/lib/resolve-locale.js"> then <script src="/lib/template-runtime.js">. A consumer that uses those URLs (payment-mocker) must serve the packaged files at /lib/. A consumer that assembles the document itself (checkout-ui) keeps its assembler and copies the same two files from this package into /lib/payment-template/ at build time.

Pin the dependency without a caret, matching payment-mocker today:

"@vtex/payment-templates-core": "1.0.0"

Follow-up: payment-mocker

Separate PR, after this package is published:

  1. Depend on the released version (no ^).
  2. Replace lib/wrap-template.js, lib/theme-tokens.js, lib/locale-tag.js, lib/origin-pattern.js with imports from @vtex/payment-templates-core (wrapTemplate from the root; helpers may come from the root or ./wrap).
  3. Point lib/preview-middleware.js routes that currently streamFile(path.join(__dirname, 'resolve-locale.js')) (and the runtime equivalent) at require.resolve('@vtex/payment-templates-core/wrap/…').
  4. Update invalidateWrapCache() so it no longer deletes those local modules.
  5. Retarget the byte-for-byte assertions in test/preview-middleware.test.js (~lines 704 and 728) at the packaged files.
  6. Import DIAGNOSTIC_CODES in src/assets/libs/template-host.js instead of keeping a private array. The host file itself stays in the mocker.
  7. Run the mocker suite and a manual grunt preview smoke test.

Do not change wrap behavior in that PR.

Follow-up: vcs.checkout-ui

Separate PR, after payment-mocker, and not in the same change as this extraction. Checkout-ui can consume this package at build time: Grunt (or the equivalent copy task) takes the two verbatim files from node_modules/@vtex/payment-templates-core/ and writes them to /lib/payment-template/. The iframe cannot import from npm at runtime — CSP default-src 'none' — so "consume the lib" means that copy, not a runtime import.

Checkout-ui currently vendors:

| Packaged file | Vendored copy today | | --- | --- | | wrap/template-runtime.js | src/lib/payment-template/template-runtime.js | | wrap/resolve-locale.js | src/lib/payment-template/resolve-locale.js | | buildThemeTokenStyle / token patterns | ES5 twin inside src/script/payment/payment-template-document.js | | DIAGNOSTIC_CODES | hardcoded list in src/script/payment/dynamic-payment-templates.js |

The vendored JS files carry a checkout-ui preamble (@vtex-vendored:begin … @vtex-vendored:end) and a byte-for-byte CI comparator that strips it and diffs the rest against vtex/payment-mocker — today's upstream, reachable only by hand-copying.

That whole apparatus (preamble, comparator, manual re-sync PRs) exists only because there is no single, installable source of truth yet: payment-mocker and checkout-ui each hold their own copy, and something has to catch the two drifting apart. Once checkout-ui depends on @vtex/payment-templates-core and its build copies the two scripts straight from node_modules/@vtex/payment-templates-core/dist/wrap/runtime/… at build time, that stops being true: there is exactly one committed copy left (this package's own src/wrap/runtime/*.js), and checkout-ui's package.json version pin is what determines which bytes it gets — the same trust every other npm dependency already carries, not something a second, repo-specific comparator needs to re-verify. This package's own build already guarantees dist/ matches a reviewed src/ (yarn check:runtime-bytes, backed by the committed checksums in scripts/runtime-checksums.json); that is the one byte-for-byte guarantee that still needs to exist, and it already does.

  1. Depend on @vtex/payment-templates-core and switch the build's copy step to read from node_modules/@vtex/payment-templates-core/dist/wrap/runtime/… instead of the committed src/lib/payment-template/*.js.
  2. Delete the vendored files, the @vtex-vendored preamble, and the byte-for-byte comparator together — there is no second copy left for any of them to protect.

payment-template-document.js is not wrapTemplate. This package's wrapTemplate emits /lib/ script URLs; checkout-ui builds script URLs from its own CDN / /lib/payment-template/ layout. Host code can import DIAGNOSTIC_CODES and the theme-token helpers from the package through the build pipeline. Do not call wrapTemplate from checkout-ui until a later RFC adds a script-directory argument with the current /lib/ default preserved.

Contributing and the release process: see CONTRIBUTING.md.