@tc-libs/env 0.0.0-stage → 5.11.1
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 +104 -2
- package/package.json +21 -4
- package/src/cli.d.ts +7 -0
- package/src/cli.js +64 -0
- package/src/cli.js.map +1 -0
- package/src/env.error.d.ts +3 -0
- package/src/env.error.js +12 -0
- package/src/env.error.js.map +1 -0
- package/src/env.file.d.ts +2 -0
- package/src/env.file.js +56 -0
- package/src/env.file.js.map +1 -0
- package/src/env.pull.d.ts +11 -0
- package/src/env.pull.js +97 -0
- package/src/env.pull.js.map +1 -0
- package/src/env.schema.d.ts +3 -0
- package/src/env.schema.js +19 -0
- package/src/env.schema.js.map +1 -0
- package/src/index.d.ts +4 -0
- package/src/index.js +8 -0
- package/src/index.js.map +1 -0
package/README.md
CHANGED
|
@@ -1,3 +1,105 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @tc-libs/env
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Configurazione d'ambiente con i segreti fuori da git. La configurazione non segreta (URL, flag, porte, TTL) resta
|
|
4
|
+
versionata nei file env del progetto; i segreti stanno in AWS Secrets Manager, un segreto JSON per servizio e ambiente,
|
|
5
|
+
per esempio `borgarello/dev/core`. Sulle VM il `.env` lo compone il deploy; questo pacchetto serve al codice e allo
|
|
6
|
+
sviluppo locale:
|
|
7
|
+
|
|
8
|
+
- `secret()` e `secretKeys()` marcano e leggono le chiavi segrete nello schema Joi del progetto, che resta l'unico
|
|
9
|
+
elenco delle variabili;
|
|
10
|
+
- `secretKeysIn()` è il controllo per i test: un file env versionato non deve definire chiavi segrete;
|
|
11
|
+
- la CLI `tc-env pull` compone un `.env` dalla configurazione versionata e dal segreto.
|
|
12
|
+
|
|
13
|
+
## Chiavi segrete nello schema
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import Joi from 'joi';
|
|
17
|
+
import { secret } from '@tc-libs/env';
|
|
18
|
+
|
|
19
|
+
export const envSchema = Joi.object({
|
|
20
|
+
APP_NAME: Joi.string().required(),
|
|
21
|
+
DATABASE_PASSWORD: secret(Joi.string().required()),
|
|
22
|
+
AUTH_JWT_ACCESS_TOKEN_SECRET_KEY: secret(Joi.string().min(32).required()),
|
|
23
|
+
});
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
`secret()` aggiunge `.meta({ secret: true })` e non cambia la validazione.
|
|
27
|
+
|
|
28
|
+
## Controllo nei test
|
|
29
|
+
|
|
30
|
+
```ts
|
|
31
|
+
import { readFileSync } from 'node:fs';
|
|
32
|
+
import { secretKeys, secretKeysIn } from '@tc-libs/env';
|
|
33
|
+
import { envSchema } from './validation';
|
|
34
|
+
|
|
35
|
+
it.each(['.env.dev', '.env.staging', '.env.prod'])(
|
|
36
|
+
'%s non definisce chiavi segrete',
|
|
37
|
+
(file) => {
|
|
38
|
+
expect(
|
|
39
|
+
secretKeysIn(readFileSync(file, 'utf8'), secretKeys(envSchema)),
|
|
40
|
+
).toEqual([]);
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## `.env` locale
|
|
46
|
+
|
|
47
|
+
Uno script per ambiente nel `package.json` del progetto, per esempio:
|
|
48
|
+
|
|
49
|
+
```json
|
|
50
|
+
"env:pull": "tc-env pull --secret-id borgarello/dev/core --config config/local.env --out .env"
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
`tc-env pull`:
|
|
54
|
+
|
|
55
|
+
- legge il segreto con le credenziali AWS locali: `--profile`, oppure `AWS_PROFILE`, oppure la catena di default
|
|
56
|
+
dell'SDK. La regione arriva da `--region`, dal profilo o da `AWS_REGION`;
|
|
57
|
+
- si ferma prima di leggere il segreto se il file di output non è ignorato da git (un file versionato non lo è mai) o
|
|
58
|
+
se coincide con la configurazione;
|
|
59
|
+
- scrive la configurazione così com'è e poi i segreti, con permessi 600, e stampa solo dei conteggi;
|
|
60
|
+
- si ferma con un errore che nomina solo le chiavi, mai i valori, se una chiave sta sia nella configurazione sia nel
|
|
61
|
+
segreto, se il segreto non è un oggetto JSON di stringhe o se un valore non si può scrivere in un file dotenv.
|
|
62
|
+
|
|
63
|
+
I valori si scrivono tra apici singoli, oppure tra backtick se contengono un apice: dotenv e `parseEnv` di Node li
|
|
64
|
+
rileggono identici, senza escape. Il segreto contiene quindi il valore come lo restituisce il parser dotenv, prima
|
|
65
|
+
dell'eventuale espansione delle variabili (`expandVariables` di `@nestjs/config`). Non sono supportati i valori su più
|
|
66
|
+
righe e quelli che contengono sia un apice sia un backtick.
|
|
67
|
+
|
|
68
|
+
## Comandi su un altro ambiente
|
|
69
|
+
|
|
70
|
+
Per lanciare dal proprio PC un comando su un altro ambiente, per esempio un seed su prod, si compone un file a parte,
|
|
71
|
+
ignorato da git, e lo si cancella dopo l'uso:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
npx tc-env pull --secret-id borgarello/prod/core --config .env.prod --out .env.prod.local
|
|
75
|
+
npx cross-env DOT_ENV_FILE=.env.prod.local npm run seed:email
|
|
76
|
+
rm .env.prod.local
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
`DOT_ENV_FILE` è il meccanismo di Borgarello per scegliere il file env; ogni progetto usa il suo.
|
|
80
|
+
|
|
81
|
+
## Permessi AWS
|
|
82
|
+
|
|
83
|
+
Chi esegue `pull` ha bisogno di `secretsmanager:GetSecretValue` sul segreto, più `kms:Decrypt` se il segreto è cifrato
|
|
84
|
+
con una chiave KMS del cliente invece di quella di default. Gli ARN dei segreti hanno un suffisso casuale, quindi la
|
|
85
|
+
risorsa si indica con un carattere jolly:
|
|
86
|
+
|
|
87
|
+
```json
|
|
88
|
+
{
|
|
89
|
+
"Effect": "Allow",
|
|
90
|
+
"Action": "secretsmanager:GetSecretValue",
|
|
91
|
+
"Resource": "arn:aws:secretsmanager:<regione>:<account>:secret:borgarello/dev/*"
|
|
92
|
+
}
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
## API
|
|
96
|
+
|
|
97
|
+
| Funzione | Uso |
|
|
98
|
+
| ----------------------------------------------------- | ---------------------------------------------------------- |
|
|
99
|
+
| `secret(schema)` | marca una variabile Joi come segreto |
|
|
100
|
+
| `secretKeys(objectSchema)` | chiavi segrete dello schema, in ordine alfabetico |
|
|
101
|
+
| `secretKeysIn(content, keys)` | chiavi segrete definite nel contenuto di un file env |
|
|
102
|
+
| `composeEnv(configContent, secrets, source)` | testo del `.env`: configurazione e segreti |
|
|
103
|
+
| `readSecret(secretId, region?)` | segreto JSON di Secrets Manager come oggetto di stringhe |
|
|
104
|
+
| `pullEnv({ secretId, configFile, outFile, region? })` | scrive il `.env` e restituisce i conteggi |
|
|
105
|
+
| `EnvError` | errore con messaggi che nominano chiavi e file, mai valori |
|
package/package.json
CHANGED
|
@@ -1,6 +1,23 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tc-libs/env",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
"version": "5.11.1",
|
|
4
|
+
"dependencies": {
|
|
5
|
+
"@aws-sdk/client-secrets-manager": "^3.1160.0",
|
|
6
|
+
"tslib": "^2.8.1"
|
|
7
|
+
},
|
|
8
|
+
"type": "commonjs",
|
|
9
|
+
"main": "./src/index.js",
|
|
10
|
+
"typings": "./src/index.d.ts",
|
|
11
|
+
"bin": {
|
|
12
|
+
"tc-env": "./src/cli.js"
|
|
13
|
+
},
|
|
14
|
+
"peerDependencies": {
|
|
15
|
+
"joi": "^17.0.0 || ^18.0.0"
|
|
16
|
+
},
|
|
17
|
+
"peerDependenciesMeta": {
|
|
18
|
+
"joi": {
|
|
19
|
+
"optional": true
|
|
20
|
+
}
|
|
21
|
+
},
|
|
22
|
+
"types": "./src/index.d.ts"
|
|
23
|
+
}
|
package/src/cli.d.ts
ADDED
package/src/cli.js
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
|
+
exports.main = main;
|
|
5
|
+
const node_util_1 = require("node:util");
|
|
6
|
+
const env_error_1 = require("./env.error");
|
|
7
|
+
const env_pull_1 = require("./env.pull");
|
|
8
|
+
const USAGE = `Uso: tc-env pull --secret-id <id> --config <file> --out <file> [--region <regione>] [--profile <profilo>]
|
|
9
|
+
|
|
10
|
+
Scrive <out> unendo la configurazione versionata <config> e il segreto JSON <id> di AWS Secrets Manager, letto con le
|
|
11
|
+
credenziali AWS locali: il profilo indicato, AWS_PROFILE oppure la catena di default. <out> deve essere ignorato da git.`;
|
|
12
|
+
async function main(argv, io = { out: console.log, err: console.error }) {
|
|
13
|
+
let parsed;
|
|
14
|
+
try {
|
|
15
|
+
parsed = (0, node_util_1.parseArgs)({
|
|
16
|
+
args: argv,
|
|
17
|
+
allowPositionals: true,
|
|
18
|
+
options: {
|
|
19
|
+
'secret-id': { type: 'string' },
|
|
20
|
+
config: { type: 'string' },
|
|
21
|
+
out: { type: 'string' },
|
|
22
|
+
region: { type: 'string' },
|
|
23
|
+
profile: { type: 'string' },
|
|
24
|
+
},
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
io.err(error.message);
|
|
29
|
+
io.err(USAGE);
|
|
30
|
+
return 2;
|
|
31
|
+
}
|
|
32
|
+
const { positionals, values } = parsed;
|
|
33
|
+
const secretId = values['secret-id'];
|
|
34
|
+
const { config, out, region, profile } = values;
|
|
35
|
+
if (positionals.join(' ') !== 'pull' || !secretId || !config || !out) {
|
|
36
|
+
io.err(USAGE);
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
// la catena di credenziali dell'SDK legge il profilo da AWS_PROFILE
|
|
40
|
+
if (profile)
|
|
41
|
+
process.env['AWS_PROFILE'] = profile;
|
|
42
|
+
try {
|
|
43
|
+
const result = await (0, env_pull_1.pullEnv)({
|
|
44
|
+
secretId,
|
|
45
|
+
configFile: config,
|
|
46
|
+
outFile: out,
|
|
47
|
+
region,
|
|
48
|
+
});
|
|
49
|
+
io.out(`${out}: ${result.configKeys} chiavi di configurazione e ${result.secretKeys} segreti da ${secretId}`);
|
|
50
|
+
return 0;
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (!(error instanceof env_error_1.EnvError))
|
|
54
|
+
throw error;
|
|
55
|
+
io.err(`tc-env: ${error.message}`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
if (require.main === module) {
|
|
60
|
+
main(process.argv.slice(2)).then((code) => {
|
|
61
|
+
process.exitCode = code;
|
|
62
|
+
});
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=cli.js.map
|
package/src/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","sourceRoot":"","sources":["../../../../packages/env/src/cli.ts"],"names":[],"mappings":";;;AAYA,oBAiDC;AA5DD,yCAAsC;AACtC,2CAAuC;AACvC,yCAAqC;AAErC,MAAM,KAAK,GAAG;;;yHAG2G,CAAC;AAInH,KAAK,UAAU,IAAI,CACxB,IAAc,EACd,KAAS,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,KAAK,EAAE;IAEjD,IAAI,MAAM,CAAC;IACX,IAAI,CAAC;QACH,MAAM,GAAG,IAAA,qBAAS,EAAC;YACjB,IAAI,EAAE,IAAI;YACV,gBAAgB,EAAE,IAAI;YACtB,OAAO,EAAE;gBACP,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC/B,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBACvB,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;gBAC1B,OAAO,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE;aAC5B;SACF,CAAC,CAAC;IACL,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,EAAE,CAAC,GAAG,CAAE,KAAe,CAAC,OAAO,CAAC,CAAC;QACjC,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACd,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,EAAE,WAAW,EAAE,MAAM,EAAE,GAAG,MAAM,CAAC;IACvC,MAAM,QAAQ,GAAG,MAAM,CAAC,WAAW,CAAC,CAAC;IACrC,MAAM,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,CAAC;IAChD,IAAI,WAAW,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,MAAM,IAAI,CAAC,QAAQ,IAAI,CAAC,MAAM,IAAI,CAAC,GAAG,EAAE,CAAC;QACrE,EAAE,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;QACd,OAAO,CAAC,CAAC;IACX,CAAC;IACD,oEAAoE;IACpE,IAAI,OAAO;QAAE,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,GAAG,OAAO,CAAC;IAElD,IAAI,CAAC;QACH,MAAM,MAAM,GAAG,MAAM,IAAA,kBAAO,EAAC;YAC3B,QAAQ;YACR,UAAU,EAAE,MAAM;YAClB,OAAO,EAAE,GAAG;YACZ,MAAM;SACP,CAAC,CAAC;QACH,EAAE,CAAC,GAAG,CACJ,GAAG,GAAG,KAAK,MAAM,CAAC,UAAU,+BAA+B,MAAM,CAAC,UAAU,eAAe,QAAQ,EAAE,CACtG,CAAC;QACF,OAAO,CAAC,CAAC;IACX,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,CAAC,KAAK,YAAY,oBAAQ,CAAC;YAAE,MAAM,KAAK,CAAC;QAC9C,EAAE,CAAC,GAAG,CAAC,WAAW,KAAK,CAAC,OAAO,EAAE,CAAC,CAAC;QACnC,OAAO,CAAC,CAAC;IACX,CAAC;AACH,CAAC;AAED,IAAI,OAAO,CAAC,IAAI,KAAK,MAAM,EAAE,CAAC;IAC5B,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE;QACxC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC;IAC1B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/src/env.error.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.EnvError = void 0;
|
|
4
|
+
// Errore della configurazione d'ambiente: il messaggio nomina chiavi, file e segreti, mai i valori.
|
|
5
|
+
class EnvError extends Error {
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.name = 'EnvError';
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
exports.EnvError = EnvError;
|
|
12
|
+
//# sourceMappingURL=env.error.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.error.js","sourceRoot":"","sources":["../../../../packages/env/src/env.error.ts"],"names":[],"mappings":";;;AAAA,oGAAoG;AACpG,MAAa,QAAS,SAAQ,KAAK;IACjC,YAAY,OAAe;QACzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,UAAU,CAAC;IACzB,CAAC;CACF;AALD,4BAKC"}
|
package/src/env.file.js
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.composeEnv = composeEnv;
|
|
4
|
+
exports.secretKeysIn = secretKeysIn;
|
|
5
|
+
const node_util_1 = require("node:util");
|
|
6
|
+
const env_error_1 = require("./env.error");
|
|
7
|
+
const KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8
|
+
function parse(content) {
|
|
9
|
+
return (0, node_util_1.parseEnv)(content);
|
|
10
|
+
}
|
|
11
|
+
// Il valore si scrive in modo che dotenv e parseEnv di Node lo rileggano identico: tra apici singoli, oppure tra
|
|
12
|
+
// backtick se contiene un apice. Per entrambi i parser le due forme sono letterali, senza escape: un $ resta un $ e
|
|
13
|
+
// l'eventuale espansione delle variabili (expandVariables di @nestjs/config) si comporta come con il file originale.
|
|
14
|
+
function quote(key, value) {
|
|
15
|
+
if (/[\r\n]/.test(value)) {
|
|
16
|
+
throw new env_error_1.EnvError(`${key}: i valori su più righe non sono supportati`);
|
|
17
|
+
}
|
|
18
|
+
if (!value.includes("'"))
|
|
19
|
+
return `'${value}'`;
|
|
20
|
+
if (!value.includes('`'))
|
|
21
|
+
return `\`${value}\``;
|
|
22
|
+
throw new env_error_1.EnvError(`${key}: il valore contiene sia ' sia \`, che un file dotenv non sa racchiudere`);
|
|
23
|
+
}
|
|
24
|
+
// Il .env completo: la configurazione versionata così com'è, poi i segreti. Una chiave non può stare in entrambi.
|
|
25
|
+
function composeEnv(configContent, secrets, source) {
|
|
26
|
+
const config = parse(configContent);
|
|
27
|
+
const invalid = Object.keys(secrets)
|
|
28
|
+
.filter((key) => !KEY.test(key))
|
|
29
|
+
.sort();
|
|
30
|
+
if (invalid.length) {
|
|
31
|
+
throw new env_error_1.EnvError(`nomi di variabile non validi nel segreto: ${invalid.join(', ')}`);
|
|
32
|
+
}
|
|
33
|
+
const both = Object.keys(secrets)
|
|
34
|
+
.filter((key) => key in config)
|
|
35
|
+
.sort();
|
|
36
|
+
if (both.length) {
|
|
37
|
+
throw new env_error_1.EnvError(`chiavi presenti sia nella configurazione sia nel segreto: ${both.join(', ')}`);
|
|
38
|
+
}
|
|
39
|
+
const configText = configContent.replace(/\r\n/g, '\n');
|
|
40
|
+
const secretLines = Object.keys(secrets)
|
|
41
|
+
.sort()
|
|
42
|
+
.map((key) => `${key}=${quote(key, secrets[key])}`);
|
|
43
|
+
return [
|
|
44
|
+
`# Generato da tc-env con ${source}: non modificarlo a mano e non versionarlo.`,
|
|
45
|
+
configText.endsWith('\n') || !configText ? configText : `${configText}\n`,
|
|
46
|
+
`# Segreti da ${source}`,
|
|
47
|
+
...secretLines,
|
|
48
|
+
'',
|
|
49
|
+
].join('\n');
|
|
50
|
+
}
|
|
51
|
+
// Chiavi segrete definite in un file env: in un file versionato devono essere zero. Le righe commentate non contano.
|
|
52
|
+
function secretKeysIn(content, secretKeys) {
|
|
53
|
+
const env = parse(content);
|
|
54
|
+
return secretKeys.filter((key) => key in env).sort();
|
|
55
|
+
}
|
|
56
|
+
//# sourceMappingURL=env.file.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.file.js","sourceRoot":"","sources":["../../../../packages/env/src/env.file.ts"],"names":[],"mappings":";;AAwBA,gCAkCC;AAGD,oCAMC;AAnED,yCAAqC;AACrC,2CAAuC;AAEvC,MAAM,GAAG,GAAG,0BAA0B,CAAC;AAEvC,SAAS,KAAK,CAAC,OAAe;IAC5B,OAAO,IAAA,oBAAQ,EAAC,OAAO,CAA2B,CAAC;AACrD,CAAC;AAED,iHAAiH;AACjH,oHAAoH;AACpH,qHAAqH;AACrH,SAAS,KAAK,CAAC,GAAW,EAAE,KAAa;IACvC,IAAI,QAAQ,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;QACzB,MAAM,IAAI,oBAAQ,CAAC,GAAG,GAAG,6CAA6C,CAAC,CAAC;IAC1E,CAAC;IACD,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,IAAI,KAAK,GAAG,CAAC;IAC9C,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,GAAG,CAAC;QAAE,OAAO,KAAK,KAAK,IAAI,CAAC;IAChD,MAAM,IAAI,oBAAQ,CAChB,GAAG,GAAG,0EAA0E,CACjF,CAAC;AACJ,CAAC;AAED,kHAAkH;AAClH,SAAgB,UAAU,CACxB,aAAqB,EACrB,OAA+B,EAC/B,MAAc;IAEd,MAAM,MAAM,GAAG,KAAK,CAAC,aAAa,CAAC,CAAC;IACpC,MAAM,OAAO,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;SACjC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;SAC/B,IAAI,EAAE,CAAC;IACV,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;QACnB,MAAM,IAAI,oBAAQ,CAChB,6CAA6C,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAClE,CAAC;IACJ,CAAC;IACD,MAAM,IAAI,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;SAC9B,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,MAAM,CAAC;SAC9B,IAAI,EAAE,CAAC;IACV,IAAI,IAAI,CAAC,MAAM,EAAE,CAAC;QAChB,MAAM,IAAI,oBAAQ,CAChB,6DAA6D,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAC/E,CAAC;IACJ,CAAC;IAED,MAAM,UAAU,GAAG,aAAa,CAAC,OAAO,CAAC,OAAO,EAAE,IAAI,CAAC,CAAC;IACxD,MAAM,WAAW,GAAG,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC;SACrC,IAAI,EAAE;SACN,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC;IACtD,OAAO;QACL,4BAA4B,MAAM,6CAA6C;QAC/E,UAAU,CAAC,QAAQ,CAAC,IAAI,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,CAAC,UAAU,CAAC,CAAC,CAAC,GAAG,UAAU,IAAI;QACzE,gBAAgB,MAAM,EAAE;QACxB,GAAG,WAAW;QACd,EAAE;KACH,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AACf,CAAC;AAED,qHAAqH;AACrH,SAAgB,YAAY,CAC1B,OAAe,EACf,UAA6B;IAE7B,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC,CAAC;IAC3B,OAAO,UAAU,CAAC,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,IAAI,GAAG,CAAC,CAAC,IAAI,EAAE,CAAC;AACvD,CAAC"}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export type PullEnvOptions = {
|
|
2
|
+
secretId: string;
|
|
3
|
+
configFile: string;
|
|
4
|
+
outFile: string;
|
|
5
|
+
region?: string;
|
|
6
|
+
};
|
|
7
|
+
export declare function readSecret(secretId: string, region?: string): Promise<Record<string, string>>;
|
|
8
|
+
export declare function pullEnv(options: PullEnvOptions): Promise<{
|
|
9
|
+
configKeys: number;
|
|
10
|
+
secretKeys: number;
|
|
11
|
+
}>;
|
package/src/env.pull.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.readSecret = readSecret;
|
|
4
|
+
exports.pullEnv = pullEnv;
|
|
5
|
+
const node_child_process_1 = require("node:child_process");
|
|
6
|
+
const node_fs_1 = require("node:fs");
|
|
7
|
+
const node_path_1 = require("node:path");
|
|
8
|
+
const node_util_1 = require("node:util");
|
|
9
|
+
const client_secrets_manager_1 = require("@aws-sdk/client-secrets-manager");
|
|
10
|
+
const env_error_1 = require("./env.error");
|
|
11
|
+
const env_file_1 = require("./env.file");
|
|
12
|
+
// Il segreto è un oggetto JSON piatto { "CHIAVE": "valore" }, come lo salva l'editor chiave/valore della console. Le
|
|
13
|
+
// credenziali sono quelle della catena di default dell'SDK: il profilo AWS dello sviluppatore, il ruolo su EC2.
|
|
14
|
+
async function readSecret(secretId, region) {
|
|
15
|
+
const client = new client_secrets_manager_1.SecretsManagerClient(region ? { region } : {});
|
|
16
|
+
let raw;
|
|
17
|
+
try {
|
|
18
|
+
const result = await client.send(new client_secrets_manager_1.GetSecretValueCommand({ SecretId: secretId }));
|
|
19
|
+
raw = result.SecretString;
|
|
20
|
+
}
|
|
21
|
+
catch (error) {
|
|
22
|
+
// nome e messaggio degli errori dell'SDK non contengono valori del segreto
|
|
23
|
+
const { name, message } = error;
|
|
24
|
+
throw new env_error_1.EnvError(`lettura di ${secretId} non riuscita: ${name}: ${message}`);
|
|
25
|
+
}
|
|
26
|
+
finally {
|
|
27
|
+
client.destroy();
|
|
28
|
+
}
|
|
29
|
+
let parsed;
|
|
30
|
+
try {
|
|
31
|
+
parsed = JSON.parse(raw ?? '');
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
throw new env_error_1.EnvError(`${secretId}: il segreto non è un JSON valido`);
|
|
35
|
+
}
|
|
36
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
37
|
+
throw new env_error_1.EnvError(`${secretId}: il segreto deve essere un oggetto JSON { "CHIAVE": "valore" }`);
|
|
38
|
+
}
|
|
39
|
+
const notStrings = Object.entries(parsed)
|
|
40
|
+
.filter(([, value]) => typeof value !== 'string')
|
|
41
|
+
.map(([key]) => key)
|
|
42
|
+
.sort();
|
|
43
|
+
if (notStrings.length) {
|
|
44
|
+
throw new env_error_1.EnvError(`${secretId}: valori non di tipo stringa per ${notStrings.join(', ')}`);
|
|
45
|
+
}
|
|
46
|
+
return parsed;
|
|
47
|
+
}
|
|
48
|
+
// Un file con segreti non deve poter finire in un commit: deve essere ignorato da git, e un file versionato non lo è
|
|
49
|
+
// mai. Fuori da un repository git il controllo non si applica.
|
|
50
|
+
function assertGitIgnored(file) {
|
|
51
|
+
const path = (0, node_path_1.resolve)(file);
|
|
52
|
+
const dir = (0, node_path_1.dirname)(path);
|
|
53
|
+
if (!(0, node_fs_1.existsSync)(dir)) {
|
|
54
|
+
throw new env_error_1.EnvError(`la cartella di ${file} non esiste`);
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
(0, node_child_process_1.execFileSync)('git', ['check-ignore', '-q', '--', path], {
|
|
58
|
+
cwd: dir,
|
|
59
|
+
stdio: 'ignore',
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
const { status, code } = error;
|
|
64
|
+
if (status === 128)
|
|
65
|
+
return;
|
|
66
|
+
if (status === 1) {
|
|
67
|
+
throw new env_error_1.EnvError(`${file} non è ignorato da git: aggiungilo al .gitignore prima di scriverci dei segreti`);
|
|
68
|
+
}
|
|
69
|
+
if (code === 'ENOENT') {
|
|
70
|
+
throw new env_error_1.EnvError('git non è disponibile: serve a verificare che il file di output sia ignorato');
|
|
71
|
+
}
|
|
72
|
+
throw error;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
// Compone outFile dalla configurazione versionata e dal segreto, con permessi ristretti. Restituisce solo conteggi.
|
|
76
|
+
async function pullEnv(options) {
|
|
77
|
+
const { secretId, configFile, outFile, region } = options;
|
|
78
|
+
if ((0, node_path_1.resolve)(configFile) === (0, node_path_1.resolve)(outFile)) {
|
|
79
|
+
throw new env_error_1.EnvError('il file di configurazione e quello di output coincidono');
|
|
80
|
+
}
|
|
81
|
+
if (!(0, node_fs_1.existsSync)(configFile)) {
|
|
82
|
+
throw new env_error_1.EnvError(`file di configurazione ${configFile} non trovato`);
|
|
83
|
+
}
|
|
84
|
+
assertGitIgnored(outFile);
|
|
85
|
+
const configContent = (0, node_fs_1.readFileSync)(configFile, 'utf8');
|
|
86
|
+
const secrets = await readSecret(secretId, region);
|
|
87
|
+
(0, node_fs_1.writeFileSync)(outFile, (0, env_file_1.composeEnv)(configContent, secrets, secretId), {
|
|
88
|
+
mode: 0o600,
|
|
89
|
+
});
|
|
90
|
+
// writeFileSync applica mode solo ai file nuovi
|
|
91
|
+
(0, node_fs_1.chmodSync)(outFile, 0o600);
|
|
92
|
+
return {
|
|
93
|
+
configKeys: Object.keys((0, node_util_1.parseEnv)(configContent)).length,
|
|
94
|
+
secretKeys: Object.keys(secrets).length,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=env.pull.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.pull.js","sourceRoot":"","sources":["../../../../packages/env/src/env.pull.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.secret = secret;
|
|
4
|
+
exports.secretKeys = secretKeys;
|
|
5
|
+
// Marca una variabile dello schema Joi come segreto: il suo valore arriva dal secret store, mai da un file env
|
|
6
|
+
// versionato. La validazione non cambia.
|
|
7
|
+
function secret(schema) {
|
|
8
|
+
// meta restituisce lo stesso schema; sull'unione Schema i tipi di Joi lo allargano ad AnySchema
|
|
9
|
+
return schema.meta({ secret: true });
|
|
10
|
+
}
|
|
11
|
+
// Chiavi marcate con secret nello schema delle variabili d'ambiente, in ordine alfabetico. Lo schema resta l'unico
|
|
12
|
+
// elenco delle variabili: niente liste parallele da tenere allineate.
|
|
13
|
+
function secretKeys(schema) {
|
|
14
|
+
const keys = (schema.describe()['keys'] ?? {});
|
|
15
|
+
return Object.keys(keys)
|
|
16
|
+
.filter((key) => keys[key]?.metas?.some((meta) => meta['secret'] === true))
|
|
17
|
+
.sort();
|
|
18
|
+
}
|
|
19
|
+
//# sourceMappingURL=env.schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"env.schema.js","sourceRoot":"","sources":["../../../../packages/env/src/env.schema.ts"],"names":[],"mappings":";;AAMA,wBAGC;AAID,gCAQC;AAjBD,+GAA+G;AAC/G,yCAAyC;AACzC,SAAgB,MAAM,CAAmB,MAAS;IAChD,gGAAgG;IAChG,OAAO,MAAM,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,EAAE,CAAM,CAAC;AAC5C,CAAC;AAED,mHAAmH;AACnH,sEAAsE;AACtE,SAAgB,UAAU,CAAC,MAAoB;IAC7C,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,QAAQ,EAAE,CAAC,MAAM,CAAC,IAAI,EAAE,CAG5C,CAAC;IACF,OAAO,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC;SACrB,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,CAAC,IAAI,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,CAAC,KAAK,IAAI,CAAC,CAAC;SAC1E,IAAI,EAAE,CAAC;AACZ,CAAC"}
|
package/src/index.d.ts
ADDED
package/src/index.js
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
const tslib_1 = require("tslib");
|
|
4
|
+
tslib_1.__exportStar(require("./env.error"), exports);
|
|
5
|
+
tslib_1.__exportStar(require("./env.schema"), exports);
|
|
6
|
+
tslib_1.__exportStar(require("./env.file"), exports);
|
|
7
|
+
tslib_1.__exportStar(require("./env.pull"), exports);
|
|
8
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../packages/env/src/index.ts"],"names":[],"mappings":";;;AAAA,sDAA4B;AAC5B,uDAA6B;AAC7B,qDAA2B;AAC3B,qDAA2B"}
|