nervur 0.13.0 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +2 -2
- package/README.md +151 -456
- package/package.json +12 -13
- package/src/being.js +125 -0
- package/src/contract.js +161 -0
- package/src/ground.js +273 -0
- package/src/index.js +5 -0
- package/src/program.js +74 -0
- package/src/projection.js +102 -0
- package/GETTING-STARTED.md +0 -111
- package/NOTICE +0 -2
- package/SECURITY.md +0 -11
- package/admin.mjs +0 -83
- package/being.mjs +0 -594
- package/blueprint.mjs +0 -462
- package/client.mjs +0 -70
- package/contract.mjs +0 -98
- package/ground.mjs +0 -381
- package/index.mjs +0 -20
- package/modules.mjs +0 -55
- package/pointer.mjs +0 -79
- package/program.mjs +0 -118
- package/tools.mjs +0 -162
- package/voice.mjs +0 -107
package/src/program.js
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { contract as read } from './contract.js';
|
|
2
|
+
|
|
3
|
+
// A contract and its resolvers, made callable. This is the whole compiler, and
|
|
4
|
+
// beings and organs both come out of it, because they are one kind of code: a
|
|
5
|
+
// field of an organ is a resolver exactly as a field of a being is, and nothing
|
|
6
|
+
// here can tell which it is holding. There is no second compiler anywhere, and
|
|
7
|
+
// a design that wants one is wrong.
|
|
8
|
+
//
|
|
9
|
+
// A resolver is sovereign. It sees its arguments by the names its own contract
|
|
10
|
+
// gave them, it reaches the world only through the context it was handed, and
|
|
11
|
+
// it never throws — a resolver that raises is answered as silence, because
|
|
12
|
+
// nothing in this system promises that an ask is fulfilled. A refusal a caller
|
|
13
|
+
// must tell apart from silence is carried in the answer type instead.
|
|
14
|
+
|
|
15
|
+
// Exactly the arguments the contract declares, and nothing else. What a caller
|
|
16
|
+
// may ask is what the contract already showed it, so a parameter nobody
|
|
17
|
+
// declared cannot ride in beside one that was.
|
|
18
|
+
const given = (args, params) =>
|
|
19
|
+
Object.freeze(
|
|
20
|
+
Object.fromEntries(
|
|
21
|
+
Object.keys(args)
|
|
22
|
+
.filter(
|
|
23
|
+
(name) =>
|
|
24
|
+
params !== null &&
|
|
25
|
+
typeof params === 'object' &&
|
|
26
|
+
Object.hasOwn(params, name) &&
|
|
27
|
+
params[name] !== undefined,
|
|
28
|
+
)
|
|
29
|
+
.map((name) => [name, params[name]]),
|
|
30
|
+
),
|
|
31
|
+
);
|
|
32
|
+
|
|
33
|
+
const complete = (args, only) =>
|
|
34
|
+
Object.entries(args).every(([name, one]) => !one.required || only[name] !== undefined);
|
|
35
|
+
|
|
36
|
+
export function program(one, resolvers) {
|
|
37
|
+
const held = typeof one === 'string' ? read(one) : one;
|
|
38
|
+
|
|
39
|
+
return Object.freeze({
|
|
40
|
+
// What may be asked here, which is the only place to find out. A field this
|
|
41
|
+
// does not name does not exist for a caller.
|
|
42
|
+
contract: held,
|
|
43
|
+
|
|
44
|
+
answer: async (field, params, context) => {
|
|
45
|
+
// Read as an own property, never through the prototype chain. Without
|
|
46
|
+
// this, `__proto__`, `constructor` and `toString` all name a field nobody
|
|
47
|
+
// declared, and a caller reaches something the contract never showed it.
|
|
48
|
+
const declared = Object.hasOwn(held.fields, field) ? held.fields[field] : undefined;
|
|
49
|
+
if (declared === undefined) return undefined;
|
|
50
|
+
|
|
51
|
+
const only = given(declared.args, params);
|
|
52
|
+
// A field short of an argument its contract made mandatory is not called
|
|
53
|
+
// at all. A resolver written against its own contract may read that
|
|
54
|
+
// argument without checking, and calling it anyway would make every
|
|
55
|
+
// resolver defensive about a promise the contract already made.
|
|
56
|
+
if (!complete(declared.args, only)) return undefined;
|
|
57
|
+
|
|
58
|
+
const resolver =
|
|
59
|
+
resolvers !== null &&
|
|
60
|
+
typeof resolvers === 'object' &&
|
|
61
|
+
Object.hasOwn(resolvers, field) &&
|
|
62
|
+
typeof resolvers[field] === 'function'
|
|
63
|
+
? resolvers[field]
|
|
64
|
+
: undefined;
|
|
65
|
+
if (resolver === undefined) return undefined;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
return await resolver(only, context);
|
|
69
|
+
} catch {
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
},
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Refusal, print } from '@quo-systems/js';
|
|
2
|
+
|
|
3
|
+
// The boundary, and there is exactly one of it. Inside a ground a contract is
|
|
4
|
+
// GraphQL; on the wire there is Quo and only Quo. A stranger implementing the
|
|
5
|
+
// protocol in another language has never heard of Nervur and must still read
|
|
6
|
+
// the blueprint a being stands on, so a contract is projected into the kit's
|
|
7
|
+
// own notation here and nowhere else.
|
|
8
|
+
//
|
|
9
|
+
// The projection never guesses. Where GraphQL says something the notation has
|
|
10
|
+
// no word for, the contract is refused at build — a being that stood on a
|
|
11
|
+
// blueprint that quietly dropped half its schema would have a digest that lied
|
|
12
|
+
// to everyone who read it.
|
|
13
|
+
|
|
14
|
+
const refuse = (code, detail) => {
|
|
15
|
+
throw new Refusal(code, detail);
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// The closed set of scalars is the law's, so this table is closed too. A name
|
|
19
|
+
// missing from it is not a scalar Nervur may invent — it is a word Quo does
|
|
20
|
+
// not have.
|
|
21
|
+
const SCALARS = Object.freeze({
|
|
22
|
+
Boolean: 'bool',
|
|
23
|
+
Int: 'int',
|
|
24
|
+
String: 'text',
|
|
25
|
+
Bytes: 'bytes',
|
|
26
|
+
B32: 'b32',
|
|
27
|
+
Being: 'being',
|
|
28
|
+
Invitation: 'invitation',
|
|
29
|
+
Card: 'card',
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// The one word the notation has and GraphQL does not: a field that answers
|
|
33
|
+
// nothing, which rides as zero bytes and is a real answer. GraphQL insists
|
|
34
|
+
// every field has a type, so a contract spells it with this name in answer
|
|
35
|
+
// position. It is not a type and may not be asked for or carried.
|
|
36
|
+
const NOTHING = 'Nothing';
|
|
37
|
+
|
|
38
|
+
// GraphQL is nullable by default and says so by omission; the notation is
|
|
39
|
+
// required by default and says the other thing with `?`. This is the whole of
|
|
40
|
+
// the difference and it is spelled out rather than inferred.
|
|
41
|
+
function projectType(node, where, records) {
|
|
42
|
+
if (node.list) {
|
|
43
|
+
return node.required
|
|
44
|
+
? { list: projectType(node.element, where, records) }
|
|
45
|
+
: { optional: { list: projectType(node.element, where, records) } };
|
|
46
|
+
}
|
|
47
|
+
const base = SCALARS[node.of] ?? (records.has(node.of) ? node.of : undefined);
|
|
48
|
+
if (base === undefined) {
|
|
49
|
+
refuse(
|
|
50
|
+
'NO_WORD',
|
|
51
|
+
node.of === NOTHING
|
|
52
|
+
? `${NOTHING} at ${where}, which is not a type`
|
|
53
|
+
: `${node.of} at ${where}`,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
return node.required ? { base } : { optional: { base } };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const projectArgs = (args, where, records) =>
|
|
60
|
+
Object.entries(args).map(([name, one]) => ({
|
|
61
|
+
name,
|
|
62
|
+
type: projectType(one, `${where}(${name})`, records),
|
|
63
|
+
}));
|
|
64
|
+
|
|
65
|
+
// A record block holds plain fields: a name and a type, and nothing else. A
|
|
66
|
+
// GraphQL object type whose fields take arguments is a type with behaviour,
|
|
67
|
+
// which the notation has no word for at all.
|
|
68
|
+
const projectRecord = (name, fields, records) =>
|
|
69
|
+
Object.freeze({
|
|
70
|
+
name,
|
|
71
|
+
fields: Object.entries(fields).map(([field, one]) => {
|
|
72
|
+
if (Object.keys(one.args).length > 0) refuse('NO_WORD', `arguments on ${name}.${field}`);
|
|
73
|
+
if (one.answers.of === NOTHING) refuse('NO_WORD', `${NOTHING} at ${name}.${field}`);
|
|
74
|
+
return { name: field, type: projectType(one.answers, `${name}.${field}`, records) };
|
|
75
|
+
}),
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// The blueprint a being actually stands on: one class block named for the
|
|
79
|
+
// contract's root type, its declared fields flat, and every other type of the
|
|
80
|
+
// schema behind it as a record. Ordering, canonical spelling, unused records
|
|
81
|
+
// and recursion are the notation's own law, so they are the kit's `print` to
|
|
82
|
+
// judge and never this file's.
|
|
83
|
+
export function project(one) {
|
|
84
|
+
const records = new Set(Object.keys(one.records));
|
|
85
|
+
const blueprint = {
|
|
86
|
+
name: one.name,
|
|
87
|
+
fields: Object.entries(one.fields).map(([name, field]) => ({
|
|
88
|
+
name,
|
|
89
|
+
args: projectArgs(field.args, name, records),
|
|
90
|
+
answer:
|
|
91
|
+
field.answers.of === NOTHING && !field.answers.list
|
|
92
|
+
? null
|
|
93
|
+
: projectType(field.answers, name, records),
|
|
94
|
+
})),
|
|
95
|
+
records: [...Object.entries(one.records)].map(([name, fields]) =>
|
|
96
|
+
projectRecord(name, fields, records),
|
|
97
|
+
),
|
|
98
|
+
};
|
|
99
|
+
return print(blueprint);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { NOTHING, SCALARS };
|
package/GETTING-STARTED.md
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
# Getting started — two castles
|
|
2
|
-
|
|
3
|
-
Picture a castle: one gate, its own residents, its own law. A **ground** is
|
|
4
|
-
the castle — one sovereign process, one handler where every letter arrives.
|
|
5
|
-
Its residents are **beings**; its capabilities are **modules**; the ground
|
|
6
|
-
under it is a **floor** — a filesystem, or a browser's IndexedDB — and the
|
|
7
|
-
stance it takes toward the world is its **terrain**. This page raises two
|
|
8
|
-
castles on two terrains and has them speak: a **mountain** (a server, always
|
|
9
|
-
reachable, always remembering) and a **house of cards** (a browser tab,
|
|
10
|
-
raised in seconds, holding what it can afford to lose). Same castle, same
|
|
11
|
-
law, different ground under it.
|
|
12
|
-
|
|
13
|
-
Everything below runs today — the bench drives this exact story in
|
|
14
|
-
`bench/worlds/story/`.
|
|
15
|
-
|
|
16
|
-
## The mountain
|
|
17
|
-
|
|
18
|
-
The machine's raise is one config and one daemon —
|
|
19
|
-
[`nervurd`](https://www.npmjs.com/package/nervurd) is the host, and its
|
|
20
|
-
README is the operator's full page. The five-minute shape:
|
|
21
|
-
|
|
22
|
-
```sh
|
|
23
|
-
npm install -g nervurd
|
|
24
|
-
nervur self mint --self ./hand.json --heir ./heir.json
|
|
25
|
-
nervur init --config ./castle.json --self ./hand.json
|
|
26
|
-
NERVUR_SECRET=... nervurd --config ./castle.json
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
The config names the shelf, the door, the secret's environment variable,
|
|
30
|
-
and the modules — the castle's capabilities, chosen by you:
|
|
31
|
-
|
|
32
|
-
```json
|
|
33
|
-
{
|
|
34
|
-
"cells": "/var/lib/nervur/castle.json",
|
|
35
|
-
"listen": { "host": "127.0.0.1", "port": 7001 },
|
|
36
|
-
"secret": "NERVUR_SECRET",
|
|
37
|
-
"modules": {}
|
|
38
|
-
}
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Keep the heir off the machine. Whoever takes the machine takes what the
|
|
42
|
-
castle holds today; the heir is why they can never become it tomorrow.
|
|
43
|
-
|
|
44
|
-
## The house of cards
|
|
45
|
-
|
|
46
|
-
The browser's raise is two calls from
|
|
47
|
-
[`@nervur-org/floor-web`](https://www.npmjs.com/package/@nervur-org/floor-web),
|
|
48
|
-
because a tab closes constantly and the two acts must not blur:
|
|
49
|
-
|
|
50
|
-
```js
|
|
51
|
-
import { found, wake } from '@nervur-org/floor-web';
|
|
52
|
-
|
|
53
|
-
// Once, ever: mint the identity, stand the castle, get custody back.
|
|
54
|
-
let kept;
|
|
55
|
-
const castle = found({ keep: (dump) => (kept = dump) });
|
|
56
|
-
// castle.address — the name; castle.custody — the hand, the heir, the
|
|
57
|
-
// secret: yours to keep (IndexedDB, or a mountain you trust), never the
|
|
58
|
-
// ground's.
|
|
59
|
-
|
|
60
|
-
// Every session after: same name, same residents, no identity act.
|
|
61
|
-
// wake hands back the standing ground itself — woken.address is the name.
|
|
62
|
-
const woken = wake({ held: kept, secret: castle.custody.secret });
|
|
63
|
-
```
|
|
64
|
-
|
|
65
|
-
Where `kept` and `custody` live is your choice, under this terrain's honest
|
|
66
|
-
limit: a browser may evict what it keeps — a card castle holds what it can
|
|
67
|
-
afford to lose, and keeps what is precious in its relationship with a
|
|
68
|
-
mountain.
|
|
69
|
-
|
|
70
|
-
## They speak
|
|
71
|
-
|
|
72
|
-
A letter is a letter on every terrain: one signed envelope, carried
|
|
73
|
-
however the two castles can reach each other — here, `fetch` to the
|
|
74
|
-
mountain's door. First the mountain's keeper invites the card castle's
|
|
75
|
-
founder (one signed act at the admin door — the CLI, or any client the
|
|
76
|
-
keeper holds), and then the founder asks from the browser:
|
|
77
|
-
|
|
78
|
-
```js
|
|
79
|
-
import { Client, Tools, Voice } from 'nervur';
|
|
80
|
-
|
|
81
|
-
const me = Voice.revive(castle.custody.hand);
|
|
82
|
-
const talk = Client({
|
|
83
|
-
voice: me,
|
|
84
|
-
deliver: async (envelope) => {
|
|
85
|
-
const res = await fetch('http://127.0.0.1:7001', {
|
|
86
|
-
body: JSON.stringify(envelope),
|
|
87
|
-
headers: { 'content-type': 'application/json' },
|
|
88
|
-
method: 'POST',
|
|
89
|
-
});
|
|
90
|
-
return res.status === 200 ? res.json() : undefined;
|
|
91
|
-
},
|
|
92
|
-
});
|
|
93
|
-
|
|
94
|
-
const heard = await talk.ask(mountainAddress, '{ census { active } }');
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
The client verifies every answer from the address alone — the mountain's
|
|
98
|
-
word arrives signed, and a broken or missing signature is dropped exactly
|
|
99
|
-
like silence. A stranger who was never invited gets silence too, and cannot
|
|
100
|
-
tell refusal from absence. That is the whole protocol: **say who you are,
|
|
101
|
-
name whom you seek, and the castle answers or keeps quiet.**
|
|
102
|
-
|
|
103
|
-
## Where to go next
|
|
104
|
-
|
|
105
|
-
- [`nervur`'s README](README.md) — the library underneath both castles.
|
|
106
|
-
- [`nervurd`'s README](../nervurd/README.md) — the operator's page: checks,
|
|
107
|
-
rotation, backups, the door's full behaviour.
|
|
108
|
-
- [`@nervur-org/floor-web`](../floor-web/README.md) — the web floor's
|
|
109
|
-
terrain contract, founding and waking in full.
|
|
110
|
-
- [`@nervur-org/species`](../species/README.md) — blueprints your castles
|
|
111
|
-
grow beings from.
|
package/NOTICE
DELETED
package/SECURITY.md
DELETED
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
# Security
|
|
2
|
-
|
|
3
|
-
This package answers one question — by whose authority — and a hole in that
|
|
4
|
-
answer is the most serious bug it can have.
|
|
5
|
-
|
|
6
|
-
Report vulnerabilities privately to **<security@nervur.com>**. Do not open a
|
|
7
|
-
public report. State what you ran, what you sent, and what answered; a
|
|
8
|
-
proof of concept is welcome but not required.
|
|
9
|
-
|
|
10
|
-
A person reads every report. Fixes ship as releases on the live 0.x line,
|
|
11
|
-
and the report is credited if you want it to be.
|
package/admin.mjs
DELETED
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
const ADMIN = `({
|
|
2
|
-
name: 'nervur.ground',
|
|
3
|
-
needs: { modules: [{ module: 'memory', contract: 'nervur.memory' }] },
|
|
4
|
-
memory: {
|
|
5
|
-
'aliases/*': { type: 'String', class: 'data' },
|
|
6
|
-
wears: { type: 'String', class: 'data' },
|
|
7
|
-
},
|
|
8
|
-
interface: \`
|
|
9
|
-
input RefInput { address: String!, nextPkHash: String, rights: String }
|
|
10
|
-
type Resident { address: String!, alias: String, digest: String, active: Boolean! }
|
|
11
|
-
type Census { residents: [Resident!]!, stored: Int!, active: Int! }
|
|
12
|
-
type Query {
|
|
13
|
-
census: Census @rights(is: [SELF, MEMBER])
|
|
14
|
-
aliasOf(name: String!): String @rights(is: [SELF, MEMBER])
|
|
15
|
-
attest(module: String!): Boolean @rights(is: [SELF, MEMBER])
|
|
16
|
-
opened: Boolean @rights(is: [SELF, MEMBER, STRANGER])
|
|
17
|
-
wears: String @rights(is: [SELF, MEMBER, STRANGER])
|
|
18
|
-
}
|
|
19
|
-
type Mutation {
|
|
20
|
-
invite(address: String!, nextPkHash: String, rights: String): Boolean @rights(is: [SELF, MEMBER])
|
|
21
|
-
drop(address: String!): Boolean @rights(is: [SELF, MEMBER])
|
|
22
|
-
extend(program: String): Boolean @rights(is: [SELF])
|
|
23
|
-
create(program: String!, refs: [RefInput!], data: String, config: String): String
|
|
24
|
-
@rights(is: [SELF, MEMBER])
|
|
25
|
-
activate(address: String!): String @rights(is: [SELF, MEMBER])
|
|
26
|
-
destroy(address: String!): String @rights(is: [SELF, MEMBER])
|
|
27
|
-
upgrade(address: String!, program: String!): Boolean @rights(is: [SELF, MEMBER])
|
|
28
|
-
alias(name: String!, address: String!): Boolean @rights(is: [SELF, MEMBER])
|
|
29
|
-
wear(address: String): Boolean @rights(is: [SELF, MEMBER])
|
|
30
|
-
}
|
|
31
|
-
\`,
|
|
32
|
-
resolvers: {
|
|
33
|
-
Query: {
|
|
34
|
-
census: (_, __, { registry }) => {
|
|
35
|
-
const residents = registry.residents();
|
|
36
|
-
return {
|
|
37
|
-
active: residents.filter((one) => one.active).length,
|
|
38
|
-
residents,
|
|
39
|
-
stored: residents.length,
|
|
40
|
-
};
|
|
41
|
-
},
|
|
42
|
-
aliasOf: (_, { name }, { modules }) => modules.memory.read('aliases/' + name),
|
|
43
|
-
attest: (_, { module }, { registry }) => registry.attest(module),
|
|
44
|
-
opened: (_, __, { modules }) => modules.memory.list('refs/').length > 0,
|
|
45
|
-
wears: (_, __, { modules }) => modules.memory.read('wears') ?? null,
|
|
46
|
-
},
|
|
47
|
-
Mutation: {
|
|
48
|
-
invite: (_, { address, nextPkHash, rights }, { modules }) =>
|
|
49
|
-
modules.memory.write('refs/' + address, {
|
|
50
|
-
nextPkHash: nextPkHash ?? null,
|
|
51
|
-
rights: rights ?? 'MEMBER',
|
|
52
|
-
}) !== undefined,
|
|
53
|
-
drop: (_, { address }, { modules }) => {
|
|
54
|
-
if (modules.memory.read('refs/' + address) === undefined) {
|
|
55
|
-
return false;
|
|
56
|
-
}
|
|
57
|
-
modules.memory.write('refs/' + address, undefined);
|
|
58
|
-
return modules.memory.read('refs/' + address) === undefined;
|
|
59
|
-
},
|
|
60
|
-
extend: (_, { program }, { registry }) => registry.extend(program) !== undefined,
|
|
61
|
-
create: (_, { program, refs, data, config }, { registry }) =>
|
|
62
|
-
registry.create({
|
|
63
|
-
config: config === undefined ? undefined : JSON.parse(config),
|
|
64
|
-
data: data ? JSON.parse(data) : {},
|
|
65
|
-
program,
|
|
66
|
-
refs: refs ?? [],
|
|
67
|
-
}),
|
|
68
|
-
activate: (_, { address }, { registry }) =>
|
|
69
|
-
registry.activate(address) === undefined ? null : address,
|
|
70
|
-
destroy: (_, { address }, { registry }) => registry.destroy(address) ?? null,
|
|
71
|
-
upgrade: (_, { address, program }, { registry }) =>
|
|
72
|
-
registry.upgrade({ address, program }) !== undefined,
|
|
73
|
-
alias: (_, { name, address }, { modules }) =>
|
|
74
|
-
modules.memory.write('aliases/' + name, address) !== undefined,
|
|
75
|
-
wear: (_, { address }, { modules }) => {
|
|
76
|
-
modules.memory.write('wears', address ?? undefined);
|
|
77
|
-
return modules.memory.read('wears') === (address ?? undefined);
|
|
78
|
-
},
|
|
79
|
-
},
|
|
80
|
-
},
|
|
81
|
-
})`;
|
|
82
|
-
|
|
83
|
-
export { ADMIN };
|