Research
Security News
Malicious npm Packages Inject SSH Backdoors via Typosquatted Libraries
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
@homer0/jimple
Advanced tools
An extended version of the Jimple lib, with extra features.
If you are wondering why I built this, go to the Motivation section.
Unfourtunately, Jimple doesn't have type declarations, but someone (*) took the time, and wrote a .d.ts
for it. The issue is that shipping a custom .d.ts
is not super straightforward, and there were a couple of references that I wanted to change (**), so I worked some (ugly) magic, and you can now import the Jimple
class from this package and it will already be typed:
import { Jimple } from '@homer0/jimple';
const container = new Jimple();
container.register({
register: (c) => {
// `c` is typed to be `Jimple` :D!
},
});
*: For the life of me, I can't remember from where I copied the
.d.ts
file. It was like more than a year ago and I couldn't find it again, sorry!! If you are the author, please let know and I'll give you credit. **: If you were to extend theJimple
class, all the references to the container type, inside the class, would point to the original class, not the extended one. That was the main thing I wanted to change.
try
methodThis method is a shortcut of has
and get
: if the resource exists in the container, it returns it, otherwise, it returns undefined
:
const myService = container.try<MyService>('myService');
// myService is either `undefined` or `MyService`
if (myService) {
// myService exists
}
Not a lot of people are happy with OOP nowadays (not me :P), so, why not create a function alternative to create a container?
import { jimple } from '@homer0/jimple';
const container = jimple();
Yes, this exists on the original lib, I actually wrote it there, but the difference here is that it's a standalone function, instead of a static method, and it's created using the resource factory:
import { provider } from '@homer0/jimple';
export class MyService {
// ...
}
export const myService = provider((c) => {
c.set('myService', () => new MyService());
});
The generated object will look like this:
const myService = {
provider: true,
register: (c) => {
c.set('myService', () => new MyService());
},
};
Which means that you can go to the container and register it like this:
container.register(myService);
A provider creator is a both a provider, and a function that can create a provider.
Let's say you created a service that can be shared in multiple applications, or as multiple services in the same container, and you would want the implementation to be able to send some options/configuration to the provider, and maybe, also have a version that uses defaults:
import { provider } from '@homer0/jimple';
type Opts = {
url?: string;
};
export class MyService {
constructor({ url = 'http://localhost:3000' }: Opts) {
// ...
}
}
export const myServiceWithOptions = (options: Opts = {}) =>
provider((c) => {
c.set('myService', () => new MyService(options));
});
export const myService = myServiceWithOptions();
But that looks kind of hacky, and whoever implements it, needs to be aware of the two providers and their differences.
Here's where the providerCreator
comes in: this wrapper allows you to wrap the function that generates the provider, and its "default version" in a single provider:
import { providerCreator } from '@homer0/jimple';
type Opts = {
url?: string;
};
export class MyService {
constructor({ url = 'http://localhost:3000' }: Opts) {
// ...
}
}
export const myService = providerCreator((options: Opts = {}) => (c) => {
c.set('myService', () => new MyService(options));
});
The magic is that the return of providerCreator
is not actually a provider, but a Proxy
. If the proxy's register
function gets called, it will internally call the "creator function" without any arguments, and return the provider; but at the same time, you can also call the proxy as if it were the "creator function":
container.register(myService);
// or
container.register(myService({ url: 'http://localhost:9000' }));
The providerCreator
is created using the resource creator factory.
As you probably already guessed, the idea here is to group multiple providers in a single object, and then register it as a single provider:
import { providers } from '@homer0/jimple';
import { myServiceA } from './services/a';
import { myServiceB } from './services/b';
export const services = providers({
myServiceA,
myServiceB,
});
This will generate a provider that, when registered, it will register all the providers in the collection:
container.register(services);
The providers
is created using the resources collection factory.
Since provider
, providerCreator
and providers
are standalone functions, if you were to extend the Jimple
class, the type for the container you receive on registration would be the original class, not the extended one.
The way to get around this is by "creating your own functions":
import {
Jimple,
createProvider,
createProviderCreator,
createProviders,
} from '@homer0/jimple';
export class MyContainer {}
export const provider = createProvider<MyContainer>();
export const providerCreator = createProviderCreator<MyContainer>();
export const providers = createProviders<MyContainer>();
The inject helper is a resource you can use when creating reusable services, and it takes care of resolving instances and injections:
Let's say that your service has a dependency on another service, and you want to give the ability to the implementation to inject it, but at the same time, if the implementation doesn't send it, you need an instance of the dependency to work with.
You could do an if
with the parameters, but after a few of them, it gets ugly, so that's why the helper has the .get
method:
// Let's define our dependency dictionary.
type Dependencies = {
dep: string;
};
// Create the helper with it.
const helper = new InjectHelper<Dependencies>();
// Define the options for the service, that can be used for the injection.
type MyServiceOptions = {
inject?: Partial<Dependencies>;
};
const myService = ({ inject = {} }: MyServiceOptions) => {
// Ensure `dep` is always a `string`
const dep = helper.get(inject, 'dep', () => 'default');
console.log('dep:', dep);
};
Another use case when creating a reusable service, is to give the ability to the provider (creator) to specify the name of the services that should be injected from the container.
Keeping with the previous example, we could look for dep
on the container, but if the implementation used a different name? This is why we have the .resolve
method:
// Let's define our dependency dictionary.
type Dependencies = {
dep: string;
};
// Create the helper with it.
const helper = new InjectHelper<Dependencies>();
// Define the options for the provider.
type MyProviderOptions = {
services?: {
[key in keyof Dependencies]?: string;
};
};
const myProvider = providerCreator(
({ services = {} }: MyProviderOptions) =>
(container) => {
container.set('myService', () => {
// Tell the helper to look for `dep` in the container.
const inject = helper.resolve(['dep'], container, services);
return myService({ inject });
});
},
);
For each dependency, the method will first check if a new name was specified (on services
), and try to get it with that name, otherwise, it will try to get it with the default name (dep
), and if it doesn't exist, it will just keep it as undefined
.
The factories are in charge of creating the functions for the providers, and can be used to create other types of objects.
A resource is small object, with a name and a function. For example, a provider:
const resource = {
provider: true,
register: () => {},
};
The way the factory works is that it takes the function type, and returns a function that can be used to create the resource:
import { resourceFactory, type Jimple } from '@homer0/jimple';
type ActionFn = (c: Jimple) => void;
const factory = resourceFactory<ActionFn>();
const myAction = factory('action', 'fn', (c) => {
c.set('foo', 'bar');
});
And action
will look like this:
const myAction = {
action: true,
fn: (c) => {
c.set('foo', 'bar');
},
};
As you may have noticed, the factory task is just to set the type of the function so it can be later validated when creating the resource.
Like I did for provider
, you can create a function that wraps the result of the factory, and only takes the function:
import { resourceFactory, type Jimple } from '@homer0/jimple';
type ActionFn = (c: Jimple) => void;
const factory = resourceFactory<ActionFn>();
const action = (fn: ActionFn) => factory('action', 'fn', fn);
const myAction = action((c) => {
c.set('foo', 'bar');
});
In case you missed the section about provider creator, a creator is a proxy that can be used both as a resource, and a function that can create a resource.
Now, this is basically the same as the resource factory: it takes the function type, and returns a function that can be used to create the resource creator.
import { resourceCreatorFactory, type Jimple } from '@homer0/jimple';
type ActionFn = (c: Jimple) => void;
const factory = resourceCreatorFactory<ActionFn>();
const myAction = factory('action', 'fn', (name = 'foo') => (c) => {
c.set(name, 'bar');
});
Now, you can register with the default name, or with a custom one:
myAction.fn(container);
myAction('fooz')(container);
And like with providerCreator
, you can wrap the creation in a function:
import { resourceCreatorFactory, type Jimple } from '@homer0/jimple';
type ActionFn = (c: Jimple) => void;
// it will be typed, but needs to extend from `any`.
type ActionCreatorFn = (...args: any[]) => ActionFn;
const factory = resourceCreatorFactory<ActionFn>();
const actionCreator = <CreatorFn extends ActionCreatorFn>(creator: CreatorFn) =>
factory('action', 'fn', creator);
const myAction = actionCreator((name = 'foo') => (c) => {
c.set(name, 'bar');
});
The collections are technically resources that contains other resources, and when their function gets called, it calls the function in every resource in the collection.
import { resourcesCollectionFactory, type Jimple } from '@homer0/jimple';
// let's assume we have some actions already.
import { myActionA, myActionB } from './actions';
type ActionFn = (c: Jimple) => void;
const factory = resourcesCollectionFactory<'action', ActionFn>();
const myActions = factory('action', 'fn')({ myActionA, myActionB });
Just like with the others, the creation of the factory adds type validations, and returns a function to actually create collections.
But in this case, to simplify the implementation, you don't need to create an extra wrapper, you can just create the function the specifies the name and key (action
and fn
), and use its returned function to create the collections:
// ...
export const actions = factory('action', 'fn');
// ...
const myActions = actions({ myActionA, myActionB });
As this project is part of the packages
monorepo, some of the tooling, like lint-staged
and husky
, are installed on the root's package.json
.
Task | Description |
---|---|
lint | Lints the package. |
test | Runs the unit tests. |
build | Transpiles the project. |
types:check | Validates the TypeScript types. |
This used to be part of the wootils
package, my personal lib of utilities, but I decided to extract them into individual packages, as part of the packages
monorepo, and take the oportunity to migrate them to TypeScript.
I really like Jimple, I've used in countless projects, but there were customizations I wanted (like the ones in the features section) that didn't make a lot of sense in the original project, plus the factories, which I needed for my jimpex
project (jimple
+ express
).
FAQs
An extended version of the Jimple lib, with extra features
The npm package @homer0/jimple receives a total of 19 weekly downloads. As such, @homer0/jimple popularity was classified as not popular.
We found that @homer0/jimple demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 0 open source maintainers collaborating on the project.
Did you know?
Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.
Research
Security News
Socket’s threat research team has detected six malicious npm packages typosquatting popular libraries to insert SSH backdoors.
Security News
MITRE's 2024 CWE Top 25 highlights critical software vulnerabilities like XSS, SQL Injection, and CSRF, reflecting shifts due to a refined ranking methodology.
Security News
In this segment of the Risky Business podcast, Feross Aboukhadijeh and Patrick Gray discuss the challenges of tracking malware discovered in open source softare.