moshcode 0.66.0 → 0.67.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/README.md +76 -0
- package/bin/moshcode.mjs +5 -0
- package/package.json +2 -2
- package/src/cli-schema.mjs +16 -0
- package/src/commands.mjs +1 -0
- package/src/integrations.mjs +7 -2
- package/src/mcp-catalog.mjs +21 -1
- package/src/name-link.mjs +259 -0
- package/src/tools.mjs +13 -0
package/README.md
CHANGED
|
@@ -14,6 +14,28 @@ curl -fsSL https://moshcoding.com/install.sh | sh
|
|
|
14
14
|
Zero-dependency ESM — all it needs is Node.js 18+. Later: `… | sh -s -- update`
|
|
15
15
|
to upgrade, `… | sh -s -- remove` to uninstall.
|
|
16
16
|
|
|
17
|
+
## What is in this repo
|
|
18
|
+
|
|
19
|
+
The CLI is the root package — `bin/`, `src/`, `plugins/` — and that is what
|
|
20
|
+
`npm publish` and the installer above ship. Two apps live alongside it:
|
|
21
|
+
|
|
22
|
+
| path | what it is | deployed as |
|
|
23
|
+
|---|---|---|
|
|
24
|
+
| `apps/pwa` | the Moshpit registry and account app | pit.moshcode.sh, app.moshcode.sh |
|
|
25
|
+
| `apps/qryptchat-web` | QryptChat, the post-quantum messenger | qrypt.chat |
|
|
26
|
+
|
|
27
|
+
One repo, but **not one dependency tree**. Each app installs itself, with its
|
|
28
|
+
own lockfile, and the root `pnpm install` does not reach into either. That is
|
|
29
|
+
deliberate on both counts and `pnpm-workspace.yaml` says why. The practical
|
|
30
|
+
rule: run `pnpm install` in the directory you are working in, not at the root
|
|
31
|
+
and not the other way round.
|
|
32
|
+
|
|
33
|
+
The two apps share a feature rather than code — a person can sign in to
|
|
34
|
+
QryptChat by proving they hold a Moshpit name, which `moshcode name link`
|
|
35
|
+
produces the proof for. The shared half of that is still duplicated in both
|
|
36
|
+
trees; see `pnpm-workspace.yaml` for what has to be fixed before it can be
|
|
37
|
+
lifted into `packages/`.
|
|
38
|
+
|
|
17
39
|
## Commands
|
|
18
40
|
|
|
19
41
|
`moshcode help <command>` drills into any of these — flags, examples and all.
|
|
@@ -48,6 +70,7 @@ or miss one that does. A test fails the build when it drifts.
|
|
|
48
70
|
| `moshcode load` | account | bring your saved pit settings onto this machine |
|
|
49
71
|
| `moshcode console` | account | serve or connect to the browser terminal |
|
|
50
72
|
| `moshcode dns` | hosting | resolve Moshpit names on this machine |
|
|
73
|
+
| `moshcode name` | hosting | prove you hold a Moshpit name, so an app can use it as your identity |
|
|
51
74
|
| `moshcode doh` | hosting | run the DNS-over-HTTPS resolver |
|
|
52
75
|
| `moshcode site` <br>`serve` | hosting | install web-server config for a Moshpit name |
|
|
53
76
|
| `moshcode template` <br>`templates` | hosting | scaffold a stack for a Moshpit-hosted service |
|
|
@@ -590,6 +613,40 @@ moshcode ugig --json gigs list # arguments/output go straight to ugig
|
|
|
590
613
|
moshcode coinpay wallet balance # arguments/output go straight to coinpay
|
|
591
614
|
```
|
|
592
615
|
|
|
616
|
+
### BufferOverride — the failure in front of you, already answered
|
|
617
|
+
|
|
618
|
+
[BufferOverride](https://bufferoverride.com) is where humans and agents debug
|
|
619
|
+
together: every answer declares the versions it works on, who or what wrote it,
|
|
620
|
+
and whether anyone independent reproduced it. `bo` is that from a terminal.
|
|
621
|
+
|
|
622
|
+
```sh
|
|
623
|
+
moshcode install bo # npm i -g @profullstack/bufferoverride
|
|
624
|
+
|
|
625
|
+
moshcode bo run -- pnpm test # run it, keep what it printed, search for it
|
|
626
|
+
moshcode bo search "worker exited before finishing"
|
|
627
|
+
moshcode bo get a1b2c3d4e5 --markdown
|
|
628
|
+
```
|
|
629
|
+
|
|
630
|
+
The product is BufferOverride and the binary is `bo` — the same split
|
|
631
|
+
`secrets`/`logicsrc` and `spinifex`/`spx` have, keyed the short way round here
|
|
632
|
+
because this is a command you type every time something fails.
|
|
633
|
+
|
|
634
|
+
`bo run --` wraps a command rather than replacing it: the wrapped command's exit
|
|
635
|
+
code passes straight through, so it can go in front of something already in CI
|
|
636
|
+
without changing what CI sees. It captures stdout, stderr, the exit code, the
|
|
637
|
+
OS, the architecture and the detected dependency versions, redacts what it
|
|
638
|
+
recognises as a secret, and searches for the failure **before** offering to
|
|
639
|
+
publish it. Nothing leaves the machine until you have seen it, and outside a TTY
|
|
640
|
+
nothing is published at all unless you pass `--ask`.
|
|
641
|
+
|
|
642
|
+
Redaction is best effort and cannot be complete — no pattern list catches a
|
|
643
|
+
custom-format secret — so `--dry-run` is the habit its own docs ask for.
|
|
644
|
+
|
|
645
|
+
Reads need no credential: `search` and `get` work before you have ever run `bo
|
|
646
|
+
login`. Publishing needs one, and `bo login` is a device-code exchange, so a
|
|
647
|
+
terminal never handles a browser session. `bo mcp config` prints the MCP
|
|
648
|
+
registration for a coding agent, which is the same graph over a different door.
|
|
649
|
+
|
|
593
650
|
### Cloud + infra CLIs
|
|
594
651
|
|
|
595
652
|
```sh
|
|
@@ -1038,6 +1095,7 @@ Some MCP servers are worth remembering by name rather than by npx invocation:
|
|
|
1038
1095
|
```sh
|
|
1039
1096
|
moshcode mcp catalog # what we know how to run
|
|
1040
1097
|
moshcode mcp add porkbun # expands to: npx -y @porkbunllc/mcp-server
|
|
1098
|
+
moshcode mcp add bufferoverride # expands to: https://bufferoverride.com/mcp
|
|
1041
1099
|
```
|
|
1042
1100
|
|
|
1043
1101
|
That registers it across every engine that supports MCP (claude, gemini, qwen,
|
|
@@ -1059,6 +1117,23 @@ from and five to rotate. Porkbun's API access is off by default and enabled
|
|
|
1059
1117
|
per-domain — and its documentation tools work with no keys at all, which is a
|
|
1060
1118
|
sensible way to try the server before trusting it with DNS writes.
|
|
1061
1119
|
|
|
1120
|
+
**BufferOverride is the useful-unauthenticated one.** Five read tools
|
|
1121
|
+
(`search_questions`, `get_question`, `list_questions`, `list_tags`, `whoami`)
|
|
1122
|
+
work with no credential, and the write tools are gated on the scopes a key
|
|
1123
|
+
actually carries — `tools/list` advertises only what your key can use. So the
|
|
1124
|
+
bare `mcp add bufferoverride` above is a complete, working registration. To
|
|
1125
|
+
publish from an engine, add the credential as a header:
|
|
1126
|
+
|
|
1127
|
+
```sh
|
|
1128
|
+
moshcode mcp add bufferoverride -H "Authorization: Bearer bo_..."
|
|
1129
|
+
```
|
|
1130
|
+
|
|
1131
|
+
`bo mcp config` prints the same thing from a terminal that has already signed
|
|
1132
|
+
in, and `bo mcp config --no-token` prints a form safe to paste in public. It is
|
|
1133
|
+
registered under the name the CLI uses, so both routes produce one server rather
|
|
1134
|
+
than two. See [BufferOverride](#bufferoverride--the-failure-in-front-of-you-already-answered)
|
|
1135
|
+
above for the CLI itself.
|
|
1136
|
+
|
|
1062
1137
|
## Claude Code plugins
|
|
1063
1138
|
|
|
1064
1139
|
MoshCode publishes its own plugin marketplace, so the pit's slash commands work
|
|
@@ -1371,6 +1446,7 @@ chmod +x deploy.mosh
|
|
|
1371
1446
|
| `coinpay(args…)` | drive the coinpay workflow CLI |
|
|
1372
1447
|
| `c0mpute(args…)` | drive the c0mpute workflow CLI |
|
|
1373
1448
|
| `c0upons(args…)` | drive the c0upons workflow CLI |
|
|
1449
|
+
| `bo(args…)` | drive the BufferOverride CLI (capture a failure, search, ask, answer, verify) |
|
|
1374
1450
|
| `secrets(args…)` | drive the logicsrc secrets CLI |
|
|
1375
1451
|
| `railway(args…)` | drive the Railway CLI |
|
|
1376
1452
|
| `gh(args…)` | drive the GitHub CLI |
|
package/bin/moshcode.mjs
CHANGED
|
@@ -33,6 +33,7 @@ import { consoleCommand } from "../src/console.mjs";
|
|
|
33
33
|
import { herdCommand, herdStart, splitDetachArgs } from "../src/herd-cli.mjs";
|
|
34
34
|
import { detectSubstrate, substrateNote } from "../src/herd.mjs";
|
|
35
35
|
import { dnsCommand } from "../src/dns.mjs";
|
|
36
|
+
import { nameCommand } from "../src/name-link.mjs";
|
|
36
37
|
import { templateCommand } from "../src/templates.mjs";
|
|
37
38
|
import { serveCommand } from "../src/serve.mjs";
|
|
38
39
|
import { createDohServer, nginxDohSite, parseDohPort, parseGuardArgs, DEFAULT_DOH_PORT, DOH_PATH } from "../src/doh-server.mjs";
|
|
@@ -545,6 +546,10 @@ async function main() {
|
|
|
545
546
|
process.exitCode = (await dnsCommand(rest)) || 0;
|
|
546
547
|
return;
|
|
547
548
|
}
|
|
549
|
+
if (cmd === "name") {
|
|
550
|
+
process.exitCode = (await nameCommand(rest)) || 0;
|
|
551
|
+
return;
|
|
552
|
+
}
|
|
548
553
|
if (cmd === "doh") {
|
|
549
554
|
const nameAt = rest.indexOf("--nginx");
|
|
550
555
|
if (nameAt >= 0) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "moshcode",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.67.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "moshcode — a metal wrapper for coding engines and native UGig/CoinPay workflow CLIs, with OpenPRD and moshscript",
|
|
6
6
|
"repository": {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"start": "node bin/moshcode.mjs",
|
|
20
20
|
"demo": "node bin/moshcode.mjs run examples/alive.mosh",
|
|
21
|
-
"test": "node --test"
|
|
21
|
+
"test": "node --test \"test/**/*.test.mjs\" \"tests/**/*.test.mjs\" \"apps/pwa/test/**/*.test.mjs\""
|
|
22
22
|
},
|
|
23
23
|
"files": [
|
|
24
24
|
"bin",
|
package/src/cli-schema.mjs
CHANGED
|
@@ -394,6 +394,22 @@ export const CORE_CLI_COMMANDS = [
|
|
|
394
394
|
],
|
|
395
395
|
seeAlso: ["doh", "site"],
|
|
396
396
|
},
|
|
397
|
+
{
|
|
398
|
+
name: "name",
|
|
399
|
+
group: "hosting",
|
|
400
|
+
description: "prove you hold a Moshpit name, so an app can use it as your identity",
|
|
401
|
+
synopsis: [["moshcode name link <name> [--app <url>]", ""]],
|
|
402
|
+
flags: [
|
|
403
|
+
["--app <url>", "the app to prove the name to", "https://qrypt.chat"],
|
|
404
|
+
["--dir <path>", "where the name's key and certificate live", "/etc/ssl/moshpit"],
|
|
405
|
+
["--json", "print the proof bundle as machine-readable JSON", ""],
|
|
406
|
+
],
|
|
407
|
+
examples: [
|
|
408
|
+
["sudo moshcode name link chovy.hacker", "the key is root-owned"],
|
|
409
|
+
["sudo moshcode name link blue.eggs --json", "hand the bundle to a script"],
|
|
410
|
+
],
|
|
411
|
+
seeAlso: ["dns", "site", "whoami"],
|
|
412
|
+
},
|
|
397
413
|
{
|
|
398
414
|
name: "doh",
|
|
399
415
|
group: "hosting",
|
package/src/commands.mjs
CHANGED
|
@@ -668,6 +668,7 @@ const COMMANDS = [
|
|
|
668
668
|
cliVerb("coinpay", "drive the coinpay workflow CLI"),
|
|
669
669
|
cliVerb("c0mpute", "drive the c0mpute workflow CLI"),
|
|
670
670
|
cliVerb("c0upons", "drive the c0upons workflow CLI"),
|
|
671
|
+
cliVerb("bo", "drive the BufferOverride CLI (capture a failure, search, ask, answer, verify)"),
|
|
671
672
|
cliVerb("secrets", "manage/view team secrets via logicsrc (login, teams, credentials)"),
|
|
672
673
|
cliVerb("railway", "drive the Railway CLI (deploys, services, env vars)"),
|
|
673
674
|
cliVerb("gh", "drive the GitHub CLI (repos, PRs, issues, releases)"),
|
package/src/integrations.mjs
CHANGED
|
@@ -238,9 +238,14 @@ export async function mcpCommand(tokens, { run, installedSet } = {}) {
|
|
|
238
238
|
const missing = (parsed.catalog?.env || []).filter((k) => !process.env[k]);
|
|
239
239
|
if (missing.length) {
|
|
240
240
|
console.log(ash(` note: ${spec.name} needs ${missing.join(" and ")} in the environment.`));
|
|
241
|
-
if (parsed.catalog?.note) console.log(ash(` ${parsed.catalog.note}`));
|
|
242
|
-
if (parsed.catalog?.docs) console.log(ash(` ${parsed.catalog.docs}`));
|
|
243
241
|
}
|
|
242
|
+
// The catalog's own note and docs are printed whenever the catalog was used,
|
|
243
|
+
// not only when a variable is missing. A server whose credential is a header
|
|
244
|
+
// rather than an environment variable — or one that needs none at all to be
|
|
245
|
+
// useful — has nothing in `env`, and hanging its note off that check is what
|
|
246
|
+
// made the note invisible for exactly the servers it was written for.
|
|
247
|
+
if (parsed.catalog?.note) console.log(ash(` note: ${parsed.catalog.note}`));
|
|
248
|
+
if (parsed.catalog?.docs) console.log(ash(` ${parsed.catalog.docs}`));
|
|
244
249
|
if (spec.headers.length || /^https?:/i.test(spec.target)) {
|
|
245
250
|
console.log(ash(" note: OAuth/HTTP servers may still need per-engine auth (e.g. `opencode mcp auth`, `codex mcp login`)."));
|
|
246
251
|
}
|
package/src/mcp-catalog.mjs
CHANGED
|
@@ -24,6 +24,23 @@ export const MCP_CATALOG = {
|
|
|
24
24
|
// before deciding whether to trust it with keys.
|
|
25
25
|
note: "API access is off by default and must be enabled per-domain; the docs tools work without keys",
|
|
26
26
|
},
|
|
27
|
+
bufferoverride: {
|
|
28
|
+
// A remote HTTP server, so the target is the URL and there are no args —
|
|
29
|
+
// every engine's builder pushes the target alone for a remote server.
|
|
30
|
+
target: "https://bufferoverride.com/mcp",
|
|
31
|
+
args: [],
|
|
32
|
+
desc: "BufferOverride — version-aware technical answers, with provenance and reproductions",
|
|
33
|
+
docs: "https://bufferoverride.com/docs/mcp",
|
|
34
|
+
// Named to match what the CLI's own `bo mcp config` emits, so registering
|
|
35
|
+
// it either way produces one server rather than two under different names.
|
|
36
|
+
//
|
|
37
|
+
// No `env`: the credential is a bearer header, not a variable, and it is
|
|
38
|
+
// deliberately not listed here. Five read tools work with no key at all,
|
|
39
|
+
// and the write tools are gated on the scopes a key actually carries — so
|
|
40
|
+
// the useful default really is unauthenticated. `bo mcp config` prints the
|
|
41
|
+
// header form for a terminal that has signed in.
|
|
42
|
+
note: "reads need no credential; to publish, add -H \"Authorization: Bearer bo_…\" (see `bo mcp config`)",
|
|
43
|
+
},
|
|
27
44
|
};
|
|
28
45
|
|
|
29
46
|
/** Resolve a catalog name to a spec fragment, or null. Own properties only. */
|
|
@@ -44,7 +61,10 @@ export function catalogNames() {
|
|
|
44
61
|
|
|
45
62
|
/** One line per known server, for `mcp catalog`. */
|
|
46
63
|
export function catalogList() {
|
|
64
|
+
// Width from the longest name rather than a fixed pad: `bufferoverride` is
|
|
65
|
+
// wider than the old 10, and a name that overruns the pad loses the column.
|
|
66
|
+
const width = Math.max(10, ...Object.keys(MCP_CATALOG).map((key) => key.length));
|
|
47
67
|
return Object.entries(MCP_CATALOG)
|
|
48
|
-
.map(([key, e]) => ` ${key.padEnd(
|
|
68
|
+
.map(([key, e]) => ` ${key.padEnd(width)} ${e.desc}`)
|
|
49
69
|
.join("\n");
|
|
50
70
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
// `moshcode name link <name>` — prove you hold a Moshpit name, so an app can
|
|
2
|
+
// use it as your identity.
|
|
3
|
+
//
|
|
4
|
+
// The registry already publishes, per name, the SHA-256 of the SubjectPublicKeyInfo
|
|
5
|
+
// that name's certificate must present (see src/pins.mjs). That pin is a
|
|
6
|
+
// name-to-key binding nobody else can forge, which makes it a credential: sign
|
|
7
|
+
// something with the pinned key and you have proved you hold the name.
|
|
8
|
+
//
|
|
9
|
+
// What this does NOT do is generate the app's encryption key. The pinned key is
|
|
10
|
+
// P-256 and it signs; a messenger's key is ML-KEM-1024 and it encrypts, and a
|
|
11
|
+
// KEM key cannot sign at all. They are two keys with two jobs, and the private
|
|
12
|
+
// half of the second one belongs on the device the person actually reads
|
|
13
|
+
// messages on — not here. So this emits a proof bundle and stops. The app posts
|
|
14
|
+
// it alongside a key it generated itself and never showed anyone.
|
|
15
|
+
//
|
|
16
|
+
// The bundle is single-use and short-lived: the challenge it answers is burned
|
|
17
|
+
// on redemption, so a copy of it is worth nothing once used.
|
|
18
|
+
|
|
19
|
+
import fs from "node:fs";
|
|
20
|
+
import crypto from "node:crypto";
|
|
21
|
+
|
|
22
|
+
/** Where an app that speaks this protocol lives, unless told otherwise. */
|
|
23
|
+
export const DEFAULT_APP = "https://qrypt.chat";
|
|
24
|
+
|
|
25
|
+
/** Where moshcode's own tooling writes a name's key and certificate. */
|
|
26
|
+
export const DEFAULT_KEY_DIR = "/etc/ssl/moshpit";
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* A name is `<label>.<tld>`, lowercase.
|
|
30
|
+
*
|
|
31
|
+
* Empty segments are refused rather than collapsed. `chovy..hacker` folding
|
|
32
|
+
* into `chovy.hacker` is harmless in a filename and is a way in when the string
|
|
33
|
+
* is an identity.
|
|
34
|
+
*
|
|
35
|
+
* @param {unknown} input
|
|
36
|
+
* @returns {string | null}
|
|
37
|
+
*/
|
|
38
|
+
export function normalizeName(input) {
|
|
39
|
+
const clean = String(input ?? "").trim().toLowerCase().replace(/\.$/, "");
|
|
40
|
+
if (!clean) return null;
|
|
41
|
+
const parts = clean.split(".");
|
|
42
|
+
if (parts.length !== 2 || parts.some((p) => !p)) return null;
|
|
43
|
+
if (!parts.every((p) => /^[a-z0-9-]+$/.test(p) && !p.startsWith("-") && !p.endsWith("-"))) return null;
|
|
44
|
+
return parts.join(".");
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Where a name's key and certificate live.
|
|
49
|
+
*
|
|
50
|
+
* Mirrors keyPaths() in src/pins.mjs, including that separators which could
|
|
51
|
+
* climb out of the directory are dropped rather than escaped.
|
|
52
|
+
*
|
|
53
|
+
* @param {string} name
|
|
54
|
+
* @param {string} [dir]
|
|
55
|
+
*/
|
|
56
|
+
export function keyPaths(name, dir = DEFAULT_KEY_DIR) {
|
|
57
|
+
const safe = String(name ?? "")
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9.-]/g, "")
|
|
60
|
+
.replace(/\.{2,}/g, ".")
|
|
61
|
+
.replace(/^[.-]+|[.-]+$/g, "");
|
|
62
|
+
if (!safe) return null;
|
|
63
|
+
return { key: `${dir}/${safe}.key`, cert: `${dir}/${safe}.crt` };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Sign a challenge with a name's private key.
|
|
68
|
+
*
|
|
69
|
+
* DER, because that is what `crypto.verify` reads back with `dsaEncoding: 'der'`
|
|
70
|
+
* on the other side. A P-1363 signature is the same numbers in a shape the
|
|
71
|
+
* verifier rejects, and the failure looks identical to a wrong key.
|
|
72
|
+
*
|
|
73
|
+
* @param {{keyPem: string, nonce: string}} args
|
|
74
|
+
* @returns {string} base64 signature
|
|
75
|
+
*/
|
|
76
|
+
export function signChallenge({ keyPem, nonce }) {
|
|
77
|
+
const key = crypto.createPrivateKey(keyPem);
|
|
78
|
+
return crypto
|
|
79
|
+
.sign("sha256", Buffer.from(nonce, "utf8"), { key, dsaEncoding: "der" })
|
|
80
|
+
.toString("base64");
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The pin a certificate's key hashes to — SHA-256 over the SPKI, base64.
|
|
85
|
+
* Shown so the operator can eyeball it against what the registry publishes
|
|
86
|
+
* before wondering why a proof was refused.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} certPem
|
|
89
|
+
*/
|
|
90
|
+
export function pinOf(certPem) {
|
|
91
|
+
const cert = new crypto.X509Certificate(certPem);
|
|
92
|
+
const der = cert.publicKey.export({ type: "spki", format: "der" });
|
|
93
|
+
return crypto.createHash("sha256").update(der).digest("base64");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Ask an app for a challenge to sign.
|
|
98
|
+
*
|
|
99
|
+
* @param {{app: string, name: string, fetchImpl?: typeof fetch}} args
|
|
100
|
+
*/
|
|
101
|
+
export async function fetchChallenge({ app, name, fetchImpl = fetch }) {
|
|
102
|
+
const url = `${app.replace(/\/+$/, "")}/api/auth/moshpit/challenge`;
|
|
103
|
+
const res = await fetchImpl(url, {
|
|
104
|
+
method: "POST",
|
|
105
|
+
headers: { "content-type": "application/json" },
|
|
106
|
+
body: JSON.stringify({ name }),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
let body = null;
|
|
110
|
+
try {
|
|
111
|
+
body = await res.json();
|
|
112
|
+
} catch {
|
|
113
|
+
body = null;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!res.ok) {
|
|
117
|
+
throw new Error(body?.error || `${app} answered ${res.status}`);
|
|
118
|
+
}
|
|
119
|
+
if (!body?.jti || !body?.nonce) {
|
|
120
|
+
throw new Error(`${app} did not return a challenge`);
|
|
121
|
+
}
|
|
122
|
+
return body;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Read a name's key and certificate off disk, with the failure a person can act on.
|
|
127
|
+
*
|
|
128
|
+
* The key is root-owned, so "permission denied" is the expected first
|
|
129
|
+
* experience and deserves the fix rather than the errno.
|
|
130
|
+
*
|
|
131
|
+
* @param {{name: string, keyPath: string, certPath: string, readFile?: (p: string, e: string) => string}} args
|
|
132
|
+
*/
|
|
133
|
+
export function readNameKey({ name, keyPath, certPath, readFile = (p, e) => fs.readFileSync(p, e) }) {
|
|
134
|
+
let keyPem;
|
|
135
|
+
try {
|
|
136
|
+
keyPem = readFile(keyPath, "utf8");
|
|
137
|
+
} catch (error) {
|
|
138
|
+
if (error.code === "EACCES") {
|
|
139
|
+
throw new Error(`cannot read ${keyPath} — it is root-owned, so run this with sudo`);
|
|
140
|
+
}
|
|
141
|
+
if (error.code === "ENOENT") {
|
|
142
|
+
throw new Error(`no key for ${name} at ${keyPath} — mint one before linking it`);
|
|
143
|
+
}
|
|
144
|
+
throw error;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
let certPem;
|
|
148
|
+
try {
|
|
149
|
+
certPem = readFile(certPath, "utf8");
|
|
150
|
+
} catch (error) {
|
|
151
|
+
if (error.code === "ENOENT") {
|
|
152
|
+
throw new Error(`no certificate for ${name} at ${certPath}`);
|
|
153
|
+
}
|
|
154
|
+
throw error;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return { keyPem, certPem };
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Parse `name link <name> [--app url] [--dir path] [--json]`.
|
|
162
|
+
* @param {string[]} argv
|
|
163
|
+
*/
|
|
164
|
+
export function parseArgs(argv) {
|
|
165
|
+
const flag = (name, fallback) => {
|
|
166
|
+
const at = argv.indexOf(`--${name}`);
|
|
167
|
+
return at >= 0 && argv[at + 1] ? argv[at + 1] : fallback;
|
|
168
|
+
};
|
|
169
|
+
const positional = argv.filter((a, i) => {
|
|
170
|
+
if (a.startsWith("--")) return false;
|
|
171
|
+
// Skip a value that belongs to the flag before it.
|
|
172
|
+
return !(i > 0 && argv[i - 1].startsWith("--") && argv[i - 1] !== "--json");
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
verb: positional[0] || "",
|
|
177
|
+
name: positional[1] || "",
|
|
178
|
+
app: flag("app", DEFAULT_APP),
|
|
179
|
+
dir: flag("dir", DEFAULT_KEY_DIR),
|
|
180
|
+
json: argv.includes("--json"),
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* `moshcode name link <name>` — fetch a challenge, sign it, print the bundle.
|
|
186
|
+
*
|
|
187
|
+
* @param {string[]} argv
|
|
188
|
+
* @param {{out?: (s: string) => void, err?: (s: string) => void, fetchImpl?: typeof fetch,
|
|
189
|
+
* readFile?: (p: string, e: string) => string}} [io]
|
|
190
|
+
* @returns {Promise<number>} exit code
|
|
191
|
+
*/
|
|
192
|
+
export async function nameCommand(argv, io = {}) {
|
|
193
|
+
const out = io.out || ((s) => console.log(s));
|
|
194
|
+
const err = io.err || ((s) => console.error(s));
|
|
195
|
+
const { verb, name: raw, app, dir, json } = parseArgs(argv);
|
|
196
|
+
|
|
197
|
+
if (verb !== "link") {
|
|
198
|
+
err("usage: moshcode name link <name> [--app <url>] [--dir <path>] [--json]");
|
|
199
|
+
return 1;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const name = normalizeName(raw);
|
|
203
|
+
if (!name) {
|
|
204
|
+
err(`not a Moshpit name: ${raw || "(none)"} — expected <label>.<tld>`);
|
|
205
|
+
return 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const paths = keyPaths(name, dir);
|
|
209
|
+
if (!paths) {
|
|
210
|
+
err(`not a Moshpit name: ${raw}`);
|
|
211
|
+
return 1;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
let keyPem;
|
|
215
|
+
let certPem;
|
|
216
|
+
try {
|
|
217
|
+
({ keyPem, certPem } = readNameKey({
|
|
218
|
+
name,
|
|
219
|
+
keyPath: paths.key,
|
|
220
|
+
certPath: paths.cert,
|
|
221
|
+
readFile: io.readFile,
|
|
222
|
+
}));
|
|
223
|
+
} catch (error) {
|
|
224
|
+
err(error.message);
|
|
225
|
+
return 1;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
let challenge;
|
|
229
|
+
try {
|
|
230
|
+
challenge = await fetchChallenge({ app, name, fetchImpl: io.fetchImpl });
|
|
231
|
+
} catch (error) {
|
|
232
|
+
err(`could not get a challenge from ${app}: ${error.message}`);
|
|
233
|
+
return 1;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let signature;
|
|
237
|
+
try {
|
|
238
|
+
signature = signChallenge({ keyPem, nonce: challenge.nonce });
|
|
239
|
+
} catch (error) {
|
|
240
|
+
err(`could not sign with ${paths.key}: ${error.message}`);
|
|
241
|
+
return 1;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const bundle = { jti: challenge.jti, name, certPem, signature };
|
|
245
|
+
|
|
246
|
+
if (json) {
|
|
247
|
+
out(JSON.stringify(bundle));
|
|
248
|
+
return 0;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
out(`Proved ${name} against ${app}`);
|
|
252
|
+
out(` pin ${pinOf(certPem)}`);
|
|
253
|
+
out(` expires ${challenge.expiresAt || "shortly"}`);
|
|
254
|
+
out("");
|
|
255
|
+
out("Paste this into the app to finish linking. It is single-use:");
|
|
256
|
+
out("");
|
|
257
|
+
out(JSON.stringify(bundle));
|
|
258
|
+
return 0;
|
|
259
|
+
}
|
package/src/tools.mjs
CHANGED
|
@@ -82,6 +82,19 @@ export const TOOLS = {
|
|
|
82
82
|
// where those words mean something else entirely.
|
|
83
83
|
aliases: { cmd: "cli-tools", args: ["aliases", "--json"] },
|
|
84
84
|
},
|
|
85
|
+
bo: {
|
|
86
|
+
desc: "BufferOverride — capture a failing command, redact it, and find the answer that already exists",
|
|
87
|
+
// The product is BufferOverride and the binary is `bo`, the same split
|
|
88
|
+
// `secrets` → `logicsrc` and `spinifex` → `spx` have. It is keyed the other
|
|
89
|
+
// way round from those two on purpose: `bo` is what its own documentation
|
|
90
|
+
// tells you to type, and this is a command you run every time something
|
|
91
|
+
// fails, so the short word is the one worth having in the pit.
|
|
92
|
+
bin: "bo",
|
|
93
|
+
// An ordinary global npm package with no dependencies of its own, and
|
|
94
|
+
// `npm install -g` is idempotent, so re-running the install IS the upgrade
|
|
95
|
+
// — no `upgrade` key, the same as mcpjam and railway.
|
|
96
|
+
install: { cmd: "npm", args: ["install", "-g", "@profullstack/bufferoverride"] },
|
|
97
|
+
},
|
|
85
98
|
secrets: {
|
|
86
99
|
desc: "LogicSRC — end-to-end-encrypted team credential sharing (login, teams, credentials)",
|
|
87
100
|
// The passthrough target is the `logicsrc` binary; the moshcode command is
|