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/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 };
@@ -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
@@ -1,2 +0,0 @@
1
- Nervur
2
- Copyright 2026 Razvan Gherghina
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 };