@valfuse-node/core
v0.5.0
Published
Umbrella entry point for Valfuse runtime APIs, framework adapters, and explicit Node.js subpaths.
Maintainers
Readme
@valfuse-node/core
Umbrella entry point for valfuse-node — a common import facade for the form domain, localization, React/Vue adapters, Express middleware, and Node.js code generators.
To load the full root facade directly in Node.js, install its optional framework peers too:
npm install @valfuse-node/core react react-dom vueThe core package graph gives you:
- Node.js asset generation API via @valfuse-node/core/assets
- Node.js barrel generation API via @valfuse-node/core/barrel
- Express middleware via @valfuse-node/core/express
- 📦 Form domain (
@valfuse-node/form) — schema, rules, validation, transformation, state - 🌐 Localization (
@valfuse-node/localization) — CLI compiler, validators, browser runtime - ⚛️ React adapter (
@valfuse-node/react) —useReactValfuseFormhook,<ValfuseController>,<LocalizationProvider> - 💚 Vue adapter (
@valfuse-node/vue) —useVueValfuseForm, localization provider, and composables
Install and compatibility
Framework peers: @valfuse-node/core declares react >= 18 and
vue >= 3 as optional peers because not every application uses both adapters.
@valfuse-node/express is an optional peer, used only by the explicit
/express subpath. Install it when that subpath is needed.
The React adapter also declares react-dom >= 18 as a peer dependency.
The core root entry re-exports both the React and Vue adapters. When loading
the core root entry, make both framework packages available, even if the
application only calls APIs from one adapter. The install command at the top
includes those peers.
Do not rely on tree-shaking to avoid resolving these peer imports. For a framework-neutral project, install and use the standalone packages instead:
npm install @valfuse-node/form @valfuse-node/localizationFor an app that uses only one adapter, install that adapter with
@valfuse-node/form and its framework peer directly.
The published packages declare Node.js 22 or newer in engines. React
applications need React and React DOM 18 or newer; Vue applications need Vue 3
or newer. Install only the framework packages used by your application when
you consume the individual adapter packages.
The root import is the runtime facade. Node.js code-generation APIs use
explicit subpaths: @valfuse-node/core/assets, @valfuse-node/core/barrel,
and @valfuse-node/core/express. This keeps filesystem-based generators and
the Express peer out of the root module graph for browser-oriented imports.
Express
Use the Express subpath with the standalone form and localization packages. It does not load the React or Vue adapters:
npm install @valfuse-node/core @valfuse-node/express @valfuse-node/form @valfuse-node/localization expressimport {
validateBody,
validateParams,
validateQuery,
createLocalizationMiddleware,
} from "@valfuse-node/core/express";
import { createSchema } from "@valfuse-node/form";TypeScript apps should also install compatible Express declarations:
npm install --save-dev @types/expressSee the @valfuse-node/express README for complete
middleware examples.
Table of Contents
- Quick Start by Adapter
- Install and compatibility
- Import Map
- API Reference
- Code generation APIs (Node.js)
- End-to-End Example
- Architecture
- Sub-package READMEs
- Release history
- Support
- License
Quick Start by Adapter
🧩 Form-only (Node.js, server actions, any framework)
For framework-neutral use, import the standalone form package directly. This keeps the React and Vue adapter peers out of the dependency graph.
import { createSchema, validateSchema, transformValues, t } from "@valfuse-node/form";
const schema = createSchema({
email: {
type: "string",
transform: t.pipe(t.trim, t.toLowerCase),
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "email", error: { message: "Invalid" } },
],
},
age: {
type: "number",
transform: t.toInteger,
rules: [{ name: "min", value: 18, error: { message: "18+" } }],
},
});
const cleaned = transformValues(schema, { email: " [email protected] ", age: "25" });
// → { email: "[email protected]", age: 25 }
const result = validateSchema(schema, cleaned);
// result.errors → {}⚛️ React
import {
createSchema,
useReactValfuseForm,
LocalizationProvider,
useLocalization,
localStorageStrategy,
} from "@valfuse-node/core";
import manifest from "./loc/manifest.json";
<LocalizationProvider manifest={manifest} storage={localStorageStrategy()}>
<LoginForm />
</LocalizationProvider>;
const schema = createSchema({
email: {
type: "string",
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "email", error: { message: "Invalid" } },
],
},
password: {
type: "string",
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "minLength", value: 8, error: { message: "Min 8" } },
],
},
});
export function LoginForm() {
const { translate } = useLocalization();
const form = useReactValfuseForm({
schema,
defaultValues: { email: "", password: "" },
mode: "onBlur",
});
return (
<form
onSubmit={form.handleSubmit(async (values) => {
await loginApi(values);
})}
>
<input {...form.register("email")} placeholder={translate("auth.email")} />
{form.formState.errors.email && <span>{form.formState.errors.email.message}</span>}
<input type="password" {...form.register("password")} />
{form.formState.errors.password && <span>{form.formState.errors.password.message}</span>}
<button type="submit" disabled={form.formState.isSubmitting}>
{translate("auth.submit")}
</button>
</form>
);
}💚 Vue
<script setup lang="ts">
import { createSchema, useVueValfuseForm } from "@valfuse-node/core";
const schema = createSchema({
email: {
type: "string",
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "email", error: { message: "Invalid" } },
],
},
password: {
type: "string",
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "minLength", value: 8, error: { message: "Min 8" } },
],
},
});
type Values = { email: string; password: string };
const form = useVueValfuseForm<Values>({
schema,
defaultValues: { email: "", password: "" },
});
async function onSubmit(values: Values) {
await loginApi(values);
}
</script>
<template>
<form @submit="form.handleSubmit(onSubmit)">
<input v-bind="form.register('email')" />
<p v-if="form.formState.errors.email">{{ form.formState.errors.email.message }}</p>
<input type="password" v-bind="form.register('password')" />
<p v-if="form.formState.errors.password">{{ form.formState.errors.password.message }}</p>
<button type="submit" :disabled="form.formState.isSubmitting">Log in</button>
</form>
</template>🌐 Localization (CLI + runtime)
# Compile YAML/JSON locale files → type-safe TypeScript
npx valfuse-localization init
npx valfuse-localization generate
npx valfuse-localization generate --watch// Or use the runtime interpolator directly (browser-safe):
import { interpolate } from "@valfuse-node/core";
interpolate("Hello, {name}!", { name: "Alice" }); // → "Hello, Alice!"
interpolate("{count, plural, one {# item} other {# items}}", { count: 5 });
// → "5 items"Import Map
@valfuse-node/core re-exports four packages. Two are flattened (no name collisions). The two adapter packages share a single value-level name (useValfuseForm), which is renamed at the umbrella level to useReactValfuseForm / useVueValfuseForm following the {Tech}{Domain}{Feature} convention. The underlying adapter packages keep their original useValfuseForm name.
| Source | Access pattern | Why? |
| ---------------------------- | ---------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| @valfuse-node/form | Top-level (createSchema, validateSchema, t, …) | Framework-agnostic, no collision risk |
| @valfuse-node/localization | Top-level (interpolate, compileProject, loadConfig, …) | Framework-agnostic, no collision risk |
| @valfuse-node/react | Top-level (useReactValfuseForm, ValfuseController, LocalizationProvider, useLocalization, …) | The single conflicting value useValfuseForm is renamed to disambiguate |
| @valfuse-node/vue | Top-level (useVueValfuseForm, VueLocalizationProvider, useVueLocalization, …) | Adapter names are prefixed to keep framework choice explicit |
Express middleware is available only from @valfuse-node/core/express; it is
kept out of the root entry so browser applications do not load Express.
import {
// Form domain
createSchema,
validateSchema,
transformValues,
normalizeError,
t,
// Localization
interpolate,
compileProject,
loadConfig,
runGenerate,
// React adapter — note the Tech-prefix
useReactValfuseForm,
ValfuseController,
LocalizationProvider,
useLocalization,
useLocalizationTree,
createLocalizationStore,
createLazyLocaleLoader,
createSsrLocalizationState,
localStorageStrategy,
sessionStorageStrategy,
cookieStrategy,
memoryStrategy,
composeStorage,
// Vue adapter — Tech-prefix disambiguates the identically-named React hook
useVueValfuseForm,
VueLocalizationProvider,
provideVueLocalization,
useVueLocalization,
useVueLocalizationTree,
} from "@valfuse-node/core";Why tech-prefix the hooks? Both
@valfuse-node/reactand@valfuse-node/vueexport a hook nameduseValfuseForm. Flattening both into the umbrella would silently shadow one. The{Tech}{Domain}{Feature}rename (useReactValfuseForm/useVueValfuseForm) makes the choice explicit at the call site. The underlying adapter packages still exportuseValfuseFormfor backward compat — the rename is umbrella-level only.
TypeScript-only: all types are re-exported flat from the form domain (no collision — both adapters import the same types from
@valfuse-node/form).
API Reference
Form domain (flattened to top level)
createSchema(fields)
Define your field structure and per-field rules. Returns a plain schema object — createSchema is an identity function that gives you type inference.
const schema = createSchema({
name: { type: "string", rules: [{ name: "required", error: { message: "Required" } }] },
age: { type: "number", rules: [{ name: "min", value: 18, error: { message: "18+" } }] },
tags: { type: "array", rules: [{ name: "nonempty", error: { message: "Add at least 1" } }] },
});Supported field types: string | number | boolean | array | object
Built-in rules (full reference in @valfuse-node/form README):
| Type | Rules |
| --------- | ----------------------------------------------------------------------------------------------------------------------------- |
| string | required, min, max, length, email, url, uuid, regex, includes, startsWith, endsWith |
| number | required, min, max, gt, gte, lt, lte, int, positive, nonnegative, negative, nonpositive, multipleOf |
| boolean | required, literal, accepted |
| array | required, min, max, length, nonempty |
| object | required, shape |
| generic | custom, refine, matchField, oneOf, notOneOf |
validateSchema(schema, values)
const errors = validateSchema(loginSchema, { email: "bad", password: "123" });
// → { email: { message: "Invalid email format", code: "email.invalid", type: "validation" } }transformValues(schema, rawValues)
const typed = transformValues(schema, { age: "25", active: "true" });
// → { age: 25, active: true }normalizeError(raw)
normalizeError("Something went wrong");
// → { message: "Something went wrong" }t(...transformerNames) — built-in transformers
| Group | Transformer | Effect |
| -------- | --------------------------------------------------------------------- | ---------------------------- |
| String | t.trim, t.trimStart, t.trimEnd | Whitespace removal |
| String | t.toLowerCase, t.toUpperCase, t.toTitleCase, t.toSentenceCase | Case |
| String | t.collapseSpaces | Collapse whitespace |
| Coercion | t.toNumber, t.toInteger, t.toFloat | String → number |
| Coercion | t.toBoolean | "true"/"1"/1/true → true |
| Compose | t.pipe(...fns) | Left-to-right composition |
const schema = createSchema({
email: {
type: "string",
transform: t.pipe(t.trim, t.toLowerCase),
rules: [{ name: "required", error: { message: "Required" } }],
},
});See the @valfuse-node/form README
for full coverage of all transformers and custom-transformer authoring.
Localization (flattened to top level)
The localization package has three import surfaces (CLI + compiler, browser runtime, types). All are available from @valfuse-node/core.
Compiler / CLI (Node.js only)
| Export | Use |
| ---------------------------------------------------------------------- | ----------------------------------------------------------- |
| loadConfig(cwd) | Load and parse the localization section of valfuse.yaml |
| compileProject(config) | Run the full compile pipeline |
| normalizeProject(config) | Normalize raw locale data |
| validateProject(config) | Check key/placeholder parity |
| runInit / runGenerate / runValidate / runCoverage / runClean | CLI command handlers |
import { loadConfig, compileProject } from "@valfuse-node/core";
const config = await loadConfig("./");
const compiled = await compileProject("./", config);Runtime (browser-safe)
| Export | Use |
| ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| interpolate(template, params, options?) | Replace {name} placeholders |
| lookupMessage(context, key) | Look up a translation by dot-path key |
| pickPluralVariant(variants, count) | Pick plural form by count |
| pickGenderVariant(variants, gender) | Pick gender form |
| pickContextVariant(variants, context) | Pick context form |
| pickStructuredPluralVariant / pickStructuredGenderVariant / pickStructuredContextVariant | High-level structured variant pickers (auto-parse the JSON payload from the manifest) |
| parseStructuredVariants(value) | Decode a JSON-encoded variant map |
import { interpolate } from "@valfuse-node/core";
interpolate("Hello, {name}!", { name: "Alice" });
// → "Hello, Alice!"
interpolate("{count, plural, one {# item} other {# items}}", { count: 5 });
// → "5 items"CLI
npx valfuse-localization init # scaffold sample locale files
npx valfuse-localization generate # compile JSON → TypeScript
npx valfuse-localization generate --watch
npx valfuse-localization validate # key/placeholder parity check
npx valfuse-localization coverage # per-locale coverage report
npx valfuse-localization clean # remove generated outputRequires Node.js ≥ 22.
See the @valfuse-node/localization README
for the full source-file format, structured variants, config file, and
programmatic compiler pipeline.
React adapter
All React values are at the top level. React is an optional peer in package metadata, but the core root facade statically re-exports both adapters and requires their peer modules to be resolvable. For form/localization-only usage, import the standalone packages as described in Install and compatibility.
import {
useReactValfuseForm, // main hook
ValfuseController, // controlled-input bridge component
LocalizationProvider, // context provider
useLocalization, // translation hook (full localizer API)
useLocalizationTree, // nested-tree hook
createLocalizationStore, // standalone mutable store
createLazyLocaleLoader, // code-split locales
createSsrLocalizationState, // SSR snapshot helper
localStorageStrategy,
sessionStorageStrategy,
cookieStrategy,
memoryStrategy,
composeStorage,
} from "@valfuse-node/core";useReactValfuseForm(options)
const form = useReactValfuseForm<UserValues>({
schema, // ValfuseSchema (required)
defaultValues, // { [field]: value } (required) — `TFieldValues` is inferred
mode?: "onSubmit" | "onChange" | "onBlur",
reValidateMode?: "onChange" | "onBlur" | "onSubmit",
});| Method / Property | Description |
| -------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| form.register(name) | Spread { name, value, onChange, onBlur, ref } onto an <input> |
| form.handleSubmit(fn) | Returns onSubmit handler; only calls fn(values) when validation passes |
| form.formState.errors | Partial<Record<keyof T, ValfuseFieldError>> — active errors |
| form.formState.isSubmitting | true while submit function is awaiting |
| form.formState.isSubmitted | true after first submit attempt |
| form.formState.isSubmitSuccessful | true if the most recent submit completed without throwing |
| form.formState.submitCount | Total submit attempts |
| form.formState.isValid | true when no validation errors exist |
| form.formState.isDirty | true when any field differs from defaultValues |
| form.formState.dirtyFields | Fields that differ from defaultValues |
| form.formState.touchedFields | Fields the user has blurred |
| form.formState.defaultValues | The defaults passed to the hook |
| form.setErrors(errors) | Inject errors manually (e.g. from API response) |
| form.clearErrors(fields?) | Clear one, many, or all errors |
| form.setValue(name, value, options?) | Programmatically set a field value ({ shouldValidate: true } to run validation) |
| form.trigger(name?) | Manually trigger validation. name can be a string, an array, or omitted (validate all) |
| form.watch(...) | Multi-overload subscribe / snapshot — watch(), watch("email"), watch(["a","b"]), watch(callback) |
| form.reset(values?) | Reset to defaultValues (or provided partial values); also clears submission state |
| form.control | Pass to <ValfuseController> for custom inputs |
<ValfuseController>
For controlled inputs that don't work with register (custom select, date picker, checkbox group).
<ValfuseController
control={form.control}
name="role"
render={({ field, fieldState }) => (
<Select
value={field.value}
onChange={field.onChange}
onBlur={field.onBlur}
isInvalid={!!fieldState.error}
/>
)}
/>Localization runtime
import {
useReactValfuseForm,
LocalizationProvider,
useLocalization,
localStorageStrategy,
} from "@valfuse-node/core";
<LocalizationProvider
manifest={localization}
storage={localStorageStrategy({ key: "app-locale" })}
initialLocale="en"
>
<App />
</LocalizationProvider>;
function Header() {
const { translate, locale, setLocale } = useLocalization();
return (
<header>
<h1>{translate("common.app.title")}</h1>
<select value={locale} onChange={(e) => setLocale(e.target.value)}>
<option value="en">English</option>
<option value="id">Bahasa Indonesia</option>
</select>
</header>
);
}useLocalization() returns:
| Group | API | Description |
| --------- | ------------------------------ | -------------------------------------------------------------------------------------------------------- |
| Translate | translate(key, fallback?) | Lookup with optional fallback |
| Translate | translateOrNull(key) | Returns null when missing or key is null/undefined |
| Format | format(key, params) | Lookup + placeholder interpolation |
| Format | formatOrNull(key, params) | Format, returns null when missing |
| Variants | plural(key, count) | Pick a plural branch |
| Variants | pluralOrNull(key, count) | Plural, returns null when missing |
| Variants | gender(key, value, params) | Pick a gender branch |
| Variants | context(key, value, params?) | Pick a context branch |
| Namespace | namespace(scope) | Returns a NamespacedLocalizer with the 8 methods above, all auto-prefixed |
| Iteration | entriesForLocale | Array<[key, value]> sorted alphabetically by key |
| Context | locale | Current locale string |
| Context | setLocale(locale) | Switch active locale (also writes to the configured storage) |
| Context | store | Lower-level LocalizationStore — store.t(key, params), store.getLocale(), store.setLocale(locale) |
| Context | manifest | The raw RuntimeManifest passed to the provider |
Storage strategies: localStorageStrategy | sessionStorageStrategy | cookieStrategy | memoryStrategy | composeStorage
See the @valfuse-node/react README
for full storage-strategy options and the useLocalizationTree() hook.
Vue adapter
All Vue values are at the top level. Vue is an optional peer in package
metadata; the core root facade still requires both adapter peers to be
resolvable when imported. Use @valfuse-node/vue directly if you do not need
the umbrella facade.
<script setup lang="ts">
import { createSchema, useVueValfuseForm } from "@valfuse-node/core";
const schema = createSchema({
email: {
type: "string",
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "email", error: { message: "Invalid" } },
],
},
password: {
type: "string",
rules: [
{ name: "required", error: { message: "Required" } },
{ name: "minLength", value: 8, error: { message: "Min 8" } },
],
},
});
type Values = { email: string; password: string };
const form = useVueValfuseForm<Values>({
schema,
defaultValues: { email: "", password: "" },
});
async function onSubmit(values: Values) {
await loginApi(values);
}
</script>
<template>
<form @submit="form.handleSubmit(onSubmit)">
<input v-bind="form.register('email')" />
<p v-if="form.formState.errors.email">{{ form.formState.errors.email.message }}</p>
<input type="password" v-bind="form.register('password')" />
<p v-if="form.formState.errors.password">{{ form.formState.errors.password.message }}</p>
<button type="submit" :disabled="form.formState.isSubmitting">Log in</button>
</form>
</template>The Vue register() returns { name, modelValue, "onUpdate:modelValue", onBlur } — compatible with Vue's v-bind and v-model. Vue also provides getValue() / getValues() and a localization provider/composable. Custom fields use Vue computed bindings; the React-specific <ValfuseController> is not part of the Vue API.
Code generation APIs (Node.js)
For programmatic asset and barrel generation, import the explicit Node.js subpaths. The command-line tools remain available as valfuse-assets and valfuse-barrel.
import { generateAssets } from "@valfuse-node/core/assets";
import { generateBarrel } from "@valfuse-node/core/barrel";
await generateAssets({ cwd: process.cwd() });
await generateBarrel({
cwd: process.cwd(),
config: {
inputDir: "src/rules",
outputFile: "src/rules/index.ts",
include: ["email.ts", "required.ts"],
},
});End-to-End Example
A complete React form with validation, transformation, server-error injection, and localization:
import {
createSchema,
useReactValfuseForm,
ValfuseController,
LocalizationProvider,
useLocalization,
localStorageStrategy,
} from "@valfuse-node/core";
import manifest from "./loc/manifest.json";
const schema = createSchema({
email: {
type: "string",
transform: (v) => String(v).trim().toLowerCase(),
rules: [
{ name: "required", error: { message: "Email is required", code: "email.required" } },
{ name: "email", error: { message: "Invalid email", code: "email.invalid" } },
],
},
password: {
type: "string",
rules: [
{ name: "required", error: { message: "Required", code: "password.required" } },
{ name: "minLength", value: 8, error: { message: "Min 8 chars", code: "password.min" } },
],
},
});
export function App() {
return (
<LocalizationProvider manifest={manifest} storage={localStorageStrategy()}>
<SignupForm />
</LocalizationProvider>
);
}
export function SignupForm() {
const { translate, format } = useLocalization();
const form = useReactValfuseForm({
schema,
defaultValues: { email: "", password: "" },
mode: "onBlur",
});
const onSubmit = form.handleSubmit(async (values) => {
try {
await signupApi(values);
} catch (err) {
form.setErrors({
email: { message: "Account already exists", type: "server", code: "auth.duplicate" },
});
}
});
return (
<form onSubmit={onSubmit}>
<label>
{translate("auth.email")}
<input {...form.register("email")} />
{form.formState.errors.email && (
<span className="error">{form.formState.errors.email.message}</span>
)}
</label>
<label>
{translate("auth.password")}
<input type="password" {...form.register("password")} />
{form.formState.errors.password && (
<span className="error">{form.formState.errors.password.message}</span>
)}
</label>
<ValfuseController
control={form.control}
name="email"
render={({ field, fieldState }) => (
<input
value={field.value}
onChange={(e) => field.onChange(e.target.value)}
onBlur={field.onBlur}
className={fieldState.error ? "invalid" : ""}
/>
)}
/>
<button type="submit" disabled={form.formState.isSubmitting}>
{translate("auth.submit")}
</button>
<p>{format("auth.password_hint", { min: 8 })}</p>
</form>
);
}Architecture
@valfuse-node/core keeps its main runtime entry as a re-export facade. Code-generation APIs and Express middleware live at explicit subpaths, so importing the root does not load the filesystem-based generators or Express peer.
┌────────────────────────────────────────────────────────────┐
│ @valfuse-node/core (this package — facade only) │
│ ├─ Form domain exports (flattened) │
│ ├─ Localization exports (flattened) │
│ ├─ React adapter exports (flat, with useReactValfuseForm)│
│ └─ Vue adapter exports (flat, with useVueValfuseForm) │
└────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
@valfuse-node/form @valfuse-node/ @valfuse-node/ @valfuse-node/
localization react vue
(yaml, chokidar) (peer: react, (peer: vue)
react-dom)Dependency direction (strictly inner→outer is forbidden):
form→ nothing (pure domain)localization→yaml,chokidarfor compiler/watch tooling; browser runtime is isolatedreact→form,localization(peers:react,react-dom)vue→form,localization(peer:vue)express→form,localization(peer:express)core→form,localization,react,vue,express,assets,barrel(React, Vue, Express peers are optional)
Build: turbo builds dependencies before core, including form, localization, express, react, vue, assets, and barrel. Each package's dist/ is what core resolves at runtime.
Tree-shaking and peer loading: core is a facade with named re-exports,
which lets compatible bundlers remove unused exports. Its root entry still
statically re-exports both framework adapters, so direct Node.js imports need
the React and Vue peer modules to resolve. Use the standalone packages or a
single adapter package when you need to avoid an unused framework peer.
Sub-package READMEs
For deeper detail on any specific surface:
@valfuse-node/assets— static asset registry CLI and API@valfuse-node/barrel— TypeScript barrel CLI and API@valfuse-node/form— schema, rules, transformers, validation, framework-agnostic state@valfuse-node/localization— CLI, compiler, runtime interpolation, structured variants, validators@valfuse-node/react—useValfuseForm,<ValfuseController>,LocalizationProvider, storage strategies@valfuse-node/vue—useValfuseForm, v-model bindings, localization provider and composables@valfuse-node/express— body/params/query validation and request-scoped localization
Release history
Release notes for this independently versioned package are maintained in the monorepo's changelog.
Support
For usage questions, bug reports, and feature requests, see the project support guide. For security reports, follow the security policy. Do not use public issues for security reports.
