🛡️ The Inox programming language is your shield against complexity.
Inox is released as a single binary that contains all you need to do full-stack development.
Inox deeply integrates with its built-in database engine, testing engine and HTTP server.
Zero config. Zero boilerplate. Secure by Default.
Web Dev Features
- XML Expressions (HTML)
- HTTP Server - Filesystem Routing
- Built-in Database
- Project and Virtual Filesystem
- Built-in Browser Automation
Security Features
Other Features
👥 Discord Server: https://discord.gg/53YGx8GzgE
📖 Language Reference: docs/language-reference.md
I am working full-time on Inox, please consider donating. Thanks !
Inox can be used on any operating system by installing the VSCode extension.
If you are using Linux, an archive with a binary and some examples is available in the release assets
- uncompress the archive
- install the
inox
binary to/usr/local/bin
sudo install ./inox -o root -m 0755 /usr/local/bin/inox
If you want to compile the language yourself go here.
- VSCode : LSP, Debug, colorization, snippets
You can learn Inox directly in VSCode: download the extension and follow the explanations.
Inox can be used for scripting & provides a shell. The development of the language in those domains is not very active because Inox primarily focuses on Web Application Development.
To learn scripting go here. View Shell Basics to learn how to use Inox interactively.
📖 Language Reference: docs/language-reference.md
HTML elements can be created without imports using the built-in html namespace and a JSX-like syntax:
manifest {}
return html<div> Hello world ! </div>
Inox comes with a built-in HTTP server that supports filesystem routing:
# main.ix
const (
HOST = https://localhost:8080
)
manifest {
permissions: {
provide: HOST
read: %/...
}
}
server = http.Server!(HOST, {
routing: {
static: /static/
dynamic: /routes/
}
})
For maximum security, each request is processed in an isolated module:
# /routes/api/POST-users.ix
manifest {
parameters: {
# JSON body parameter
name: {
pattern: %str
}
}
permissions: {
create: %https://internal-service/users/...
}
}
username = mod-args.name
...
Inox includes an embedded database engine. Databases are described in the manifest at the top of the module:
manifest {
permissions: {
read: %/...
write: %/...
}
databases: {
main: {
resource: ldb://main #ldb stands for Local Database
resolution-data: /databases/main/
expected-schema-update: true
}
}
}
# define the pattern for user data
pattern user = {
name: str
}
dbs.main.update_schema(%{
users: Set(user, #url)
})
Objects can be directly added to and retrieved from the database.
This is made possible by the fact that most Inox types are constrained to be serializable.
new_user = {name: "John"}
dbs.main.users.add(new_user)
# true
dbs.main.users.has(new_user)
You can learn more here.
The database currently uses a single-file key-value store, it obviously cannot handle hundreds of Gigabytes.
The improvement of the database engine is a main focus point. The goal is to have a DB engine that is aware of the code accessing it (HTTP request handlers) in order to smartly pre-fetch and cache data.
Most Inox types (objects, lists, Sets) are serializable so they cannot contain transient values.
object = {
# error: non-serializable values are not allowed as initial values of properties
lthread: go do {
return 1
}
}
# same error
list = [
go do { return 1 }
]
The transient counterparts of objects are structs (not implemented yet).
struct Task {
name: str
}
task1 = Task{name: "0"}
task2 = Task{name: "1"}
array = Array(task1, task2)
The Inox binary provides a project server that the developer connects to with VsCode.
It enables the developer to develop, test, deploy & manage secrets, all from VsCode.
The project server will also provide automatic infrastructure management in the near future.
graph TD
A(Project Server) <-->|Development| B(VsCode)
A -->|Manages| C(Infrastructure)
subgraph A[Project Server]
A1(LSP Server) -->|Invocation & Debug| A2(Inox Runtime)
A1 --> A3(Project)
end
Link to the Inox Runtime Architecture
An Inox project lives in a virtual filesystem (container) for better security & reproducibility. Note that this virtual filesystem only exists in-process, there is no FUSE filesystem and Docker is not used.
h = chrome.Handle!()
h.nav https://go.dev/
node = h.html_node!(".Hero-blurb")
h.close()
Inox features a fine-grained permission system that restricts what a module is allowed to do, here are a few examples of permissions:
- access to the filesystem (read, create, update, write, delete)
- access to the network (several distinct permissions)
- HTTP (read, create, update, delete, listen)
- Websocket (read, write, listen)
- DNS (read)
- Raw TCP (read, write)
- access to environment variables (read, write, delete)
- create coroutines (multi threading)
- execute specific commands
Inox modules always start with a manifest that describes the required permissions.
Attempting to perform a forbidden operation raises an error:
core: error: not allowed, missing permission: [read path(s) /home/]
In imports the importing module specifies the permissions it grants to the imported module.
./app.ix
manifest {
permissions: {
read: %/...
create: {threads: {}}
}
}
import lib ./malicious-lib.ix {
arguments: {}
allow: {
read: %/tmp/...
}
}
./malicious-lib.ix
manifest {
permissions: {
read: %/...
}
}
data = fs.read!(/etc/passwd)
If the imported module asks more permissions than granted an error is thrown:
import: some permissions in the imported module's manifest are not granted: [read path(s) /...]
In addition to the checks performed by the permission system, the inox binary uses Landlock to restrict file access for the whole process and its children.
Sometimes programs have an initialization phase, for example a program reads a file or performs an HTTP request to fetch its configuration. After this phase it no longer needs some permissions so it can drop them.
drop-perms {
read: %https://**
}
Limits limit intensive operations, there are three kinds of limits: byte rate, simple rate & total. They are defined in the manifest and are shared with the children of the module.
manifest {
permissions: {
...
}
limits: {
"fs/read": 10MB/s
"http/req": 10x/s
}
}
By default strict limits are applied on HTTP request handlers in order to mitigate some types of DoS.
Secrets are special Inox values, they can only be created by defining an environment variable with a pattern like %secret-string or by storing a project secret.
- The content of the secret is hidden when printed or logged.
- Secrets are not serializable so they cannot be included in HTTP responses.
- A comparison involving a secret always returns false.
manifest {
...
env: %{
API_KEY: %secret-string
}
...
}
API_KEY = env.initial.API_KEY
This feature is very much work in progress.
Excessive Data Exposure occurs
when an HTTP API returns more data than needed, potentially exposing sensitive information.
In order to mitigate this type of vunerability the serialization of Inox values involves the concepts of value visibility and
property visibility.
Let's take an example, here is an Inox object:
{
non_sensitive: 1,
x: [email protected], # email address type
age: 30,
passwordHash: "x"
}
The serialization of the object will not include properties having a sensitive name or a sensitive value:
{
"non_sensitive": 1
}
The visibility of properties can be configured using the _visibility_
metaproperty.
{
_visibility_ {
{
public: .{passwordHash}
}
}
passwordHash: "x"
}
ℹ️ In the near future the visibility will be configurable directly in patterns & database schemas.
Inox's HTTP Server has an embedded rate limiting engine. It is pretty basic at the moment, but it will make decisions based on logs & access patterns in the near future.
In Inox interpolations are always restricted in order to prevent injections and regular strings are never trusted. URLs & paths are first-class values and must be used to perform network or filesystem operations.
When you dynamically create URLs the interpolations are restricted based on their location (path, query).
https://example.com/{path}?a={param}
In short, most malicious path
and param
values provided by a malevolent user will cause an error at runtime.
Click for more explanations.
Let's say that you are writing a piece of code that fetches public data from a private/internal service and returns the result
to a user. You are using the query parameter ?admin=false
in the URL because only public data should be returned.
public_data = http.read!(https://private-service{path}?admin=false)
The way in which the user interacts with your code is not important here, let's assume that the user can send any value for path
.
Obviously this is a very bad idea from a security standpoint.
A malicious path could be used to:
- perform a directory traversal if the private service has a vulnerable endpoint
- inject a query parameter
?admin=true
to retrieve private data - inject a port number
In Inox the URL interpolations are special, based on the location of the interpolation specific checks are performed:
https://example.com/api/{path}/?x={x}
- interpolations before the
'?'
are path interpolations- the strings/characters
'..'
,'\\'
,'?'
and'#'
are forbidden - the URL encoded versions of
'..'
and'\\'
are forbidden ':'
is forbidden at the start of the finalized path (after all interpolations have been evaluated)
- the strings/characters
- interpolations after the
'?'
are query interpolations- the characters
'&'
and'#'
are forbidden
- the characters
In the example if the path /data?admin=true
is received the Inox runtime will throw an error:
URL expression: result of a path interpolation should not contain any of the following substrings: "..", "\" , "*", "?"
lthread = go {} do {
print("hello from goroutine !")
return 1
}
# 1
result = lthread.wait_result!()
group = LThreadGroup()
lthread1 = go {group: group} do read!(https://jsonplaceholder.typicode.com/posts/1)
lthread2 = go {group: group} do read!(https://jsonplaceholder.typicode.com/posts/2)
results = group.wait_results!()
Lifetime jobs are lthreads linked to an object.
object = {
lifetimejob #handle-messages {
for msg in watch_received_messages(self){
# handle messages
}
}
}
Inox comes with many built-in functions for:
- browser automation
- file manipulation
- HTTP resource manipulation
- data container constructors (Graph, Tree, ...)
CLI parameters & environment variables can be described in the manifest:
manifest {
parameters: {
# positional parameters are listed at the start
{
name: #dir
pattern: %path
rest: false
description: "root directory of the project"
}
# non positional parameters
clean-existing: {
pattern: %bool
default: false
description: "if true delete <dir> if it already exists"
}
}
env: {
API_KEY: %secret-string
}
permissions: {
write: IWD_PREFIX # initial working directory
delete: IWD_PREFIX
}
}
# {
# "dir": ...
# "clean-existing": ...
# }
args = mod-args
API_KEY = env.initial.API_KEY
$ inox run test.ix
not enough CLI arguments
usage: <dir path> [--clean-existing]
required:
dir: %path
root directory of the project
options:
clean-existing (--clean-existing): boolean
if true delete <dir> if it already exists
Inox allows you to attach a transaction to the current execution context (think SQL transactions). When a side effect happens it is recorded in the transaction. If the execution is cancelled for whatever reason the transaction is automatically rollbacked and reversible effects are reversed. Some effects such as database changes are only applied when the transaction is committed.
tx = start_tx()
# effect
fs.mkfile ./file.txt
# rollback transaction --> delete ./file.txt
cancel_exec()
ℹ️ A transaction is created for each HTTP request.
- clone this repository
cd
into the directory- run
go build cmd/inox/inox.go
Lexter |
Datamix.io |