
Research
Two Malicious Rust Crates Impersonate Popular Logger to Steal Wallet Keys
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.
@likelytheory/fage
Advanced tools
Declarative sequential async middleware runner
Fage is an ultra-lightweight wrapper and function runner that enables composing apps as middleware and decouples the application interface from its implementation.
const myFageLogin = {
path: 'userLogin',
fns: [checkAuth, rateLimit, ctx => customLoginLogic(ctx.input)]
}
// Wire up Fage app
const app = Fage([myFageLogin, ...])
// Call it from your interface
express.post('/login', myMiddleware, (req, res) => app.userLogin(req.body, req.state.CustomAppData))
Using Fage (jump to the example):
Create logic blocks:
Define objects with a unique path
name and an array of "middleware" functions fns
. These objects are your "method blocks".
Package into functions:
These method blocks are bundled by Fage(arrayOfMethodBlocks)
into a flat object of runnable functions, keyed by each method block's path
value. Each function is a reducer that runs the middleware fns
, passing each fn: a) the ctx
context object and b) the output
of each call to the next function in the chain, returning a Promise that resolves as the final middleware output.
Run functions:
Once bundled, functions are called with two params: a) untrusted data from user input
, and b) trusted app/environment data meta
. These become available on the ctx
context passed to Fage method block fns.
Fage functions can be invoked by independent interfaces that map their interface calls, inputs and application data to named Fage functions. This decouples the interface from the underlying app logic, which itself can be composed as middleware. Your app becomes a collection of lightweight objects that can be plugged into any interface (including HTTP, sockets, RPC, CLI etc).
Fage (fayj) - a phage is used to carry code for execution. It's also a f-unction c-age.
Getting Started:
Usage:
API:
Fage is a private repository for which you will require a Github token.
TODO: Insert a howto for this
At which point you can install from Github as follows:
$ npm install likelytheory/fage
const {nyanSay} = require('./mycode')
// Example middleware function
const myIsAuthedMiddleware = (ctx) => {
if (!ctx.meta.loggedIn) throw new Error('Not Logged In')
}
// Example Fage method block:
modules.export = {
path: 'nyanExclaim',
fns: [
myIsAuthedMiddleware, // throws if ctx.meta.loggedIn not set
(ctx) => ctx.input + '!@#!!',
(ctx, output) => (nyanSay(output), output) // returns output
]
}
Fage bundles this app logic into a flat object of runnable functions keyed by the path
:
const Fage = require('fage')
const nyanExclaim = require('./exampleAbove')
const app = Fage([nyanExclaim])
// -> {nyanExclaim: Function}
Functions can then be run by passing fn(input, meta)
, where input
is the untrusted, raw user-input and meta
is an object comprising any system defined information (such as authentication details, environment data, etc).
await app.nyanExclaim('meow meow', {loggedIn: true})
// OR: await Fage.run(nyanExclaim, 'meow meow', {loggedIn: true})
// ,---/V\ ________________
// ,*'^`*.,*'^`*. ~|__(o.o) __/ meow meow!@#!! \
// .,*'^`*.,*'^`*.,*' UU UU `------------------`
// -> "meow meow!@#!!"
In the example above, failing to provide a loggedIn
value on the meta
parameter will trigger an Auth error in our method block:
await app.nyanExclaim('meow meow')
// -> Error: Not Logged In
App logic can be composed as middleware. The bundled app is decoupled from any interface (making it very testable, and easy to reason about). This also allows you define arbitrary API maps for your interfaces, completely independently of your app logic. It gets even better when you use these maps to create generated interfaces (but the principle is still powerful even if you are manually wiring).
Once you have setup a Fage Application Object:
const app = Fage(methodBlocks)
Invoke named path
functions with app.<path>(input, meta)
params, which returns a Promise
that resolves as the output result or rejects as any thrown Error.
Fage Application Objects expose Fage methods that expect to be called with two "channel" parameters (both optional):
app.method(input, meta)
// -> Promise
Fage is intended to be invoked by a separate and independent interface (read more about interfaces here).
These interfaces should accept user input of some kind (input
), attach extra system or app derived data (meta
), and map their calls to an appropriate Fage method.
It's the interface's job to separate the data channels for Fage:
input
is any input data provided by the end user.meta
is any data that your interface sets (ie. trusted data)For example: an HTTP interface might listen for a POST /hello
- when called (with say "world"
) it may first do auth token validation and set a few environment system values eg. {userId: null, turbo: false}
- the meta
channel is the mechanism for passing this application environment data to Fage. The interface would then call:
await app.hello("world", {userId: null, turbo: false})
// -> "hello world!"
The interface would then utilise the output of the Fage method ("hello world!
") however it wanted.
A Fage Method Block is a simple object, mainly comprising a path
to uniquely identify the block and an array of fns
that are the middleware functions.
path
: String: Uniquely identifies the method blockfns
: Array[Functions]: An array of Fage Middlewareref
: Object (Optional): Custom block data for use by middlewareonError
: Function(ctx, err) (Optional): Hook to observe errors thrown by middlewareNotes for onError
In general, errors should be handled by your interface layer and not by Fage itself (which should simply generate errors to be handled).
However, the optional onError
hook is a function that is invoked if the method block throws an Error, and can be used to 'observe' (but not obstruct or 'catch') middlware failure states. The onError function receives two parameters: the ctx
context object and the thrown Error
object, eg. (ctx, err)
.
Note that onError
functions are run synchronously, any return values are discarded and any exceptions in the hook are silently suppressed, so only the original error is propagated.
Example Method Block For example (providing a bunch of code and middleware imported from elsewhere):
const example = {
path: 'superHacker', // -> app.superHacker(input, meta)
ref: {
model: inputModels.targetAndIntent, // Some custom model
nonsense: 'oh yes!', // Some farcical key
scopes: ['admin'] // Specify admin scopes
},
onError: (ctx, err) => errorLogHandler(err),
fns: [
mw.ensure.isAuthed, // Checks ctx.meta.user
mw.ensure.hasScopes, // Checks ctx.ref.scopes
mw.skematic.validate, // Checks ctx.input on ctx.ref.model
(ctx) => console.log(ctx.ref.nonsense), // "oh yes!"
(ctx) => hackThePlanet(ctx.input) // row, row, row ur boat
]
}
Fage middleware are functions that accept two parameters mw(ctx, output)
, and optionally return an output. These middleware are what Fage chains together, passing the output of each previous function into the next.
Middleware functions can be either synchronous by immediately returning a value, or can be async by returning a Promise.
Fage waits on the output of each middleware before invoking the next in the chain.
Errors should throw and should be handled at the interface level - Fage methods should throw a descriptive Error object and leave the interface to determine how to handle this.
const checkMw = (ctx) => {
if (ctx.input === 'harold') throw new Error('No harolds!')
}
const sleepMw = (ctx) => sleepFor('30m').then(() => 'morning')
const logMw = (ctx, out) => console.log(`${out} ${ctx.input}!`)
const blk = {path: 'sleepy', fns: [checkMw, sleepMw, logMw]}
const app = Fage([blk])
await app.sleepy('harold')
// -> Error: No harolds!
await app.sleepy('jenny')
// (...after 30 mins...)
// "morning jenny!" (console.log output)
// -> undefined
Important note: The example above final return value was
undefined
- this is because the last middleware (logMw
) returned aconsole.log
, the return value of which isundefined
.Pay close attention to what you're returning.
Middleware have full access to the ctx
context object, detailed below.
ctx
context objectThe context ctx
object is passed as the first parameter to every middleware.
The two data channels are available on ctx
as:
input
: Any - The "user supplied" input data channelmeta
: Object - The application/interface set meta data channelIn addtion, underlying method block values are also provided:
path
: String - The unique path
value for the underlying method blockref
: Object - Any ref
data set in the underlying method blockThe context object is immutable except for its state
parameter, which middleware may choose to use to store stateful info if returning its output is insufficient.
state
: Any - A mutable field to store dataThe primary API for Fage is the single factory call Fage()
.
Packages the methodBlocks in methodBlocksArray
into a shallow object of runnable functions keyed by each method block's path
value.
const Fage = require('fage')
const mw = (ctx) => `hello ${ctx.input}!`
const greeterBlock = {path: 'hello', fns: [mw]}
const app = Fage([greeterBlock])
// -> {hello: Function}
await app.hello('world')
// -> "hello world!"
Parameters:
methodBlocksArray
: Array - An array of Fage method blocksReturns:
path
value.Helper API methods There are also a handful of helper methods that may be of use during development of Fage apps:
Runs a specific method block object.
Note: This is essentially what the factory
Fage()
method uses to bind a method block to run as a Function.
// Using the example from `Fage()` above
Fage.run(greeterBlock, 'earth')
// -> "hello earth!"
Parameters:
methodBlock
: MethodBlock Object - a Fage method block objectinput
: Any - (Optional) - any userland input datameta
: Object - (Optional) - application defined meta dataReturns:
fns
Fage simply bundles flat objects of runnable method blocks, which themselves are thin wrappers keyed by their path
values and containing middleware fns
. Fage methods receive parameters (input, meta)
, but Fage itself does not know (or care) where these come from or how they are defined.
That is the job of an interface.
An interface layer should:
meta
)meta
path
: app.<path>(input, meta)
output
from Fagecontext
to run (object with path
, model
, fns
etc)data
, andmeta
information, includingInterfaces will typically have some knowledge of the shape of meta
data that the underlying Fage app requires (or vice versa). For example, if your interface does an authentication check and retrieves user information, it will attach these to meta
based on some key convention.
eg. If your Fage app looks for user login data on ctx.meta.user
, then your interface should be putting its userData under user
:
interface.endpoint(<path>, () => {
return await app.<path>(input, {user: userData})
})
Here we setup a basic Fage app, and then create a basic Express HTTP interface.
Starting with the Fage app methods:
const Fage = require('fage')
const {chkPermissions, validateInputs, formatData, dbSave, dbGet, log} = require('./myCode')
// Fage method block
const createPost = {
path: 'postsCreate',
fns: [
ctx => chkPermissions(ctx.meta.scopes),
ctx => validateInputs(ctx.input),
ctx => formatData(ctx.input),
(ctx, formatted) => dbSave(formatted)
(ctx, created) => { log('posted', created.id); return created }
]
}
const getRandomPosts = {
path: 'postsGet',
fns: [ctx => dbGet('posts')]
}
// Bundle your Fage
const app = Fage([getPosts, createPost])
module.exports = app
And then writing up a very basic HTTP interface:
const express = require('express')
const {authenticator} = require('./myAuthCode')
const app = require('./fageBundle')
const server = new express()
server.use(authenticator) // Assume attaches `user` to `req.user`
// Create your endpoint logic
server.get('/posts', (req, res) => {
app.postsGet(null, {limit: 10, scopes: req.user.scopes})
.then(posts => res.send(posts))
})
server.post('/posts', (req, res) => {
app.postsCreate(req.body, {user: req.user})
.then(created => res.send(created))
})
server.listen(5000)
Native to Node 6+
Written using Node 6+ compatible ES6, specfically to run natively (i.e. without needing transpilation). Note that if you are using async/await
notation in your app design, you will need to be running Node 7.6+.
FAQs
Declarative middleware runner
We found that @likelytheory/fage demonstrated a not healthy version release cadence and project activity because the last version was released a year ago. It has 1 open source maintainer 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
Socket uncovers malicious Rust crates impersonating fast_log to steal Solana and Ethereum wallet keys from source code.
Research
A malicious package uses a QR code as steganography in an innovative technique.
Research
/Security News
Socket identified 80 fake candidates targeting engineering roles, including suspected North Korean operators, exposing the new reality of hiring as a security function.