draft
optional
kind:1337
tag:n:import
tag:n:metadata
Note
The key words MUST, MUST NOT, REQUIRED, SHALL, SHALL NOT, SHOULD, SHOULD NOT, RECOMMENDED, MAY, and OPTIONAL in this document are to be interpreted as described in RFC 2119.
- Motivation
- Short Description
- Overview
- Preliminary Definitions
- Nomad Syntax
- Nomad Semantics
- FAQs
- Philosophical Musings
This proposal aims to provide a NOSTR-powered uncensorable, decentralized, and highly available application repository framework.
A new event kind is reserved for the publication of JavaScript source code. Events of this kind can in turn declare other such events as dependencies, thus enabling behavioral composition. Additionally, they may have associated metadata, so as to tailor their interpretation by complying clients.
The code therein is expected to be executed in a secure, curated, and idempotent execution environment, a reference implementation of which is provided as part of this proposal.
First, we deal with some Preliminary Definitions.
Next, we present the Nomad Syntax, the mechanism by which users may publish code to relays to make it available to consumers.
Next, we define the Nomad Semantics: the expected semantics for code execution, importing, and metadata handling.
We follow with a series of FAQs, and close with some Philosophical Musings.
A couple of appendixes is included as well:
- Appendix A deals with the predefined dependencies that conforming implementations need to provide to Nomad scripts.
- Appendix B lists the standard global objects that conforming implementations need to make available to Nomad code.
- Appendix C specifies the virtual machine interface proper, with all of its exposed functionalities.
In what follows, we'll define some terms to be used throughout the specification.
A simple identifier MUST be a string containing a valid JavaScript identifier name, satisfying the following regular expression: /^[a-zA-Z][_a-zA-Z0-9]*$/
.
Additionally, this value MUST NOT be:
- a JavaScript reserved word (including strict mode reserved words and reserved words in module code or async function bodies), nor
- a future reserved word (including future reserved words in older standards), nor
- an identifier with special meaning, nor
- the names of standard built-in objects.
Note
Note that, in particular, identifiers as such defined MAY NOT start with an underscore (_
) nor contain any dollar signs ($
).
Note as well, that although JavaScript identifiers may contain any arbitrary Unicode code points or be specified by escape sequences, this is NOT allowed here.
Furthermore, the forbidden identifier names boil down to: AggregateError
, Array
, ArrayBuffer
, AsyncFunction
, AsyncGenerator
, AsyncGeneratorFunction
, AsyncIterator
, Atomics
, BigInt
, BigInt64Array
, BigUint64Array
, Boolean
, DataView
, Date
, Error
, EvalError
, FinalizationRegistry
, Float32Array
, Float64Array
, Function
, Generator
, GeneratorFunction
, Infinity
, Int16Array
, Int32Array
, Int8Array
, InternalError
, Intl
, Iterator
, JSON
, Map
, Math
, NaN
, Number
, Object
, Promise
, Proxy
, RangeError
, ReferenceError
, Reflect
, RegExp
, Set
, SharedArrayBuffer
, String
, Symbol
, SyntaxError
, TypeError
, URIError
, Uint16Array
, Uint32Array
, Uint8Array
, Uint8ClampedArray
, WeakMap
, WeakRef
, WeakSet
, abstract
, arguments
, as
, async
, await
, boolean
, break
, byte
, case
, catch
, char
, class
, const
, continue
, debugger
, decodeURI
, decodeURIComponent
, default
, delete
, do
, double
, else
, encodeURI
, encodeURIComponent
, enum
, escape
, eval
, eval
, export
, extends
, false
, final
, finally
, float
, for
, from
, function
, get
, globalThis
, goto
, if
, implements
, import
, in
, instanceof
, int
, interface
, isFinite
, isNaN
, let
, long
, native
, new
, null
, of
, package
, parseFloat
, parseInt
, private
, protected
, public
, return
, set
, short
, static
, super
, switch
, synchronized
, this
, throw
, throws
, transient
, true
, try
, typeof
, undefined
, unescape
, var
, void
, volatile
, while
, with
, and yield
.
A simple body MUST be a string consisting of valid JavaScript asynchronous function body code (ie. code that may legally be used as the sole argument to the standard-but-indirectly-available AsyncFunction
constructor) under strict mode (ie. preceded by "use strict";
), and most likely SHOULD contain at least one return
statement.
Alternatively, if body
contains the prospective simple body, the following JavaScript code MUST NOT throw:
new (Object.getPrototypeOf(async function () {}).constructor)(`"use strict"; ${body}`);
Warning
Note that a simple body is not expected to be a Generator
, thus, you MAY NOT use yield
within its global scope.
Incidentally, you MAY use yield
in an internally-defined function within the body proper.
Furthermore, a simple body MUST consist solely of the following byte values:
0x09
,0x0a
,0x0c
,0x0d
, or0x20
to0x7e
.
These are the ASCII values of:
- horizontal tab,
- line feed,
- form feed,
- carriage return, and
- all printable characters.
Note
As is the case above, whereas JavaScript allows for arbitrary code points to be a part of a function body, we do NOT: if they're required, they can be built up from escape sequences.
A simple path MUST be a string consisting of one or more simple identifiers separated by slashes (/
).
A Nomad event is a kind:1337
event of the form:
{
...,
"kind": 1337,
...,
"tags": [
...,
[
"n:import",
"{identifier}",
"{nomad_event_id}",
"{recommended_relay_url}", // optional
], // optional
...,
[
"n:metadata",
"{identifier}",
"{arg[1]}", "{arg[2]}", ..., "{arg[n]}", // optional
], // optional
...,
],
...,
"content": ...,
...,
}
Note that 1337
is an enumerated kind, meaning it is a regular event: relays MUST store and return them to clients for an indefinite amount of time.
Furthermore, kind:1337
events are non-deletable events1: upon receiving a kind:5
event targeting a kind:1337
event, relays MUST ignore it and not forward it.
A Nomad event's .tag
field MAY contain any number of n:import
or n:metadata
tags.
A Nomad event's .content
field MUST be a simple body; we'll call the .content
of a Nomad event a Nomad script or simply a Nomad.
An n:import
tag MUST have the following form:
[
"n:import",
"{identifier}",
"{nomad_event_id}",
"{recommended_relay_url}", // optional
]
where:
-
{identifier}
: This value MUST be a simple identifier. -
{nomad_event_id}
: This value MUST be a valid NOSTR event.id
, consisting of 64 lowercase hexadecimal characters. This MUST be the.id
of a Nomad event (ie. akind:1337
event itself). -
{recommended_relay_url}
: This value, if present, MUST be a valid NOSTR relay URL.
If two instances of this tag have the same {identifier}
, then they MUST have the same {nomad_event_id}
field, but may differ in their {recommended_relay_url}
fields.
On the other hand, the same {nomad_event_id}
MAY appear more than once, even on tags with different {identifier}
values.
An n:metadata
tag MUST have the following form:
[
"n:metadata",
"{identifier}",
"{arg[1]}", "{arg[2]}", ..., "{arg[n]}", // optional
]
where:
-
{identifier}
: This value MUST be a simple identifier. -
{arg[1]}
,{arg[2]}
, ...,{arg[n]}
: These MUST be zero or more arbitrary strings.
If two instances of this tag have the same {identifier}
, then they MUST also have the same list of arguments (byte-by-byte equal, that is).
The {identifier}
is expected to serve as a way of modifying the execution mode for the Nomad event it is attached to.
The current specification defines the following metadata identifiers:
predefined
: used for predefined Nomad dependencies.internal
: used in the general execution procedure.external
: used in the general execution procedure, but also as a semantic marker of sorts.
Future specifications may further define additional metadata {identifier}
values in order to tailor how events are validated and interpreted.
Tip
In order to avoid future conflicts, it is RECOMMENDED that non-standardized n:metadata
identifiers start with x-
, so as to make absolutely apparent the fact that they're intended for experimental or non-standard purposes.
A predefined
metadata identifier has the form:
["n:metadata", "predefined". "{predefined_name}"]
where:
{predefined_name}
: This value MUST be a simple path.
See the general execution procedure for its intended semantics.
An internal
metadata identifier has the form (note the absence of arguments):
["n:metadata", "internal"]
See the general execution procedure for its intended semantics.
An external
metadata identifier has the form (note the absence of arguments):
["n:metadata", "external"]
See the general execution procedure for its intended semantics.
In order for a NOSTR event to be a valid Nomad event, it MUST be a valid NOSTR event, and furthermore the following conditions need to be satisfied:
- The
.kind
field MUST equal1337
. - For any two
n:import
tags in.tags
with identical{identifier}
parts their{nomad_event_id}
parts MUST be equal (but their{recommended_relay_url}
parts MAY differ). - For any two
n:metadata
tags in.tags
with identical{identifier}
parts their arguments MUST be identical (byte-by-byte equal). - For every
n:import
tag in.tags
:- Their
{nomad_event_id}
part MUST be the.id
field of a NOSTR event that is itself valid according to these rules. - Their
{identifier}
part MUST be a valid simple identifier. - Their
{recommended_relay_url}
part MUST be a syntactically valid Secure WebSocket URL.
- Their
- For every
n:metadata
tag in.tags
:- Their
{identifier}
part MUST be a valid simple identifier. - The event as a whole MUST be valid according to the validation rules the metadata definition for
{identifier}
establishes.
- Their
- The
.content
field MUST be a valid simple body.
Note that this is simply a syntactic validation: Nomad events may still be invalid by virtue of their dynamic behavior.
Execution of Nomad event with id id
(a string
), querying relays
(a list of string
s), and parameters params
(a mapping from string
s to arbitrary values), comes in two flavours:
- external: denoted as
executeExternal(id, relays, params)
, it's the main execution form, used when executing a Nomad script on the "outer" scope. - internal: denoted as
executeInternal(id, relays, params)
, used to execute imported and predefined scripts.
Both of these are very similar, but differ in their handling the resulting value.
To that end, we define the toJsonResult
procedure first, that takes an arbitrary data
parameter and proceeds as follows:
Let
result
be the result of executing:JSON.stringify(data);If the previous operation threw, return
FAILURE
.If
result
isundefined
, returnFAILURE
.Return
result
.
Now, we'll treat each execution procedure in turn.
An executeExternal(id, relays, params)
call proceeds as follows:
Query
relays
for the event with idid
, call the resulte
.If
e
is empty, returnFAILURE
.if
e
is not valid according to the validation procedure above, returnFAILURE
.If
e.tags
does not contain ann:metadata
tag with anexternal
metadata identifier, returnFAILURE
.If
e.tags
contains ann:metadata
tag with apredefined
metadata identifier, then:
- If
id
is not recognized as a predefined dependency by the executing agent, returnFAILURE
.- Let
result
be the result of executing the predefined dependency.- Return the result of calling
toJsonResult(result)
.Let
imports
be a new empty mapping withstring
keys and arbitrary values.For each
n:import
tag ine.tags
, call itt
, do:
- Let
name
bet[1]
(ie, the{identifier}
part of ann:import
tag).- If
name
is a key inparams
, returnFAILURE
.- Let
iid
bet[2]
(ie. the{nomad_event_id}
part of ann:import
tag).- Let
rrelays
be[t[3]]
(ie. the{recommended_relay_url}
part of ann:import
tag), or[]
ift[3]
does not exist.- Let
value
be the result of callingexecuteInternal(iid, [...rrelays, ...relays], {})
.- If
value
isFAILURE
, returnFAILURE
.- Assign
value
toimports[name]
.Let
body
be the result of prepending'"use strict";'
toe.content
.Let
importedNames
be the keys ofimports
, andimportedValues
their corresponding values.Let
paramNames
be the keys ofparams
, andparamValues
their corresponding values.Let
result
be the result of executing:await (new AsyncFunction( ...importedNames, ...paramNames, body, ))( ...importedValues, ...paramValues, );If the previous operation threw, return
FAILURE
.Return the result of executing
toJsonResult(result)
.
An executeInternal(id, relays, params)
call proceeds very similarly:
Query
relays
for the event with idid
, call the resulte
.If
e
is empty, returnFAILURE
.if
e
is not valid according to the validation procedure above, returnFAILURE
.If
e.tags
does not contain ann:metadata
tag with aninternal
metadata identifier, returnFAILURE
.If
e.tags
contains ann:metadata
tag with apredefined
metadata identifier, then:
- If
id
is not recognized as a predefined dependency by the executing agent, returnFAILURE
.- Return the result of executing the predefined dependency.
Let
imports
be a new empty mapping withstring
keys and arbitrary values.For each
n:import
tag ine.tags
, call itt
, do:
- Let
name
bet[1]
(ie, the{identifier}
part of ann:import
tag).- If
name
is a key inparams
, returnFAILURE
.- Let
iid
bet[2]
(ie. the{nomad_event_id}
part of ann:import
tag).- Let
rrelays
be[t[3]]
(ie. the{recommended_relay_url}
part of ann:import
tag), or[]
ift[3]
does not exist.- Let
value
be the result of callingexecuteInternal(iid, [...rrelays, ...relays], {})
.- If
value
isFAILURE
, returnFAILURE
.- Assign
value
toimports[name]
.Let
body
be the result of prepending'"use strict";'
toe.content
.Let
importedNames
be the keys ofimports
, andimportedValues
their corresponding values.Let
paramNames
be the keys ofparams
, andparamValues
their corresponding values.Let
result
be the result of executing:await (new AsyncFunction( ...importedNames, ...paramNames, body, ))( ...importedValues, ...paramValues, )If the previous operation threw, return
FAILURE
.Return
result
.
Conforming agents MAY execute a Nomad event.
This is done by spinning up a Nomad Virtual Machine, and submitting the Nomad event in question for execution, taking n:import
tags into account.
Any number of named parameters MAY be passed to it, and differing n:metadata
identifiers MAY stipulate for specific parameter names to be passed in specific execution contexts.
Abstractly, a Nomad execution procedure interface looks like:
type JsonAble = number | string | boolean | null | JsonAble[] | { [_: string]: JsonAble };
declare function execute(nomadEvent: object, parameters: { [_: string]: JsonAble }): JsonAble;
Where the nomadEvent
parameter receives a whole NOSTR event, and the parameters
parameter receives a mapping from argument names to argument values.
The arguments given MUST be serializable as JSON, so as to be able to be communicated to the Nomad Virtual Machine running the Nomad script proper.
Upon a Nomad event e being submitted for execution, the following steps take place:
-
Import Closure / Validation: the
n:import
tags found on e are scanned, the event ids therein mentioned collected, and this process is recursively repeated for every Nomad event mentioned; all collected events are themselves validated. If given,{recommended_relay_url}
values MAY be used first (repeated{identifier}
values with matching{nomad_event_id}
values representing multiple relay URLs to query for the same import).If any of these collections fails (ie. because the relays used have no knowledge of the required event), execution as a whole fails.
The result of this procedure is a DAG specifying all the Nomad events collected and related to each other via their import relations.
-
Topological Import Sorting: the Nomad events collected in the previous step are topologically sorted, using the events'
.id
fields' numerical values as a tie-breakers.This will yield a linear ordering of all the Nomad events with e in the last position.
Finally, every Nomad event already installed in the Nomad Virtual Machine is removed from this list, so as to only keep "new" events in it.
-
VM Enclosure Creation: the Nomad Virtual Machine is able to isolate a set of Nomad events so as to limit their interaction with already installed Nomads. This step stipulates the creation of one such temporary "enclosure" so as to limit Nomad interactions during set-up.
-
Dependencies Installation: all but the last event in the list (ie. e) are now installed into the created enclosure. Installation entails execution and caching of the result, so as to make it available to importing Nomads.
-
Script Execution: now e is ready to be executed; based on e's
n:metadata
tags, the actual execution mode may change somewhat, but it will eventually entail asking the Nomad Virtual Machine to execute e's.content
within the enclosure created above, and report the return value as the ultimate result altogether, passing any given parameters forward.
An internal
metadata identifier has the form (note the absence of arguments):
["n:metadata", "internal"]
This identifier is intended to signal that the result of executing this Nomad event's .content
field is not JSON-able, and to be used exclusively as an import by other Nomad events in their n:import
tags.
If the Nomad event being executed at the "top level" (ie. e above) contains an internal
metadata identifier, the execution procedure MUST stop immediately.
Likewise, an external
metadata identifier has the form:
["n:metadata", "external"]
This identifier signals that the result of executing this Nomad event's .content
field is JSON-able.
If the Nomad event being executed at the "top level" (ie. e above) does not contain an external
metadata identifier, the execution procedure MAY stop immediately.
When a Nomad event contains a valid n:import
tag with {identifier}
x and {nomad_event_id}
i, the event's .content
will be executed in an environment that contains a local variable named x, the contents of which will be the frozen result of executing the Nomad event indicated by i.
By mean of example, consider this Nomad event:
{
"id": "0101010101010101010101010101010101010101010101010101010101010101",
...,
"kind": 1337,
"tags": [
["n:metadata", "internal"]
],
"content": "return { hello: (x) => `Hello ${x}!!`, goodbye: (x) => `Goodbye ${x}!!` };",
...,
}
Now, let's use it as a import in a new Nomad event:
{
"id": "0202020202020202020202020202020202020202020202020202020202020202",
...,
"kind": 1337,
"tags": [
["n:import", "say", "0101010101010101010101010101010101010101010101010101010101010101"],
["n:metadata", "external"]
],
"content": "let sayHello = say.hello('foo'); let sayGoodbye = say.goodbye('bar'); return `${sayHello}...${sayGoodbye}`;",
...,
}
Upon executing this second Nomad event, the result should be a single string:
"Hello foo!!...Goodbye bar!!"
Note how the variable say
needed not be defined in the Nomad script's body, but was rather provided by the Nomad runtime.
Note as well, how the first Nomad event is free to return whatever it wishes to, since it is merely being used as an import dependency, there's no restriction on its result being JSON-able; on the contrary, the second Nomad event is forced to return a JSON-able result, for it must be communicated to the host system via JSON encoding.
In a similar vein, certain n:metadata
identifiers MAY require the Nomad runtime (or the framework using the Nomad runtime) to pass additional parameters to the Nomad event being executed.
Parameters in this sense are always named: ie. named parameters are used exclusively, and each n:metadata
identifier MUST specify the names expected to be used.
By way of example, consider the following Nomad event (nb. the x-with-current-time
is merely an example and inconsequential):
{
"id": "0303030303030303030303030303030303030303030303030303030303030303",
...,
"kind": 1337,
"tags": [
["n:metadata", "x-with-current-time"],
["n:metadata", "external"]
],
"content": "return currentTime.toString();",
...,
}
Note how the variable currentTime
is neither defined within the Nomad script's body, nor imported via an n:import
tag, it is rather provided by the Nomad runtime because of the x-with-current-time
identifier in the n:metadata
tag.
There's more that can be done with the Nomad runtime as it stands today, and, this being a living standard, it is expected to evolve from its current status.
...
...
...
...
...
...
...
Footnotes
-
There's a precedent for non-deletable events in NIP-09's "Deleting a Deletion" section. ↩