Huge News!Announcing our $40M Series B led by Abstract Ventures.Learn More
Socket
Sign inDemoInstall
Socket

@contrast/dep-hooks

Package Overview
Dependencies
Maintainers
0
Versions
22
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@contrast/dep-hooks - npm Package Compare versions

Comparing version 1.4.0 to 1.5.0

lib/export-handler-registry.d.ts

116

lib/index.d.ts

@@ -1,26 +0,94 @@

/*
* Copyright: 2024 Contrast Security, Inc
* Contact: support@contrastsecurity.com
* License: Commercial
* NOTICE: This Software and the patented inventions embodied within may only be
* used as part of Contrast Security’s commercial offerings. Even though it is
* made available through public repositories, use of this Software is subject to
* the applicable End User Licensing Agreement found at
* https://www.contrastsecurity.com/enduser-terms-0317a or as otherwise agreed
* between Contrast Security and the End User. The Software may not be reverse
* engineered, modified, repackaged, sold, redistributed or otherwise used in a
* way not consistent with the End User License Agreement.
declare function _exports(core: {
readonly logger: import('@contrast/logger').Logger;
depHooks: DepHooks;
}): DepHooks;
declare namespace _exports {
export { DepHooks };
export { Descriptor };
}
export = _exports;
export type Descriptor = {
/**
* module name, as passed to `require`, to handle.
*/
name: string;
/**
* alternative to `name`, taking precedent when provided.
*/
module?: string | undefined;
/**
* if provided, the file under the module's root that we want to hook. otherwise, the module's `main` will be hooked.
*/
file?: string | undefined;
/**
* if provided, hooks will only execute against an installed module that matches the SemVer version range
*/
version?: string | undefined;
};
/**
* Allows clients to register function handlers which run as a 'post-hook' at
* require-time.
*/
import RequireHook from '@contrast/require-hook'; // TODO: fix these types
import { Logger } from '@contrast/logger';
interface Core {
readonly logger: Logger;
depHooks: RequireHook;
declare class DepHooks {
/**
* @param {import('pino').Logger=} logger
*/
constructor(logger?: import('pino').Logger | undefined);
/** @type {import('pino').Logger=} */
logger: import('pino').Logger | undefined;
originalLoad: any;
/** @type {HandlerInvoker} */
invoker: HandlerInvoker;
/** @type {ExportHandlerRegistry} */
registry: ExportHandlerRegistry;
/** @type {WeakMap<Object, Object>} */
requiredModules: WeakMap<Object, Object>;
/** @type {Set<string>} */
resets: Set<string>;
/**
* Registers handlers to run afer the described module is required.
* @template {Object} T
* @param {Descriptor | string} descriptor describes the module to hook
* @param {ExportHookDescriptor.Handler<T>[]} handlers the function hooks to execute after require
*/
resolve<T extends Object>(descriptor: Descriptor | string, ...handlers: ExportHookDescriptor.Handler<T>[]): void;
/**
* Provided with an export, a collection of handlers, and metadata, will
* invoke only the handlers which have not yet run on the export instance.
* @template {Object} T
* @param {T} xport the exported value of a required module
* @param {ExportHookDescriptor.Handler<T>[]} handlers the function hooks to execute on require
* @param {import('./package-finder').Metadata} metadata the export's metadata
* @returns {T}
*/
runRequireHandlers<T_1 extends Object>(xport: T_1, handlers: ExportHookDescriptor.Handler<T_1>[], metadata: import('./package-finder').Metadata): T_1;
/**
* Checks if module name exists in resets set. If so, it will remove it from
* the set as well as remove it from the invoker WeakMap. This will force
* instrumentation handlers to re-run. This use case is only used for testing
* of the node agent in certain cases.
* @template {Object} T
* @param {string} request the string passed to require()
* @param {T} xport the exported value of a required module
*/
maybeClearHandlers<T_2 extends Object>(request: string, xport: T_2): void;
/**
* Overrides the Module._load method to run registered handlers _after_
* the modules have loaded. This method is invoked by require.
*/
install(): void;
/**
* Resets Module's _load method to the original value.
*/
uninstall(): void;
/**
* Resets the seen handlers for a given module - they will be re-run on next
* require.
* @param {string} request the string passed to require()
*/
reset(request: string): void;
}
declare function init(core: Core): RequireHook;
export = init;
import HandlerInvoker = require("./handler-invoker");
import ExportHandlerRegistry = require("./export-handler-registry");
import ExportHookDescriptor = require("./export-hook-descriptor");
//# sourceMappingURL=index.d.ts.map

@@ -15,13 +15,148 @@ /*

*/
// @ts-check
'use strict';
const RequireHook = require('@contrast/require-hook');
Object.defineProperty(exports, "__esModule", { value: true });
const Module = require('node:module');
const ExportHandlerRegistry = require('./export-handler-registry');
const ExportHookDescriptor = require('./export-hook-descriptor');
const HandlerInvoker = require('./handler-invoker');
/**
* @typedef {Object} Descriptor
* @property {string} name module name, as passed to `require`, to handle.
* @property {string=} module alternative to `name`, taking precedent when provided.
* @property {string=} file if provided, the file under the module's root that we want to hook. otherwise, the module's `main` will be hooked.
* @property {string=} version if provided, hooks will only execute against an installed module that matches the SemVer version range
*/
/**
* Coerces a string into a minimal object that can be made into a descriptor.
* @param {Descriptor | string} descriptor The export descriptor
* @returns {Descriptor}
*/
const normalizeDescriptor = (descriptor) => {
if (typeof descriptor === 'string') {
return { name: descriptor };
}
if (descriptor.module) {
descriptor.name = descriptor.module;
}
return descriptor;
};
/**
* Allows clients to register function handlers which run as a 'post-hook' at
* require-time.
*/
class DepHooks {
/**
* @param {import('pino').Logger=} logger
*/
constructor(logger) {
/** @type {import('pino').Logger=} */
this.logger = logger;
// set this up before we start patching Module methods
this.originalLoad = Reflect.get(Module, '_load');
/** @type {HandlerInvoker} */
this.invoker = new HandlerInvoker(logger);
/** @type {ExportHandlerRegistry} */
this.registry = new ExportHandlerRegistry(logger);
/** @type {WeakMap<Object, Object>} */
this.requiredModules = new WeakMap();
/** @type {Set<string>} */
this.resets = new Set();
}
/**
* Registers handlers to run afer the described module is required.
* @template {Object} T
* @param {Descriptor | string} descriptor describes the module to hook
* @param {ExportHookDescriptor.Handler<T>[]} handlers the function hooks to execute after require
*/
resolve(descriptor, ...handlers) {
const normalized = normalizeDescriptor(descriptor);
const info = ExportHookDescriptor.create({ ...normalized, handlers });
this.registry.update(info);
}
/**
* Provided with an export, a collection of handlers, and metadata, will
* invoke only the handlers which have not yet run on the export instance.
* @template {Object} T
* @param {T} xport the exported value of a required module
* @param {ExportHookDescriptor.Handler<T>[]} handlers the function hooks to execute on require
* @param {import('./package-finder').Metadata} metadata the export's metadata
* @returns {T}
*/
runRequireHandlers(xport, handlers, metadata) {
return this.invoker.invoke(xport, handlers, metadata);
}
/**
* Checks if module name exists in resets set. If so, it will remove it from
* the set as well as remove it from the invoker WeakMap. This will force
* instrumentation handlers to re-run. This use case is only used for testing
* of the node agent in certain cases.
* @template {Object} T
* @param {string} request the string passed to require()
* @param {T} xport the exported value of a required module
*/
maybeClearHandlers(request, xport) {
if (this.resets.has(request)) {
this.resets.delete(request);
this.invoker.reset(xport);
}
}
/**
* Overrides the Module._load method to run registered handlers _after_
* the modules have loaded. This method is invoked by require.
*/
install() {
this.logger?.trace('Applying Module._load override');
const self = this;
/**
* @this {Module}
* @param {string} request the string passed to require()
* @param {Module} parent the module executing require()
* @param {boolean} isMain indicates whether the module executing require() is the entry point
*/
const __loadOverride = function __loadOverride(request, parent, isMain) {
let xportSubstitution;
const exportHandlerInfo = self.registry.query(request, parent, isMain);
const xport = Reflect.apply(self.originalLoad, this, [
request,
parent,
isMain,
]);
if (exportHandlerInfo) {
self.maybeClearHandlers(request, xport);
xportSubstitution = self.runRequireHandlers(self.requiredModules.get(xport) ?? xport, exportHandlerInfo.handlers, exportHandlerInfo.metadata);
self.requiredModules.set(xport, xportSubstitution);
}
return self.requiredModules.get(xport) ?? xport;
};
Reflect.set(Module, '_load', __loadOverride);
}
/**
* Resets Module's _load method to the original value.
*/
uninstall() {
this.logger?.trace('Removing Module._load override');
Reflect.set(Module, '_load', this.originalLoad);
}
/**
* Resets the seen handlers for a given module - they will be re-run on next
* require.
* @param {string} request the string passed to require()
*/
reset(request) {
this.resets.add(request);
}
}
/**
* @param {{
* readonly logger: import('@contrast/logger').Logger;
* depHooks: DepHooks;
* }} core
* @returns {DepHooks}
*/
module.exports = function init(core) {
core.depHooks = new RequireHook(
core.logger.child({ name: 'contrast:dep-hooks' })
);
return core.depHooks;
core.depHooks = new DepHooks(core.logger.child({ name: 'contrast:dep-hooks' }));
return core.depHooks;
};
module.exports.DepHooks = DepHooks;
//# sourceMappingURL=index.js.map
{
"name": "@contrast/dep-hooks",
"version": "1.4.0",
"description": "Wrapper around the Contrast require-hook library",
"version": "1.5.0",
"description": "Post hooks for Module.prototype.require",
"license": "SEE LICENSE IN LICENSE",

@@ -13,12 +13,14 @@ "author": "Contrast Security <nodejs@contrastsecurity.com> (https://www.contrastsecurity.com)",

"engines": {
"npm": ">=6.13.7 <7 || >= 8.3.1",
"node": ">= 16.9.1"
"npm": ">=6.13.7 <7 || >=8.3.1",
"node": ">=16.9.1"
},
"scripts": {
"build": "tsc --build src/",
"test": "../scripts/test.sh"
},
"dependencies": {
"@contrast/logger": "1.9.0",
"@contrast/require-hook": "^5.0.0"
"@contrast/find-package-json": "^1.1.0",
"@contrast/logger": "1.10.0",
"semver": "^7.6.3"
}
}

@@ -1,18 +0,94 @@

# `@contrast/dep-hooks`
# @contrast/dep-hooks
Register hooks for module loads.
Intercept calls to `require` in order to modify or replace exports.
This provides a factory wrapper around `@contrast/require-hook`.
## Usage
RequireHook class exposes the following methods:
### Class: `DepHooks`
`resolve(descriptor, ...handlers)` - Registers handlers to run after the described
module is required.
* descriptor:String - descriptor that describes the module to hook
* handlers:Function - functions that are executed after the require
#### Instantiation
`reset(request)` - Resets the seen handlers for a given module - they will be re-run
on next require.
* request:String - request the string passed to require()
[https://github.com/Contrast-Security-Inc/node-require-hook]()
```javascript
const DepHooks = require('./lib');
const depHooks = new DepHooks();
```
The `DepHooks` constructor accepts a [`pino`](https://github.com/pinojs/pino)
logger as an argument.
#### `.resolve(descriptor, ...handlers)`
Options:
- `descriptor`: This can be a string or an object describing the module you want
to intercept. If a string is used, or if the version field of the descriptor
isn't set, all versions of the described module will be matched. Descriptors
can have a `name`, `version`, and `file` property.
- `handlers`: The remaning arguments are the handlers which will be invoked when
the described module is `require`'d. Each handler is passed the exported
module and metadata including the module's root directory and its name and
version as seen in its `package.json` file. If a handler returns a truthy
value, then that value will replace the return value of `require`.
_**Note:**_ Registered handlers run _once_ per unique instance of an export
matching a descriptor.
#### `.install()`
This will monkey-patch `Module.prototype.require` so that exports can be
intercepted. The monkey-patching will only happen once regardless of how many
times this is invoked.
#### `.uninstall()`
This will reset `Module.prototype.require` to its value before being
monkey-patched by the instance.
## Examples
**Use case:** For `express` versions greater than or equal to 4, intercept the
export of the package's `lib/view.js` file (relative to the package's base
directory) and apply a tag to the exported function.
```javascript
const DepHooks = require('./lib');
const depHooks = new DepHooks();
depHooks.resolve(
{
name: 'express',
version: '>=4',
file: 'lib/view.js',
},
(xport, metadata) => {
// Read from the package.json:
// - metadata.name
// - metadata.version
// Absolute path to file:
// - metadata.packageDir
// xport === function View() { /*...*/ }
xport['I was intercepted'] = true;
},
);
```
**Use case:** Intercept all versions of `body-parser` and replace the exported
functions.
```javascript
const DepHooks = require('./lib');
const depHooks = new DepHooks();
depHooks.resolve({ name: 'body-parser' }, (xport, metadata) => {
// Read from the package.json:
// - metadata.name
// - metadata.version
// Absolute path to file:
// - metadata.packageDir
// xport === function bodyParser() { /*...*/ }
return function bodyParserReplacement() {
/*...*/
};
});
```
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc