nervur 0.3.0 → 0.6.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/contract.mjs ADDED
@@ -0,0 +1,96 @@
1
+ import { buildSchema, isEnumType, isInputObjectType, parse, print } from 'graphql';
2
+ import { Tools } from './tools.mjs';
3
+
4
+ const argsAgree = (wanted, given) => {
5
+ for (const arg of wanted.args ?? []) {
6
+ const same = (given.args ?? []).find((one) => one.name === arg.name);
7
+ if (same === undefined || String(same.type) !== String(arg.type)) {
8
+ return false;
9
+ }
10
+ }
11
+ return (given.args ?? [])
12
+ .filter((one) => !(wanted.args ?? []).some((asked) => asked.name === one.name))
13
+ .every((one) => !String(one.type).endsWith('!') || one.defaultValue !== undefined);
14
+ };
15
+
16
+ const Contract = (name, sdl) => {
17
+ try {
18
+ const schema = buildSchema(sdl);
19
+ const printed = print(parse(sdl));
20
+ return Object.freeze({
21
+ digest: Tools.digest(Tools.canon({ interface: printed, name })),
22
+ name,
23
+ satisfies: (offered) => {
24
+ const theirs = (() => {
25
+ try {
26
+ return buildSchema(offered);
27
+ } catch {
28
+ return undefined;
29
+ }
30
+ })();
31
+ if (theirs === undefined) {
32
+ return false;
33
+ }
34
+ for (const [typeName, wanted] of Object.entries(schema.getTypeMap())) {
35
+ if (typeName.startsWith('__')) {
36
+ continue;
37
+ }
38
+ const given = theirs.getTypeMap()[typeName];
39
+ if (isEnumType(wanted)) {
40
+ if (
41
+ !isEnumType(given) ||
42
+ !wanted.getValues().every((one) => given.getValue(one.name) !== undefined)
43
+ ) {
44
+ return false;
45
+ }
46
+ continue;
47
+ }
48
+ if (wanted.getFields === undefined) {
49
+ continue;
50
+ }
51
+ if (given?.getFields === undefined) {
52
+ return false;
53
+ }
54
+ for (const [field, held] of Object.entries(wanted.getFields())) {
55
+ const mine = given.getFields()[field];
56
+ if (mine === undefined || String(mine.type) !== String(held.type)) {
57
+ return false;
58
+ }
59
+ if (!argsAgree(held, mine)) {
60
+ return false;
61
+ }
62
+ }
63
+ if (isInputObjectType(wanted)) {
64
+ const demanded = Object.entries(given.getFields())
65
+ .filter(([field]) => wanted.getFields()[field] === undefined)
66
+ .filter(
67
+ ([, one]) => String(one.type).endsWith('!') && one.defaultValue === undefined,
68
+ );
69
+ if (demanded.length > 0) {
70
+ return false;
71
+ }
72
+ }
73
+ }
74
+ return true;
75
+ },
76
+ sdl: printed,
77
+ });
78
+ } catch {
79
+ return undefined;
80
+ }
81
+ };
82
+
83
+ const Contracts = (written) => {
84
+ const held = Object.entries(written).map(([name, sdl]) => Contract(name, sdl));
85
+ if (held.some((one) => one === undefined)) {
86
+ return undefined;
87
+ }
88
+ const byName = new Map(held.map((one) => [one.name, one]));
89
+ return Object.freeze({
90
+ digests: () => Object.fromEntries(held.map((one) => [one.name, one.digest])),
91
+ get: (name) => byName.get(name),
92
+ has: (name) => byName.has(name),
93
+ });
94
+ };
95
+
96
+ export { Contract, Contracts };
package/ground.mjs ADDED
@@ -0,0 +1,196 @@
1
+ import { ADMIN } from './admin.mjs';
2
+ import { Being, Compartment, Registry, make, rootFor } from './being.mjs';
3
+ import { coreClass } from './blueprint.mjs';
4
+ import { Client } from './client.mjs';
5
+ import { Contracts } from './contract.mjs';
6
+ import { Memory, Paths, Wire } from './modules.mjs';
7
+ import { Mcp, Pointer } from './pointer.mjs';
8
+ import { Program } from './program.mjs';
9
+ import { Stamp, Tools } from './tools.mjs';
10
+ import { Voice } from './voice.mjs';
11
+
12
+ const GROUND = 'Ground';
13
+
14
+ const FLOOR = ['blueprints', 'cells', 'contracts', 'keychain', 'paths', 'watch', 'wire'];
15
+
16
+ const install = ({ cells, keychain, program }) => {
17
+ const memory = Memory(cells);
18
+ const voice = Voice();
19
+ const address = Tools.address(voice.pk);
20
+ const sealed = keychain.seal(voice.pack());
21
+ if (sealed === undefined) {
22
+ return;
23
+ }
24
+ make({ memory, voice, address, program });
25
+ cells.put('ground/admin', { address, sealed });
26
+ };
27
+
28
+ const Ground = (modules = {}) => {
29
+ const cells = modules.cells;
30
+ const keychain = modules.keychain;
31
+ if (cells === undefined || keychain?.seal === undefined || keychain?.unseal === undefined) {
32
+ return undefined;
33
+ }
34
+
35
+ const written = modules.contracts ?? Contracts({});
36
+ const extra = Object.fromEntries(
37
+ Object.entries(modules).filter(([name]) => !FLOOR.includes(name)),
38
+ );
39
+ const bound = Object.entries(extra).filter(([, one]) => one?.at !== undefined);
40
+ if (bound.some(([, one]) => !written.has(one.contract))) {
41
+ return undefined;
42
+ }
43
+
44
+ const program = modules.blueprints?.ground() ?? ADMIN;
45
+ if (cells.peek('ground/admin') === undefined) {
46
+ install({ cells, keychain, program });
47
+ }
48
+ const installed = cells.peek('ground/admin');
49
+ if (installed === undefined) {
50
+ return undefined;
51
+ }
52
+ const bundle = keychain.unseal(installed.sealed);
53
+ const adminVoice = bundle === undefined ? undefined : Voice.revive(bundle);
54
+ if (adminVoice === undefined) {
55
+ return undefined;
56
+ }
57
+
58
+ // Never leaves this closure — nothing outside may be handed it.
59
+ const vouched = Symbol('vouched');
60
+
61
+ const watch = (fact) => {
62
+ try {
63
+ modules.watch?.(Object.freeze(fact));
64
+ } catch {
65
+ return undefined;
66
+ }
67
+ };
68
+
69
+ const address = installed.address;
70
+ const memory = Memory(cells);
71
+ const paths = modules.paths ?? Paths(new Map());
72
+ const wire = modules.wire ?? Wire();
73
+
74
+ const stamp = Stamp();
75
+ const boundFor = (voice) =>
76
+ Object.fromEntries(
77
+ bound.map(([name, one]) => [
78
+ name,
79
+ Pointer(written.get(one.contract).sdl, (source, variables) =>
80
+ deliver(Tools.envelope(voice, one.at, { ask: { source, variables } }, stamp())),
81
+ ),
82
+ ]),
83
+ );
84
+ const modulesFor = (voice) =>
85
+ Object.freeze({
86
+ ...extra,
87
+ ...boundFor(voice),
88
+ deliver: (envelope) => deliver(envelope),
89
+ memory,
90
+ paths,
91
+ wire,
92
+ });
93
+ const adminModules = modulesFor(adminVoice);
94
+
95
+ const adminMine = Compartment({
96
+ memory,
97
+ voice: adminVoice,
98
+ address,
99
+ root: rootFor({ memory, voice: adminVoice, address }),
100
+ classOf: (key) =>
101
+ Program(memory.read(address, 'program')?.value)?.classOf(key) ?? coreClass(key),
102
+ });
103
+
104
+ const receive = async (envelope, token) => {
105
+ const at = envelope?.to;
106
+ const door = at === address ? admin : registry.activate(at);
107
+ if (door === undefined) {
108
+ watch({
109
+ from: envelope?.from,
110
+ kind: envelope?.op === undefined ? undefined : Object.keys(envelope.op)[0],
111
+ outcome: 'no-door',
112
+ to: at,
113
+ });
114
+ return undefined;
115
+ }
116
+ return door(envelope, token);
117
+ };
118
+ const announce = (at) => paths.publish(at, receive);
119
+ const forget = (at) => {
120
+ paths.publish(at, undefined);
121
+ cells.drop(`${at}/`);
122
+ };
123
+
124
+ const attest = async (name) => {
125
+ const one = extra[name];
126
+ if (one?.at === undefined) {
127
+ return adminModules[name] !== undefined;
128
+ }
129
+ const seen = await deliver(Tools.envelope(adminVoice, one.at, { describe: {} }, stamp()));
130
+ return seen?.body?.sdl === undefined
131
+ ? false
132
+ : written.get(one.contract).satisfies(seen.body.sdl);
133
+ };
134
+
135
+ const registry = Registry({
136
+ memory,
137
+ adminMine,
138
+ modules: adminModules,
139
+ modulesFor,
140
+ announce,
141
+ forget,
142
+ vouched,
143
+ attest,
144
+ watch,
145
+ });
146
+ const admin = Being({
147
+ voice: adminVoice,
148
+ address,
149
+ modules: adminModules,
150
+ registry,
151
+ vouched,
152
+ watch,
153
+ });
154
+
155
+ announce(address);
156
+ for (const key of adminMine.list('identity/custody/')) {
157
+ announce(key.slice('identity/custody/'.length));
158
+ }
159
+
160
+ const held = (at) => at === address || adminMine.read(`identity/custody/${at}`) !== undefined;
161
+ const deliver = async (envelope) =>
162
+ held(envelope.to) ? receive(envelope, vouched) : wire.carry(paths.find(envelope.to), envelope);
163
+
164
+ // Client edge only — a resolver must never be handed this.
165
+ const resolve = (one) => (one === GROUND ? address : (adminMine.read(`aliases/${one}`) ?? one));
166
+
167
+ return Object.freeze({
168
+ address,
169
+ receive,
170
+ serve: (voice) => {
171
+ const client = Client({ voice, deliver, resolve });
172
+ const sdlOf = async (at) => (await client.describe(at))?.body?.sdl;
173
+ const asker = (at) => (source, variables) => client.ask(at, source, variables);
174
+ return Object.freeze({
175
+ address: client.address,
176
+ client,
177
+ gql: (at) =>
178
+ Object.freeze({
179
+ ask: asker(at),
180
+ introspect: () => client.introspect(at),
181
+ sdl: () => sdlOf(at),
182
+ }),
183
+ mcp: async (at) => {
184
+ const sdl = await sdlOf(at);
185
+ return sdl === undefined ? undefined : Mcp(sdl, asker(at));
186
+ },
187
+ pointer: async (at) => {
188
+ const sdl = await sdlOf(at);
189
+ return sdl === undefined ? undefined : Pointer(sdl, asker(at));
190
+ },
191
+ });
192
+ },
193
+ });
194
+ };
195
+
196
+ export { FLOOR, GROUND, Ground };
package/index.mjs ADDED
@@ -0,0 +1,10 @@
1
+ export { ADMIN } from './admin.mjs';
2
+ export { Blueprint, CLASSES, CORE_ONLY, OPEN, STRUCTURAL, coreClass, reads } from './blueprint.mjs';
3
+ export { Client } from './client.mjs';
4
+ export { Contract, Contracts } from './contract.mjs';
5
+ export { GROUND, Ground } from './ground.mjs';
6
+ export { Cells, Clock, Dns, Entropy, Keychain, Memory, Paths, Wire } from './modules.mjs';
7
+ export { Mcp, Pointer } from './pointer.mjs';
8
+ export { Program } from './program.mjs';
9
+ export { MEASURE, Stamp, Tools } from './tools.mjs';
10
+ export { Voice } from './voice.mjs';
package/modules.mjs ADDED
@@ -0,0 +1,111 @@
1
+ import { existsSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
2
+ import { Tools } from './tools.mjs';
3
+
4
+ const Cells = ({ file } = {}) => {
5
+ const cells = new Map(
6
+ file && existsSync(file) ? Object.entries(JSON.parse(readFileSync(file, 'utf8'))) : [],
7
+ );
8
+ const flush = () => {
9
+ if (file) {
10
+ writeFileSync(`${file}.next`, JSON.stringify(Object.fromEntries(cells)));
11
+ renameSync(`${file}.next`, file);
12
+ }
13
+ };
14
+ return Object.freeze({
15
+ drop: (prefix) => {
16
+ for (const cell of [...cells.keys()].filter((one) => one.startsWith(prefix))) {
17
+ cells.delete(cell);
18
+ }
19
+ flush();
20
+ },
21
+ dump: () => JSON.stringify(Object.fromEntries(cells)),
22
+ keys: (prefix) => [...cells.keys()].filter((cell) => cell.startsWith(prefix)),
23
+ peek: (cell) => cells.get(cell),
24
+ put: (cell, held) => {
25
+ cells.set(cell, held);
26
+ flush();
27
+ },
28
+ });
29
+ };
30
+
31
+ const Memory = (cells) => {
32
+ const held = (address, key) => cells.peek(`${address}/${key}`);
33
+ const writerAt = (address) => {
34
+ const chain = held(address, 'chain/pk');
35
+ return chain === undefined ? Tools.pkOf(address) : Buffer.from(chain.value, 'hex');
36
+ };
37
+ return Object.freeze({
38
+ list: (address, prefix) =>
39
+ cells.keys(`${address}/${prefix}`).map((cell) => cell.slice(address.length + 1)),
40
+
41
+ read: (address, key) => held(address, key),
42
+
43
+ replace: ({ address, pk, nextPkHash, sig }) => {
44
+ const committed = held(address, 'chain/nextPkHash');
45
+ if (committed === undefined || Tools.digest(Buffer.from(pk, 'hex')) !== committed.value) {
46
+ return undefined;
47
+ }
48
+ const version = committed.version + 1;
49
+ const claim = { address, nextPkHash, pk, version };
50
+ if (!Tools.verifies(Buffer.from(pk, 'hex'), Tools.canon(claim), sig)) {
51
+ return undefined;
52
+ }
53
+ cells.put(`${address}/chain/pk`, { value: pk, version });
54
+ cells.put(`${address}/chain/nextPkHash`, { value: nextPkHash, version });
55
+ return version;
56
+ },
57
+
58
+ write: ({ address, key, value, version, sig }) => {
59
+ if (key.startsWith('chain/') && version !== 1) {
60
+ return undefined;
61
+ }
62
+ const writer = writerAt(address);
63
+ if (writer === null || version !== (held(address, key)?.version ?? 0) + 1) {
64
+ return undefined;
65
+ }
66
+ if (!Tools.verifies(writer, Tools.canon({ address, key, value, version }), sig)) {
67
+ return undefined;
68
+ }
69
+ cells.put(`${address}/${key}`, { value, version });
70
+ return version;
71
+ },
72
+ });
73
+ };
74
+
75
+ const Clock = (ticks) => {
76
+ let at = 0;
77
+ return Object.freeze({ now: () => ticks[Math.min(at++, ticks.length - 1)] });
78
+ };
79
+
80
+ const Entropy = (grains) => {
81
+ let at = 0;
82
+ return Object.freeze({ grain: () => grains[Math.min(at++, grains.length - 1)] });
83
+ };
84
+
85
+ const Keychain = (secret) =>
86
+ Object.freeze({
87
+ seal: (value) => {
88
+ try {
89
+ return Tools.seal(secret, value);
90
+ } catch {
91
+ return undefined;
92
+ }
93
+ },
94
+ unseal: (sealed) => Tools.unseal(secret, sealed),
95
+ });
96
+
97
+ const Dns = () => new Map();
98
+
99
+ const Paths = (dns) =>
100
+ Object.freeze({
101
+ find: (address) => dns.get(address),
102
+ publish: (address, endpoint) =>
103
+ endpoint === undefined ? dns.delete(address) : dns.set(address, endpoint),
104
+ });
105
+
106
+ const Wire = () =>
107
+ Object.freeze({
108
+ carry: async (endpoint, envelope) => (endpoint === undefined ? undefined : endpoint(envelope)),
109
+ });
110
+
111
+ export { Cells, Clock, Dns, Entropy, Keychain, Memory, Paths, Wire };
package/package.json CHANGED
@@ -1,20 +1,22 @@
1
1
  {
2
2
  "name": "nervur",
3
- "version": "0.3.0",
4
- "description": "The nervur CLI — a client of a running carcass, never the runtime (papers/host.md).",
3
+ "version": "0.6.0",
4
+ "description": "A distributed system of sovereign beings — one door, one question: by whose authority.",
5
+ "license": "Apache-2.0",
6
+ "author": "Razvan Gherghina",
7
+ "homepage": "https://nervur.org",
5
8
  "type": "module",
6
- "bin": {
7
- "nervur": "nervur.js"
9
+ "exports": {
10
+ ".": "./index.mjs"
8
11
  },
9
12
  "files": [
10
- "nervur.js",
11
- "preflight.js",
12
- "scaffold.js",
13
- "completion.js",
14
- "style.js"
13
+ "*.mjs",
14
+ "NOTICE"
15
15
  ],
16
- "dependencies": {
17
- "@nervur-org/kit": "^0.1.0"
16
+ "engines": {
17
+ "node": ">=22"
18
18
  },
19
- "license": "Apache-2.0"
19
+ "dependencies": {
20
+ "graphql": "^17.0.2"
21
+ }
20
22
  }
package/pointer.mjs ADDED
@@ -0,0 +1,79 @@
1
+ import { buildSchema, getNamedType, isAbstractType, isObjectType } from 'graphql';
2
+
3
+ const JSON_TYPE = { Boolean: 'boolean', Float: 'number', ID: 'string', Int: 'integer' };
4
+
5
+ const selection = (type, depth) => {
6
+ const named = getNamedType(type);
7
+ if (!isObjectType(named) || depth === 0) {
8
+ return '';
9
+ }
10
+ const fields = Object.values(named.getFields())
11
+ .filter(
12
+ (one) =>
13
+ !one.args.some((arg) => String(arg.type).endsWith('!') && arg.defaultValue === undefined),
14
+ )
15
+ .filter((one) => !isAbstractType(getNamedType(one.type)))
16
+ .filter((one) => depth > 1 || !isObjectType(getNamedType(one.type)))
17
+ .map((one) => `${one.name}${selection(one.type, depth - 1)}`);
18
+ return fields.length === 0 ? '' : ` { ${fields.join(' ')} }`;
19
+ };
20
+
21
+ const document = (kind, field, args) => {
22
+ const used = field.args.filter((one) => args[one.name] !== undefined);
23
+ const names = used.map((one) => `$${one.name}: ${one.type}`).join(', ');
24
+ const pass = used.map((one) => `${one.name}: $${one.name}`).join(', ');
25
+ return `${kind}${names ? `(${names})` : ''} { ${field.name}${
26
+ pass ? `(${pass})` : ''
27
+ }${selection(field.type, 3)} }`;
28
+ };
29
+
30
+ const roots = (schema) =>
31
+ ['query', 'mutation'].flatMap((kind) => {
32
+ const type = kind === 'query' ? schema.getQueryType() : schema.getMutationType();
33
+ return type === undefined || type === null
34
+ ? []
35
+ : Object.values(type.getFields()).map((field) => ({ field, kind }));
36
+ });
37
+
38
+ const calls = (sdl, ask) => {
39
+ const schema = buildSchema(sdl);
40
+ return roots(schema).map(({ field, kind }) => ({
41
+ field,
42
+ kind,
43
+ run: async (args = {}) => {
44
+ const answered = await ask(document(kind, field, args), args);
45
+ return answered?.body?.data?.[field.name];
46
+ },
47
+ }));
48
+ };
49
+
50
+ const Pointer = (sdl, ask) =>
51
+ Object.freeze(Object.fromEntries(calls(sdl, ask).map((one) => [one.field.name, one.run])));
52
+
53
+ const Mcp = (sdl, ask) => {
54
+ const held = calls(sdl, ask);
55
+ return Object.freeze({
56
+ call: (name, args) => held.find((one) => one.field.name === name)?.run(args),
57
+ tools: held.map(({ field, kind }) =>
58
+ Object.freeze({
59
+ annotations: { readOnlyHint: kind === 'query' },
60
+ description: field.description ?? undefined,
61
+ inputSchema: {
62
+ properties: Object.fromEntries(
63
+ field.args.map((one) => [
64
+ one.name,
65
+ { type: JSON_TYPE[getNamedType(one.type).name] ?? 'string' },
66
+ ]),
67
+ ),
68
+ required: field.args
69
+ .filter((one) => String(one.type).endsWith('!'))
70
+ .map((one) => one.name),
71
+ type: 'object',
72
+ },
73
+ name: field.name,
74
+ }),
75
+ ),
76
+ });
77
+ };
78
+
79
+ export { Mcp, Pointer };
package/program.mjs ADDED
@@ -0,0 +1,74 @@
1
+ import {
2
+ buildSchema,
3
+ defaultFieldResolver,
4
+ getIntrospectionQuery,
5
+ graphql,
6
+ isObjectType,
7
+ } from 'graphql';
8
+ import { Blueprint } from './blueprint.mjs';
9
+
10
+ const bound = (blueprint) => {
11
+ const schema = blueprint.schema;
12
+ for (const type of Object.values(schema.getTypeMap())) {
13
+ if (!isObjectType(type) || type.name.startsWith('__')) {
14
+ continue;
15
+ }
16
+ for (const [field, held] of Object.entries(type.getFields())) {
17
+ const resolver = blueprint.resolvers[type.name]?.[field] ?? defaultFieldResolver;
18
+ held.resolve = (source, args, context, info) =>
19
+ blueprint.admits(type.name, field, context.rights)
20
+ ? resolver(source, args, context, info)
21
+ : null;
22
+ }
23
+ }
24
+ return schema;
25
+ };
26
+
27
+ const Program = (source) => {
28
+ const blueprint = Blueprint(source);
29
+ if (blueprint === undefined || blueprint.trespass.length > 0) {
30
+ return undefined;
31
+ }
32
+ const schema = bound(blueprint);
33
+ const curtained = new Map();
34
+ const seenBy = (held) => {
35
+ if (!curtained.has(held)) {
36
+ try {
37
+ curtained.set(held, buildSchema(blueprint.sdlFor(held)));
38
+ } catch {
39
+ curtained.set(held, undefined);
40
+ }
41
+ }
42
+ return curtained.get(held);
43
+ };
44
+ return Object.freeze({
45
+ ask: async ({ source: asked, variables, context }) => {
46
+ const answered = await graphql({
47
+ contextValue: context,
48
+ schema,
49
+ source: asked,
50
+ variableValues: variables,
51
+ });
52
+ return answered.errors === undefined ? { data: answered.data } : undefined;
53
+ },
54
+ bindings: blueprint.bindings,
55
+ classOf: blueprint.classOf,
56
+ config: blueprint.config,
57
+ declares: blueprint.declares,
58
+ digest: blueprint.digest,
59
+ introspect: async (held) => {
60
+ const filtered = seenBy(held);
61
+ if (filtered === undefined) {
62
+ return undefined;
63
+ }
64
+ const answered = await graphql({ schema: filtered, source: getIntrospectionQuery() });
65
+ return answered.errors === undefined ? answered.data : undefined;
66
+ },
67
+ modules: blueprint.modules,
68
+ name: blueprint.name,
69
+ rights: blueprint.rights,
70
+ sdlFor: blueprint.sdlFor,
71
+ });
72
+ };
73
+
74
+ export { Program };