supcoflow 0.1.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/README.md +83 -3
- package/dist/api.js +187 -6
- package/dist/argumentos.js +63 -3
- package/dist/candado.js +57 -0
- package/dist/cli.js +66 -5
- package/dist/comandos/clave-vigente.js +18 -0
- package/dist/comandos/claves.js +161 -0
- package/dist/comandos/configurar.js +187 -0
- package/dist/comandos/datos-y-politica.js +112 -0
- package/dist/comandos/describir.js +173 -3
- package/dist/comandos/guia.js +244 -0
- package/dist/comandos/proyecto-del-repo.js +26 -0
- package/dist/comandos/proyecto.js +213 -0
- package/dist/comandos/quien-soy.js +6 -13
- package/dist/comandos/repo-conectar.js +126 -0
- package/dist/entorno.js +323 -5
- package/dist/flags-por-comando.js +33 -0
- package/dist/guia.js +11 -0
- package/dist/idempotencia.js +88 -0
- package/dist/repo-local.js +68 -0
- package/dist/secreto-a-archivo.js +109 -0
- package/package.json +1 -1
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// El secreto de una clave de consulta, a un archivo que nombró la persona (§6.5, decisión 5 del bloque 4).
|
|
2
|
+
// Nunca sale por stdout: quien corre `claves crear` suele ser un asistente, y lo que lee queda en su
|
|
3
|
+
// contexto. El archivo es la ÚNICA copia, así que se escribe con las mismas defensas que las credenciales:
|
|
4
|
+
// fuera de un repo de git (un `git add -A` la publicaría), `0600`, sin atravesar enlaces y vía `.tmp` +
|
|
5
|
+
// `rename` (nunca queda un archivo a medio escribir).
|
|
6
|
+
//
|
|
7
|
+
// Lo que ya hay en el destino no se pisa salvo que sea una clave de LA MISMA fila (mismo sufijo): eso es
|
|
8
|
+
// la recuperación de un intento a medias, y `claves crear` sólo llega con el candado tomado. Cualquier otra
|
|
9
|
+
// cosa podría ser una clave vigente de otro proyecto. Y nada de lo que hay ahí llega a un mensaje.
|
|
10
|
+
//
|
|
11
|
+
// ─── Un destino ausente se publica sin pisar (revisión del 2026-10-16) ──────────
|
|
12
|
+
//
|
|
13
|
+
// El candado es por proyecto + NOMBRE de la clave, no por destino. Dos `claves crear` con nombres distintos y
|
|
14
|
+
// el mismo `--guardar-en` leían los dos «ausente» y hacían los dos `rename`: el segundo pisaba el archivo del
|
|
15
|
+
// primero, que igual confirmaba su clave, activa y sin nadie que tuviera el secreto. Por eso, cuando el
|
|
16
|
+
// destino estaba ausente, el temporal se publica con una operación que FALLA si ya existe (`link`); quien
|
|
17
|
+
// llega segundo recibe el mismo 4 de «ya tiene otra cosa» y no confirma. El `rename` que pisa queda sólo
|
|
18
|
+
// para la recuperación de la MISMA fila, que sí está bajo el candado de ese nombre.
|
|
19
|
+
import { randomUUID } from "node:crypto";
|
|
20
|
+
import path from "node:path";
|
|
21
|
+
import { dentroDeUnWorkingTree } from "./guardado/repo.js";
|
|
22
|
+
// Una clave ocupa 85 caracteres: 4 KB es de sobra, y algo más grande ya no es una clave.
|
|
23
|
+
const TOPE = 4 * 1024;
|
|
24
|
+
const CLAVE = /^scf_([0-9a-f]{16})_[0-9a-f]{64}$/;
|
|
25
|
+
const OCUPADO = { error: "el archivo ya tiene otra cosa; borralo o elegí otro destino", codigo: 4 };
|
|
26
|
+
export function crearGuardaDeSecretos(fs) {
|
|
27
|
+
function borrarTemporal(tmp) {
|
|
28
|
+
try {
|
|
29
|
+
fs.borrarSiExiste(tmp);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* best-effort */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function leer(ruta) {
|
|
36
|
+
const l = fs.leerSinSeguir(ruta, TOPE);
|
|
37
|
+
if (l.tipo === "ausente")
|
|
38
|
+
return { estado: "ausente" };
|
|
39
|
+
if (l.tipo !== "contenido")
|
|
40
|
+
return { estado: "otra_cosa" };
|
|
41
|
+
// Un salto final (el que escribe `guardar`, o el de un editor) se tolera; cualquier otra cosa no.
|
|
42
|
+
const m = CLAVE.exec(l.texto.endsWith("\n") ? l.texto.slice(0, -1) : l.texto);
|
|
43
|
+
return m ? { estado: "clave", claveId: `cla_${m[1]}`, secreto: m[0] } : { estado: "otra_cosa" };
|
|
44
|
+
}
|
|
45
|
+
function validarDestino(ruta) {
|
|
46
|
+
const destino = path.resolve(ruta);
|
|
47
|
+
// `realpath` lanza con un tramo que es un enlace roto: sin saber dónde cae, no se escribe.
|
|
48
|
+
let raiz;
|
|
49
|
+
try {
|
|
50
|
+
raiz = dentroDeUnWorkingTree(path.dirname(destino), fs);
|
|
51
|
+
}
|
|
52
|
+
catch {
|
|
53
|
+
return { error: "no pude resolver la ruta real de la carpeta (¿un enlace roto?)", codigo: 3 };
|
|
54
|
+
}
|
|
55
|
+
if (raiz !== null)
|
|
56
|
+
return { error: `cae dentro del repo de git ${raiz}, y una clave en un repo termina commiteada; elegí un destino fuera`, codigo: 3 };
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
return {
|
|
60
|
+
leer,
|
|
61
|
+
validarDestino,
|
|
62
|
+
guardar(ruta, claveId, secreto) {
|
|
63
|
+
const destino = path.resolve(ruta);
|
|
64
|
+
// También acá, aunque `claves crear` ya lo miró antes de pedir: entre una cosa y otra pasa tiempo.
|
|
65
|
+
const invalido = validarDestino(destino);
|
|
66
|
+
if (invalido !== null)
|
|
67
|
+
return invalido;
|
|
68
|
+
const hay = fs.leerSinSeguir(destino, TOPE);
|
|
69
|
+
if (hay.tipo === "enlace")
|
|
70
|
+
return { error: "el destino es un enlace; elegí un archivo común", codigo: 3 };
|
|
71
|
+
if (hay.tipo !== "ausente") {
|
|
72
|
+
const actual = leer(destino);
|
|
73
|
+
if (actual.estado !== "clave" || actual.claveId !== claveId)
|
|
74
|
+
return OCUPADO;
|
|
75
|
+
}
|
|
76
|
+
// El temporal en la MISMA carpeta (el `rename` sólo es atómico dentro del mismo disco), con nombre al
|
|
77
|
+
// azar y en exclusiva: `wx` no atraviesa un enlace que alguien dejara con ese nombre.
|
|
78
|
+
const tmp = path.join(path.dirname(destino), `.${path.basename(destino)}.${randomUUID()}.tmp`);
|
|
79
|
+
let publicado;
|
|
80
|
+
try {
|
|
81
|
+
fs.escribirExclusivo(tmp, secreto + "\n", 0o600);
|
|
82
|
+
if (hay.tipo === "ausente") {
|
|
83
|
+
publicado = fs.publicarSinPisar(tmp, destino);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
fs.renombrar(tmp, destino);
|
|
87
|
+
publicado = "publicado";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// El mensaje del sistema no se reenvía: podría traer la ruta del temporal o algo peor.
|
|
92
|
+
borrarTemporal(tmp);
|
|
93
|
+
return { error: "no se pudo escribir ahí (¿la carpeta existe y tenés permiso?)", codigo: 3 };
|
|
94
|
+
}
|
|
95
|
+
// Después de publicar, el temporal es un nombre de más (con `link`) o ya no existe (con `rename`): no
|
|
96
|
+
// borrarlo no cambia que la clave quedó en el destino, y convertir eso en un fallo dejaría una clave
|
|
97
|
+
// guardada sin confirmar.
|
|
98
|
+
borrarTemporal(tmp);
|
|
99
|
+
// Otro apareció entre la lectura y la publicación: lo suyo queda intacto y esta clave no se confirma.
|
|
100
|
+
if (publicado === "existe")
|
|
101
|
+
return OCUPADO;
|
|
102
|
+
// Sin caer a `rename`: eso reabre la carrera que `link` cierra. Otro destino, en otro disco, sirve.
|
|
103
|
+
if (publicado === "sin_enlaces") {
|
|
104
|
+
return { error: "el disco de esa carpeta no admite enlaces duros (¿FAT, exFAT o una carpeta de red?), y sin ellos no puedo escribir sin riesgo de pisar otra clave; elegí un destino en otro disco", codigo: 3 };
|
|
105
|
+
}
|
|
106
|
+
return "guardado";
|
|
107
|
+
},
|
|
108
|
+
};
|
|
109
|
+
}
|
package/package.json
CHANGED