nervur 0.3.0 → 0.5.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 +0 -1
- package/NOTICE +2 -0
- package/README.md +325 -43
- package/admin.mjs +58 -0
- package/being.mjs +365 -0
- package/blueprint.mjs +281 -0
- package/client.mjs +43 -0
- package/contract.mjs +96 -0
- package/ground.mjs +196 -0
- package/index.mjs +10 -0
- package/modules.mjs +110 -0
- package/package.json +14 -12
- package/pointer.mjs +79 -0
- package/program.mjs +74 -0
- package/tools.mjs +139 -0
- package/voice.mjs +90 -0
- package/completion.js +0 -122
- package/nervur.js +0 -672
- package/preflight.js +0 -162
- package/scaffold.js +0 -50
- package/style.js +0 -82
package/tools.mjs
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createCipheriv,
|
|
3
|
+
createDecipheriv,
|
|
4
|
+
createHash,
|
|
5
|
+
createPublicKey,
|
|
6
|
+
randomBytes,
|
|
7
|
+
verify,
|
|
8
|
+
} from 'node:crypto';
|
|
9
|
+
|
|
10
|
+
const ED25519_SPKI_PREFIX = Buffer.from('302a300506032b6570032100', 'hex');
|
|
11
|
+
|
|
12
|
+
const B32 = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ234567';
|
|
13
|
+
|
|
14
|
+
const VERSION_G = 6 << 3;
|
|
15
|
+
|
|
16
|
+
const ordered = (value) =>
|
|
17
|
+
value === null || typeof value !== 'object'
|
|
18
|
+
? value
|
|
19
|
+
: Array.isArray(value)
|
|
20
|
+
? value.map(ordered)
|
|
21
|
+
: Object.fromEntries(
|
|
22
|
+
Object.keys(value)
|
|
23
|
+
.sort()
|
|
24
|
+
.map((key) => [key, ordered(value[key])]),
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
const crc16 = (bytes) => {
|
|
28
|
+
let crc = 0;
|
|
29
|
+
for (const b of bytes) {
|
|
30
|
+
crc ^= b << 8;
|
|
31
|
+
for (let i = 0; i < 8; i += 1) {
|
|
32
|
+
crc = (crc & 0x8000 ? (crc << 1) ^ 0x1021 : crc << 1) & 0xffff;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return crc;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const base32 = (bytes) => {
|
|
39
|
+
let bits = 0;
|
|
40
|
+
let value = 0;
|
|
41
|
+
let out = '';
|
|
42
|
+
for (const b of bytes) {
|
|
43
|
+
value = (value << 8) | b;
|
|
44
|
+
bits += 8;
|
|
45
|
+
while (bits >= 5) {
|
|
46
|
+
out += B32[(value >>> (bits - 5)) & 31];
|
|
47
|
+
bits -= 5;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return out;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const unbase32 = (text) => {
|
|
54
|
+
let bits = 0;
|
|
55
|
+
let value = 0;
|
|
56
|
+
const out = [];
|
|
57
|
+
for (const c of text) {
|
|
58
|
+
const index = B32.indexOf(c);
|
|
59
|
+
if (index < 0) {
|
|
60
|
+
return null;
|
|
61
|
+
}
|
|
62
|
+
value = (value << 5) | index;
|
|
63
|
+
bits += 5;
|
|
64
|
+
if (bits >= 8) {
|
|
65
|
+
out.push((value >>> (bits - 8)) & 0xff);
|
|
66
|
+
bits -= 8;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
return Buffer.from(out);
|
|
70
|
+
};
|
|
71
|
+
|
|
72
|
+
const Tools = Object.freeze({
|
|
73
|
+
address: (pk) => {
|
|
74
|
+
const payload = Buffer.concat([Buffer.from([VERSION_G]), pk]);
|
|
75
|
+
const crc = crc16(payload);
|
|
76
|
+
return base32(Buffer.concat([payload, Buffer.from([crc & 0xff, crc >> 8])]));
|
|
77
|
+
},
|
|
78
|
+
canon: (claim) => Buffer.from(JSON.stringify(ordered(claim))),
|
|
79
|
+
derive: (root, label) => createHash('sha256').update(root).update(label).digest(),
|
|
80
|
+
digest: (bytes) => createHash('sha256').update(bytes).digest('hex'),
|
|
81
|
+
envelope: (voice, to, op, seq) => {
|
|
82
|
+
const from = Tools.address(voice.pk);
|
|
83
|
+
return { from, op, seq, sig: voice.sign(Tools.canon({ from, op, seq, to })), to };
|
|
84
|
+
},
|
|
85
|
+
pkOf: (address) => {
|
|
86
|
+
const bytes = typeof address === 'string' && address.length === 56 ? unbase32(address) : null;
|
|
87
|
+
if (bytes === null || bytes[0] !== VERSION_G) {
|
|
88
|
+
return null;
|
|
89
|
+
}
|
|
90
|
+
const payload = bytes.subarray(0, 33);
|
|
91
|
+
const checksum = bytes[33] | (bytes[34] << 8);
|
|
92
|
+
return crc16(payload) === checksum ? Buffer.from(payload.subarray(1)) : null;
|
|
93
|
+
},
|
|
94
|
+
seal: (secret, value) => {
|
|
95
|
+
const iv = randomBytes(12);
|
|
96
|
+
const cipher = createCipheriv('aes-256-gcm', secret, iv);
|
|
97
|
+
const sealed = Buffer.concat([cipher.update(JSON.stringify(value), 'utf8'), cipher.final()]);
|
|
98
|
+
return Buffer.concat([iv, cipher.getAuthTag(), sealed]).toString('base64');
|
|
99
|
+
},
|
|
100
|
+
secret: () => randomBytes(32),
|
|
101
|
+
unseal: (secret, sealed) => {
|
|
102
|
+
try {
|
|
103
|
+
const bytes = Buffer.from(sealed, 'base64');
|
|
104
|
+
const decipher = createDecipheriv('aes-256-gcm', secret, bytes.subarray(0, 12));
|
|
105
|
+
decipher.setAuthTag(bytes.subarray(12, 28));
|
|
106
|
+
return JSON.parse(
|
|
107
|
+
Buffer.concat([decipher.update(bytes.subarray(28)), decipher.final()]).toString('utf8'),
|
|
108
|
+
);
|
|
109
|
+
} catch {
|
|
110
|
+
return undefined;
|
|
111
|
+
}
|
|
112
|
+
},
|
|
113
|
+
verifies: (pk, bytes, sig) => {
|
|
114
|
+
try {
|
|
115
|
+
return verify(
|
|
116
|
+
null,
|
|
117
|
+
bytes,
|
|
118
|
+
createPublicKey({
|
|
119
|
+
key: Buffer.concat([ED25519_SPKI_PREFIX, pk]),
|
|
120
|
+
format: 'der',
|
|
121
|
+
type: 'spki',
|
|
122
|
+
}),
|
|
123
|
+
Buffer.from(sig, 'base64'),
|
|
124
|
+
);
|
|
125
|
+
} catch {
|
|
126
|
+
return false;
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
const Stamp = () => {
|
|
132
|
+
let last = 0;
|
|
133
|
+
return () => {
|
|
134
|
+
last = Math.max(last + 1, Date.now());
|
|
135
|
+
return last;
|
|
136
|
+
};
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
export { Stamp, Tools };
|
package/voice.mjs
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import {
|
|
2
|
+
createHash,
|
|
3
|
+
createPrivateKey,
|
|
4
|
+
createPublicKey,
|
|
5
|
+
diffieHellman,
|
|
6
|
+
generateKeyPairSync,
|
|
7
|
+
sign,
|
|
8
|
+
} from 'node:crypto';
|
|
9
|
+
import { Tools } from './tools.mjs';
|
|
10
|
+
|
|
11
|
+
const X25519_SPKI_PREFIX = Buffer.from('302a300506032b656e032100', 'hex');
|
|
12
|
+
|
|
13
|
+
const packed = (privateKey) =>
|
|
14
|
+
privateKey.export({ format: 'der', type: 'pkcs8' }).toString('base64');
|
|
15
|
+
|
|
16
|
+
const unpacked = (text) =>
|
|
17
|
+
createPrivateKey({ key: Buffer.from(text, 'base64'), format: 'der', type: 'pkcs8' });
|
|
18
|
+
|
|
19
|
+
const rawPk = (privateKey) =>
|
|
20
|
+
createPublicKey(privateKey).export({ format: 'der', type: 'spki' }).subarray(-32);
|
|
21
|
+
|
|
22
|
+
const agreed = (privateKey, rawPub) =>
|
|
23
|
+
createHash('sha256')
|
|
24
|
+
.update(
|
|
25
|
+
diffieHellman({
|
|
26
|
+
privateKey,
|
|
27
|
+
publicKey: createPublicKey({
|
|
28
|
+
key: Buffer.concat([X25519_SPKI_PREFIX, rawPub]),
|
|
29
|
+
format: 'der',
|
|
30
|
+
type: 'spki',
|
|
31
|
+
}),
|
|
32
|
+
}),
|
|
33
|
+
)
|
|
34
|
+
.digest();
|
|
35
|
+
|
|
36
|
+
const voiced = ({ present, next, box }) =>
|
|
37
|
+
Object.freeze({
|
|
38
|
+
boxPk: rawPk(box),
|
|
39
|
+
nextPkHash: Tools.digest(rawPk(next)),
|
|
40
|
+
open: (wrapped) => {
|
|
41
|
+
try {
|
|
42
|
+
const [ephemeral, sealed] = wrapped.split('.');
|
|
43
|
+
return Tools.unseal(agreed(box, Buffer.from(ephemeral, 'base64')), sealed);
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
},
|
|
48
|
+
pack: () => ({ box: packed(box), next: packed(next), present: packed(present) }),
|
|
49
|
+
pk: rawPk(present),
|
|
50
|
+
sign: (bytes) => sign(null, bytes, present).toString('base64'),
|
|
51
|
+
succeed: () =>
|
|
52
|
+
voiced({
|
|
53
|
+
box: generateKeyPairSync('x25519').privateKey,
|
|
54
|
+
next: generateKeyPairSync('ed25519').privateKey,
|
|
55
|
+
present: next,
|
|
56
|
+
}),
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
const Voice = Object.freeze(
|
|
60
|
+
Object.assign(
|
|
61
|
+
() =>
|
|
62
|
+
voiced({
|
|
63
|
+
box: generateKeyPairSync('x25519').privateKey,
|
|
64
|
+
next: generateKeyPairSync('ed25519').privateKey,
|
|
65
|
+
present: generateKeyPairSync('ed25519').privateKey,
|
|
66
|
+
}),
|
|
67
|
+
{
|
|
68
|
+
revive: (bundle) => {
|
|
69
|
+
try {
|
|
70
|
+
return voiced({
|
|
71
|
+
box: unpacked(bundle.box),
|
|
72
|
+
next: unpacked(bundle.next),
|
|
73
|
+
present: unpacked(bundle.present),
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
return undefined;
|
|
77
|
+
}
|
|
78
|
+
},
|
|
79
|
+
sealTo: (boxPk, value) => {
|
|
80
|
+
const ephemeral = generateKeyPairSync('x25519');
|
|
81
|
+
return `${rawPk(ephemeral.privateKey).toString('base64')}.${Tools.seal(
|
|
82
|
+
agreed(ephemeral.privateKey, boxPk),
|
|
83
|
+
value,
|
|
84
|
+
)}`;
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
),
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
export { Voice };
|
package/completion.js
DELETED
|
@@ -1,122 +0,0 @@
|
|
|
1
|
-
// nervur — shell completion (`nervur completion [bash|zsh]`). Client-local like
|
|
2
|
-
// scaffold: touches no carcass. One verb table is the single truth; both scripts
|
|
3
|
-
// are generated from it so a new verb lands in completion by editing one list.
|
|
4
|
-
// No prompt anywhere: the shell is a positional with a sane default ($SHELL).
|
|
5
|
-
|
|
6
|
-
import { appendFileSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
7
|
-
import { homedir } from 'node:os'
|
|
8
|
-
import { join } from 'node:path'
|
|
9
|
-
|
|
10
|
-
// verb → one-line description; subs/kinds drive second-position completion.
|
|
11
|
-
export const VERBS = [
|
|
12
|
-
['install', 'stand a published carcass at NERVUR_HOME'],
|
|
13
|
-
['up', '(dev bench) preflight, then stand the ground'],
|
|
14
|
-
['down', 'stop the ground'],
|
|
15
|
-
['reset', 'stop the ground and wipe its data'],
|
|
16
|
-
['preflight', 'run the bench preflight and report'],
|
|
17
|
-
['whoami', 'ask the carcass who it is'],
|
|
18
|
-
['status', 'ask the carcass for its status'],
|
|
19
|
-
['identity', 'manage identities (add · key · list · remove)'],
|
|
20
|
-
['use', 'select the identity the verbs act as'],
|
|
21
|
-
['add', 'add a being under the selected identity'],
|
|
22
|
-
['list', 'list the beings on the ground'],
|
|
23
|
-
['remove', 'remove a being'],
|
|
24
|
-
['scaffold', 'mint a species repo from a template'],
|
|
25
|
-
['deploy', 'grant + pin + run a species'],
|
|
26
|
-
['species', 'list deployed species and their pins'],
|
|
27
|
-
['completion', 'print the shell completion script (bash · zsh)'],
|
|
28
|
-
['help', 'show help']
|
|
29
|
-
]
|
|
30
|
-
|
|
31
|
-
const names = VERBS.map(([v]) => v).join(' ')
|
|
32
|
-
|
|
33
|
-
const bash = () => `# nervur completion (bash)
|
|
34
|
-
# load now: eval "$(nervur completion bash)"
|
|
35
|
-
# persist: nervur completion bash >> ~/.bashrc (or into bash_completion.d)
|
|
36
|
-
_nervur() {
|
|
37
|
-
local cur prev
|
|
38
|
-
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
39
|
-
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
40
|
-
case "$prev" in
|
|
41
|
-
identity) COMPREPLY=($(compgen -W "add key list remove" -- "$cur")); return ;;
|
|
42
|
-
completion) COMPREPLY=($(compgen -W "bash zsh" -- "$cur")); return ;;
|
|
43
|
-
add|--kind) COMPREPLY=($(compgen -W "empty" -- "$cur")); return ;;
|
|
44
|
-
--dir) COMPREPLY=($(compgen -d -- "$cur")); return ;;
|
|
45
|
-
esac
|
|
46
|
-
case "$cur" in
|
|
47
|
-
--*) COMPREPLY=($(compgen -W "--as --no-door --kind --dir --key" -- "$cur")); return ;;
|
|
48
|
-
esac
|
|
49
|
-
if [ "$COMP_CWORD" -eq 1 ]; then
|
|
50
|
-
COMPREPLY=($(compgen -W "${names}" -- "$cur"))
|
|
51
|
-
fi
|
|
52
|
-
}
|
|
53
|
-
complete -F _nervur nervur
|
|
54
|
-
`
|
|
55
|
-
|
|
56
|
-
const zsh = () => `#compdef nervur
|
|
57
|
-
# nervur completion (zsh)
|
|
58
|
-
# load now: eval "$(nervur completion zsh)"
|
|
59
|
-
# persist: nervur completion zsh > "\${fpath[1]}/_nervur" && rm -f ~/.zcompdump; compinit
|
|
60
|
-
_nervur() {
|
|
61
|
-
local -a verbs
|
|
62
|
-
verbs=(
|
|
63
|
-
${VERBS.map(([v, d]) => ` '${v}:${d.replace(/'/g, "'\\''")}'`).join('\n')}
|
|
64
|
-
)
|
|
65
|
-
if (( CURRENT == 2 )); then
|
|
66
|
-
_describe 'verb' verbs
|
|
67
|
-
return
|
|
68
|
-
fi
|
|
69
|
-
case "\${words[2]}" in
|
|
70
|
-
identity) (( CURRENT == 3 )) && _values 'sub' add key list remove ;;
|
|
71
|
-
completion) (( CURRENT == 3 )) && _values 'shell' bash zsh ;;
|
|
72
|
-
add) (( CURRENT == 3 )) && _values 'kind' empty ;;
|
|
73
|
-
scaffold) _arguments '--kind[template kind]:kind:(empty)' '--dir[target directory]:dir:_directories' ;;
|
|
74
|
-
up|preflight) _arguments '--no-door[stand on localhost only (headless/CI)]' ;;
|
|
75
|
-
esac
|
|
76
|
-
}
|
|
77
|
-
if (( ! $+functions[compdef] )); then
|
|
78
|
-
autoload -Uz compinit
|
|
79
|
-
compinit
|
|
80
|
-
fi
|
|
81
|
-
compdef _nervur nervur
|
|
82
|
-
`
|
|
83
|
-
|
|
84
|
-
export function completionScript(shell) {
|
|
85
|
-
const which = shell ?? (process.env.SHELL?.endsWith('zsh') ? 'zsh' : 'bash')
|
|
86
|
-
if (which === 'zsh') return zsh()
|
|
87
|
-
if (which === 'bash') return bash()
|
|
88
|
-
throw new Error(`usage: nervur completion [bash|zsh] — '${which}' is not a shell I know`)
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
const MARK_OPEN = '# >>> nervur completion >>>'
|
|
92
|
-
const MARK_CLOSE = '# <<< nervur completion <<<'
|
|
93
|
-
|
|
94
|
-
// Wire completion for the operator's shell: the script lands as a file in the
|
|
95
|
-
// nervur home and the rc file gains one marked source-block — sourcing a file
|
|
96
|
-
// costs nothing per shell start, where `eval "$(nervur completion)"` would
|
|
97
|
-
// spawn node every time. Idempotent by the marker block (the dev/hosts.sh
|
|
98
|
-
// idiom): re-wiring rewrites the block in place, never stacks a second one.
|
|
99
|
-
export function wireCompletion(home) {
|
|
100
|
-
const shell = process.env.SHELL?.endsWith('zsh')
|
|
101
|
-
? 'zsh'
|
|
102
|
-
: process.env.SHELL?.endsWith('bash')
|
|
103
|
-
? 'bash'
|
|
104
|
-
: null
|
|
105
|
-
if (!shell) return { wired: false }
|
|
106
|
-
mkdirSync(home, { recursive: true })
|
|
107
|
-
const scriptPath = join(home, `completion.${shell}`)
|
|
108
|
-
writeFileSync(scriptPath, completionScript(shell))
|
|
109
|
-
const rc = join(homedir(), shell === 'zsh' ? '.zshrc' : '.bashrc')
|
|
110
|
-
const block = `${MARK_OPEN}\n[ -f "${scriptPath}" ] && source "${scriptPath}"\n${MARK_CLOSE}\n`
|
|
111
|
-
const current = existsSync(rc) ? readFileSync(rc, 'utf8') : ''
|
|
112
|
-
if (current.includes(MARK_OPEN)) {
|
|
113
|
-
const next = current.replace(
|
|
114
|
-
current.slice(current.indexOf(MARK_OPEN), current.indexOf(MARK_CLOSE) + MARK_CLOSE.length + 1),
|
|
115
|
-
block
|
|
116
|
-
)
|
|
117
|
-
if (next !== current) writeFileSync(rc, next)
|
|
118
|
-
} else {
|
|
119
|
-
appendFileSync(rc, `${current && !current.endsWith('\n') ? '\n' : ''}\n${block}`)
|
|
120
|
-
}
|
|
121
|
-
return { wired: true, shell, rc, scriptPath }
|
|
122
|
-
}
|