🚀. Socket Launch Week Day 3:Socket Firewall Now Blocks Malicious VS Code and Open VSX Extensions.Learn more
Sign In

@nodable/entities

Package Overview
Dependencies
Maintainers
1
Versions
7
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@nodable/entities - npm Package Compare versions

Comparing version
2.1.1
to
2.2.0
+4
-1
package.json
{
"name": "@nodable/entities",
"version": "2.1.1",
"version": "2.2.0",
"description": "Entity parser for XML, HTML, External entites with security and NCR control",

@@ -28,2 +28,4 @@ "main": "./src/index.js",

"entity",
"entities",
"stringify",
"encode",

@@ -33,2 +35,3 @@ "decode",

"security",
"safe",
"performance"

@@ -35,0 +38,0 @@ ],

+104
-3

@@ -9,2 +9,26 @@ // ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Entity hook action constants
// ---------------------------------------------------------------------------
/**
* Action constants for `onExternalEntity` and `onInputEntity` hooks.
*
* Use these instead of raw strings to avoid typos:
*
* @example
* import EntityDecoder, { ENTITY_ACTION } from './EntityDecoder.js';
* const dec = new EntityDecoder({
* onInputEntity: (name, value) => ENTITY_ACTION.BLOCK,
* });
*/
export const ENTITY_ACTION = Object.freeze({
/** Resolve and expand the entity normally. */
ALLOW: 'allow',
/** Silently skip this entity — it will not be registered. */
BLOCK: 'block',
/** Throw an error, aborting entity registration entirely. */
THROW: 'throw',
});
// ---------------------------------------------------------------------------
// Helpers

@@ -173,2 +197,10 @@ // ---------------------------------------------------------------------------

* Action for U+0000 (null). 'allow' and 'leave' are clamped to 'remove' since null is never safe.
* @param {((name: string, value: string) => 'allow'|'block'|'throw')|null} [options.onExternalEntity=null]
* Hook called when an external entity is registered via `setExternalEntities()` or
* `addExternalEntity()`. Return `ENTITY_ACTION.ALLOW` to accept the entity,
* `ENTITY_ACTION.BLOCK` to silently skip it, or `ENTITY_ACTION.THROW` to abort with an error.
* @param {((name: string, value: string) => 'allow'|'block'|'throw')|null} [options.onInputEntity=null]
* Hook called when an input entity is registered via `addInputEntities()`. Return
* `ENTITY_ACTION.ALLOW` to accept, `ENTITY_ACTION.BLOCK` to silently skip, or
* `ENTITY_ACTION.THROW` to abort with an error.
*/

@@ -209,5 +241,42 @@ constructor(options = {}) {

this._ncrNullLevel = ncrCfg.nullLevel;
// --- Registration hooks ---
/** @type {((name: string, value: string) => 'allow'|'block'|'throw')|null} */
this._onExternalEntity = typeof options.onExternalEntity === 'function'
? options.onExternalEntity
: null;
/** @type {((name: string, value: string) => 'allow'|'block'|'throw')|null} */
this._onInputEntity = typeof options.onInputEntity === 'function'
? options.onInputEntity
: null;
}
// -------------------------------------------------------------------------
// Private: registration hook dispatch
// -------------------------------------------------------------------------
/**
* Invoke a registration hook for a single entity name/value pair.
* Returns true when the entity should be accepted, false when it should be
* silently skipped (BLOCK), and throws when the hook returns THROW.
*
* @param {((name: string, value: string) => 'allow'|'block'|'throw')|null} hook
* @param {string} name
* @param {string} value
* @param {string} context — used in error messages ('external' | 'input')
* @returns {boolean} true = accept, false = skip
*/
_applyRegistrationHook(hook, name, value, context) {
if (!hook) return true; // no hook → always accept
const action = hook(name, value);
if (action === ENTITY_ACTION.BLOCK) return false;
if (action === ENTITY_ACTION.THROW) {
throw new Error(
`[EntityDecoder] Registration of ${context} entity "&${name};" was rejected by hook`
);
}
return true; // ALLOW or any unknown return value → accept
}
// -------------------------------------------------------------------------
// Persistent external entity registration

@@ -219,2 +288,5 @@ // -------------------------------------------------------------------------

* All keys are validated — throws on invalid characters.
* If `onExternalEntity` is set, it is called once per entry; entries that
* return `ENTITY_ACTION.BLOCK` are silently omitted, `ENTITY_ACTION.THROW`
* aborts the whole call.
* @param {Record<string, string | { regex?: RegExp, val: string }>} map

@@ -228,3 +300,15 @@ */

}
this._externalMap = mergeEntityMaps(map);
if (!this._onExternalEntity) {
this._externalMap = mergeEntityMaps(map);
return;
}
// Hook present — resolve values first, then filter
const flat = mergeEntityMaps(map);
const filtered = Object.create(null);
for (const [name, value] of Object.entries(flat)) {
if (this._applyRegistrationHook(this._onExternalEntity, name, value, 'external')) {
filtered[name] = value;
}
}
this._externalMap = filtered;
}

@@ -234,2 +318,4 @@

* Add a single persistent external entity.
* If `onExternalEntity` is set it is called before the entity is stored;
* `ENTITY_ACTION.BLOCK` silently skips storage, `ENTITY_ACTION.THROW` raises.
* @param {string} key

@@ -241,3 +327,5 @@ * @param {string} value

if (typeof value === 'string' && value.indexOf('&') === -1) {
this._externalMap[key] = value;
if (this._applyRegistrationHook(this._onExternalEntity, key, value, 'external')) {
this._externalMap[key] = value;
}
}

@@ -253,2 +341,4 @@ }

* Also resets per-document expansion counters.
* If `onInputEntity` is set it is called once per entry; entries returning
* `ENTITY_ACTION.BLOCK` are silently omitted, `ENTITY_ACTION.THROW` aborts.
* @param {Record<string, string | { regx?: RegExp, regex?: RegExp, val: string }>} map

@@ -259,3 +349,14 @@ */

this._expandedLength = 0;
this._inputMap = mergeEntityMaps(map);
if (!this._onInputEntity) {
this._inputMap = mergeEntityMaps(map);
return;
}
const flat = mergeEntityMaps(map);
const filtered = Object.create(null);
for (const [name, value] of Object.entries(flat)) {
if (this._applyRegistrationHook(this._onInputEntity, name, value, 'input')) {
filtered[name] = value;
}
}
this._inputMap = filtered;
}

@@ -262,0 +363,0 @@

@@ -9,2 +9,46 @@ // ---------------------------------------------------------------------------

// ---------------------------------------------------------------------------
// Entity registration hook
// ---------------------------------------------------------------------------
/**
* Actions returned by `onExternalEntity` / `onInputEntity` hooks.
* Use the `ENTITY_ACTION` constant object instead of raw strings to avoid typos.
*
* - `'allow'` — register and expand the entity normally
* - `'block'` — silently skip the entity (not registered, treated as unknown)
* - `'throw'` — abort registration with an error
*/
export type EntityHookAction = 'allow' | 'block' | 'throw';
/**
* Immutable constant bag for entity registration hook return values.
*
* @example
* import { ENTITY_ACTION } from '@nodable/entities';
* const dec = new EntityDecoder({
* onInputEntity: (_name, _value) => ENTITY_ACTION.BLOCK,
* });
*/
export const ENTITY_ACTION: Readonly<{
/** Register and expand the entity normally. */
ALLOW: 'allow';
/** Silently skip this entity — it will not be registered. */
BLOCK: 'block';
/** Throw an error, aborting entity registration. */
THROW: 'throw';
}>;
/**
* Callback signature for `onExternalEntity` and `onInputEntity` hooks.
*
* Called once per entity **at registration time** (not at decode time).
* Return `ENTITY_ACTION.ALLOW` (or `'allow'`) to accept, `ENTITY_ACTION.BLOCK`
* to silently skip, or `ENTITY_ACTION.THROW` to raise an error.
*
* @param name — the entity name without `&` / `;`, e.g. `"brand"`
* @param value — the resolved string value after any `{regex,val}` unwrapping
*/
export type EntityRegistrationHook = (name: string, value: string) => EntityHookAction;
// ---------------------------------------------------------------------------
// Encoder options

@@ -195,2 +239,35 @@ // ---------------------------------------------------------------------------

ncr?: EntityDecoderNCROptions;
/**
* Hook called once **at registration time** for each entity passed to
* `setExternalEntities()` or `addExternalEntity()`.
*
* - `'allow'` (or `ENTITY_ACTION.ALLOW`) — register the entity normally (default)
* - `'block'` (or `ENTITY_ACTION.BLOCK`) — silently skip; the entity is not stored
* - `'throw'` (or `ENTITY_ACTION.THROW`) — abort registration with an `Error`
*
* The hook receives the entity name (without `&`/`;`) and the resolved string
* value. It is **not** called during `decode()` — only when entities are added.
*
* @example
* const dec = new EntityDecoder({
* onExternalEntity: (name, value) =>
* DANGEROUS_NAMES.has(name) ? ENTITY_ACTION.BLOCK : ENTITY_ACTION.ALLOW,
* });
*/
onExternalEntity?: EntityRegistrationHook | null;
/**
* Hook called once **at registration time** for each entity passed to
* `addInputEntities()`.
*
* Follows the same `'allow' | 'block' | 'throw'` contract as `onExternalEntity`.
*
* @example
* const dec = new EntityDecoder({
* // Block all input / DOCTYPE entities unconditionally
* onInputEntity: () => ENTITY_ACTION.BLOCK,
* });
*/
onInputEntity?: EntityRegistrationHook | null;
}

@@ -197,0 +274,0 @@

@@ -9,3 +9,3 @@ /**

export { default as EntityDecoder } from './EntityDecoder.js';
export { default as EntityDecoder, ENTITY_ACTION } from './EntityDecoder.js';
export {

@@ -12,0 +12,0 @@ COMMON_HTML,