specbox 6.14.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 +38 -0
- package/bin/specbox.mjs +202 -0
- package/lib/claude.mjs +167 -0
- package/lib/config.mjs +44 -0
- package/lib/device.mjs +68 -0
- package/lib/install.mjs +32 -0
- package/lib/internal.mjs +176 -0
- package/lib/login.mjs +87 -0
- package/lib/mcp-headers.mjs +53 -0
- package/lib/messages.mjs +124 -0
- package/lib/renew.mjs +130 -0
- package/lib/store.mjs +214 -0
- package/package.json +37 -0
package/lib/store.mjs
ADDED
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Almacén seguro de la credencial del dispositivo (UC-3904 AC-02).
|
|
3
|
+
*
|
|
4
|
+
* Una credencial por servidor MCP (clave = URL del servidor), guardada en el
|
|
5
|
+
* almacén del sistema:
|
|
6
|
+
*
|
|
7
|
+
* - macOS: Llavero (`security`). El secreto entra por stdin con
|
|
8
|
+
* `security -i`, nunca como argumento visible en `ps`.
|
|
9
|
+
* - Linux: Secret Service (`secret-tool`), secreto por stdin.
|
|
10
|
+
* - Windows: DPAPI del usuario (PowerShell), en ~/.specbox/credentials/.
|
|
11
|
+
* - Si no hay ninguno (Linux sin sesión gráfica, contenedores): un fichero
|
|
12
|
+
* ~/.specbox/credentials.json con permisos 0600. `specbox status` lo avisa.
|
|
13
|
+
*
|
|
14
|
+
* La credencial es un JSON (token, caducidad, dispositivo, persona) que se
|
|
15
|
+
* guarda en base64: así no hay comillas ni espacios que escapar.
|
|
16
|
+
*
|
|
17
|
+
* Todo es síncrono y sin dependencias: el ayudante que ejecuta Claude Code en
|
|
18
|
+
* cada conexión tiene 10 s y no puede instalar nada.
|
|
19
|
+
*/
|
|
20
|
+
import { spawnSync } from "node:child_process";
|
|
21
|
+
import { createHash } from "node:crypto";
|
|
22
|
+
import { chmodSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
|
|
25
|
+
export const SERVICE = "SpecBox MCP";
|
|
26
|
+
const SECRET_TOOL_SERVICE = "specbox-mcp";
|
|
27
|
+
|
|
28
|
+
/** Ejecuta un programa sin shell. Nunca lanza: devuelve el resultado o el error. */
|
|
29
|
+
export function runCommand(cmd, args, { input, env, cwd, timeout = 8_000 } = {}) {
|
|
30
|
+
const res = spawnSync(cmd, args, {
|
|
31
|
+
input,
|
|
32
|
+
cwd,
|
|
33
|
+
env: env ? { ...process.env, ...env } : process.env,
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
windowsHide: true,
|
|
36
|
+
timeout,
|
|
37
|
+
// Sin shell nunca: los argumentos (JSON de Claude Code) no se reinterpretan.
|
|
38
|
+
// En Windows, un `claude.cmd` de npm no arranca así (EINVAL) y se ofrece la
|
|
39
|
+
// orden para ejecutarla a mano; el `claude.exe` nativo sí.
|
|
40
|
+
});
|
|
41
|
+
return { status: res.status, stdout: res.stdout ?? "", stderr: res.stderr ?? "", error: res.error ?? null };
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function encode(credential) {
|
|
45
|
+
return Buffer.from(JSON.stringify(credential), "utf8").toString("base64");
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function decode(raw) {
|
|
49
|
+
const text = String(raw ?? "").trim();
|
|
50
|
+
if (!text) return null;
|
|
51
|
+
try {
|
|
52
|
+
return JSON.parse(Buffer.from(text, "base64").toString("utf8"));
|
|
53
|
+
} catch {
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function missing(res) {
|
|
59
|
+
return res.error?.code === "ENOENT";
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function keychainStore(run) {
|
|
63
|
+
return {
|
|
64
|
+
kind: "keychain",
|
|
65
|
+
read(account) {
|
|
66
|
+
const res = run("security", ["find-generic-password", "-s", SERVICE, "-a", account, "-w"]);
|
|
67
|
+
return res.status === 0 ? decode(res.stdout) : null;
|
|
68
|
+
},
|
|
69
|
+
write(account, credential) {
|
|
70
|
+
const res = run("security", ["-i"], {
|
|
71
|
+
input: `add-generic-password -U -s "${SERVICE}" -a "${account}" -w ${encode(credential)}\n`,
|
|
72
|
+
});
|
|
73
|
+
if (res.status !== 0 || /error/i.test(res.stderr)) throw new Error(`keychain write failed: ${res.stderr.trim()}`);
|
|
74
|
+
},
|
|
75
|
+
remove(account) {
|
|
76
|
+
run("security", ["delete-generic-password", "-s", SERVICE, "-a", account]);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function secretToolStore(run) {
|
|
82
|
+
const attrs = (account) => ["service", SECRET_TOOL_SERVICE, "account", account];
|
|
83
|
+
return {
|
|
84
|
+
kind: "secret-service",
|
|
85
|
+
read(account) {
|
|
86
|
+
const res = run("secret-tool", ["lookup", ...attrs(account)]);
|
|
87
|
+
if (missing(res)) throw Object.assign(new Error("secret-tool missing"), { code: "ENOENT" });
|
|
88
|
+
return res.status === 0 ? decode(res.stdout) : null;
|
|
89
|
+
},
|
|
90
|
+
write(account, credential) {
|
|
91
|
+
const res = run("secret-tool", ["store", `--label=${SERVICE}`, ...attrs(account)], { input: encode(credential) });
|
|
92
|
+
if (missing(res)) throw Object.assign(new Error("secret-tool missing"), { code: "ENOENT" });
|
|
93
|
+
if (res.status !== 0) throw new Error(`secret-tool store failed: ${res.stderr.trim()}`);
|
|
94
|
+
},
|
|
95
|
+
remove(account) {
|
|
96
|
+
run("secret-tool", ["clear", ...attrs(account)]);
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const PS_WRITE =
|
|
102
|
+
"Add-Type -AssemblyName System.Security; $d=[Console]::In.ReadToEnd().Trim(); " +
|
|
103
|
+
"$b=[Text.Encoding]::UTF8.GetBytes($d); " +
|
|
104
|
+
"$p=[Security.Cryptography.ProtectedData]::Protect($b,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser); " +
|
|
105
|
+
"[IO.File]::WriteAllBytes($env:SPECBOX_CRED_FILE,$p)";
|
|
106
|
+
const PS_READ =
|
|
107
|
+
"Add-Type -AssemblyName System.Security; $p=[IO.File]::ReadAllBytes($env:SPECBOX_CRED_FILE); " +
|
|
108
|
+
"$b=[Security.Cryptography.ProtectedData]::Unprotect($p,$null,[Security.Cryptography.DataProtectionScope]::CurrentUser); " +
|
|
109
|
+
"[Console]::Out.Write([Text.Encoding]::UTF8.GetString($b))";
|
|
110
|
+
|
|
111
|
+
function dpapiStore(run, home) {
|
|
112
|
+
const fileFor = (account) =>
|
|
113
|
+
join(home, "credentials", `${createHash("sha256").update(account).digest("hex").slice(0, 32)}.dpapi`);
|
|
114
|
+
return {
|
|
115
|
+
kind: "dpapi",
|
|
116
|
+
read(account) {
|
|
117
|
+
const res = run("powershell", ["-NoProfile", "-NonInteractive", "-Command", PS_READ], {
|
|
118
|
+
env: { SPECBOX_CRED_FILE: fileFor(account) },
|
|
119
|
+
});
|
|
120
|
+
return res.status === 0 ? decode(res.stdout) : null;
|
|
121
|
+
},
|
|
122
|
+
write(account, credential) {
|
|
123
|
+
mkdirSync(join(home, "credentials"), { recursive: true });
|
|
124
|
+
const res = run("powershell", ["-NoProfile", "-NonInteractive", "-Command", PS_WRITE], {
|
|
125
|
+
input: encode(credential),
|
|
126
|
+
env: { SPECBOX_CRED_FILE: fileFor(account) },
|
|
127
|
+
});
|
|
128
|
+
if (res.status !== 0) throw new Error(`DPAPI write failed: ${res.stderr.trim()}`);
|
|
129
|
+
},
|
|
130
|
+
remove(account) {
|
|
131
|
+
rmSync(fileFor(account), { force: true });
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export function fileStore(home) {
|
|
137
|
+
const file = join(home, "credentials.json");
|
|
138
|
+
const load = () => {
|
|
139
|
+
try {
|
|
140
|
+
return JSON.parse(readFileSync(file, "utf8"));
|
|
141
|
+
} catch {
|
|
142
|
+
return {};
|
|
143
|
+
}
|
|
144
|
+
};
|
|
145
|
+
const save = (all) => {
|
|
146
|
+
mkdirSync(home, { recursive: true, mode: 0o700 });
|
|
147
|
+
writeFileSync(file, JSON.stringify(all, null, 2), { mode: 0o600 });
|
|
148
|
+
chmodSync(file, 0o600);
|
|
149
|
+
};
|
|
150
|
+
return {
|
|
151
|
+
kind: "file",
|
|
152
|
+
read(account) {
|
|
153
|
+
return decode(load()[account]);
|
|
154
|
+
},
|
|
155
|
+
write(account, credential) {
|
|
156
|
+
save({ ...load(), [account]: encode(credential) });
|
|
157
|
+
},
|
|
158
|
+
remove(account) {
|
|
159
|
+
const all = load();
|
|
160
|
+
if (!(account in all)) return;
|
|
161
|
+
delete all[account];
|
|
162
|
+
save(all);
|
|
163
|
+
},
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Si el almacén del sistema no está o no responde (Linux sin sesión gráfica),
|
|
169
|
+
* se usa el fichero protegido. `kind` dice dónde quedó la última escritura.
|
|
170
|
+
*/
|
|
171
|
+
function withFallback(primary, fallback) {
|
|
172
|
+
let kind = primary.kind;
|
|
173
|
+
return {
|
|
174
|
+
get kind() {
|
|
175
|
+
return kind;
|
|
176
|
+
},
|
|
177
|
+
read(account) {
|
|
178
|
+
try {
|
|
179
|
+
const value = primary.read(account);
|
|
180
|
+
if (value) return value;
|
|
181
|
+
} catch {
|
|
182
|
+
// sin almacén del sistema: se mira el fichero
|
|
183
|
+
}
|
|
184
|
+
return fallback.read(account);
|
|
185
|
+
},
|
|
186
|
+
write(account, credential) {
|
|
187
|
+
try {
|
|
188
|
+
primary.write(account, credential);
|
|
189
|
+
kind = primary.kind;
|
|
190
|
+
return;
|
|
191
|
+
} catch {
|
|
192
|
+
// cae al fichero protegido
|
|
193
|
+
}
|
|
194
|
+
fallback.write(account, credential);
|
|
195
|
+
kind = fallback.kind;
|
|
196
|
+
},
|
|
197
|
+
remove(account) {
|
|
198
|
+
try {
|
|
199
|
+
primary.remove(account);
|
|
200
|
+
} catch {
|
|
201
|
+
// nada que borrar en el almacén del sistema
|
|
202
|
+
}
|
|
203
|
+
fallback.remove(account);
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
export function createStore({ platform = process.platform, run = runCommand, home } = {}) {
|
|
209
|
+
if (!home) throw new Error("createStore needs the specbox home");
|
|
210
|
+
if (platform === "darwin") return keychainStore(run);
|
|
211
|
+
if (platform === "win32") return dpapiStore(run, home);
|
|
212
|
+
if (platform === "linux") return withFallback(secretToolStore(run), fileStore(home));
|
|
213
|
+
return fileStore(home);
|
|
214
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "specbox",
|
|
3
|
+
"version": "6.14.0",
|
|
4
|
+
"description": "Conecta este ordenador a SpecBox sin copiar tokens: specbox login, status y logout.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"specbox": "bin/specbox.mjs"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin",
|
|
11
|
+
"lib",
|
|
12
|
+
"README.md"
|
|
13
|
+
],
|
|
14
|
+
"engines": {
|
|
15
|
+
"node": ">=18.17"
|
|
16
|
+
},
|
|
17
|
+
"scripts": {
|
|
18
|
+
"test": "node --test test/*.test.mjs"
|
|
19
|
+
},
|
|
20
|
+
"keywords": [
|
|
21
|
+
"specbox",
|
|
22
|
+
"mcp",
|
|
23
|
+
"claude-code",
|
|
24
|
+
"login"
|
|
25
|
+
],
|
|
26
|
+
"license": "MIT",
|
|
27
|
+
"author": "EmbedBuild",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "https://github.com/EmbedBuild/specbox-engine.git",
|
|
31
|
+
"directory": "packages/specbox-cli"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://cloud.specbox.build/como-se-conecta",
|
|
34
|
+
"bugs": {
|
|
35
|
+
"url": "https://github.com/EmbedBuild/specbox-engine/issues"
|
|
36
|
+
}
|
|
37
|
+
}
|