@tmlmobilidade/export-data 20251229.1536.41 → 20251229.1636.59
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/dist/index.js +4 -0
- package/dist/prompts/access-key.d.ts +1 -0
- package/dist/prompts/access-key.js +68 -0
- package/dist/prompts/export-types.d.ts +1 -1
- package/dist/prompts/filter-agency-ids.js +4 -4
- package/dist/prompts/filter-dates.js +5 -5
- package/dist/prompts/filter-line-ids.js +4 -4
- package/dist/prompts/filter-pattern-ids.js +4 -4
- package/dist/prompts/filter-stop-ids.js +5 -5
- package/dist/prompts/filter-vehicle-ids.js +4 -4
- package/dist/utils/credential-storage.d.ts +18 -0
- package/dist/utils/credential-storage.js +293 -0
- package/package.json +2 -2
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
+
import { promptAccessKey } from './prompts/access-key.js';
|
|
2
3
|
import { promptExportTypes } from './prompts/export-types.js';
|
|
3
4
|
import { promptFilterByAgencyIds } from './prompts/filter-agency-ids.js';
|
|
4
5
|
import { promptFilterByDates } from './prompts/filter-dates.js';
|
|
@@ -32,6 +33,9 @@ import { ASCII_CM_SHORT } from '@tmlmobilidade/consts';
|
|
|
32
33
|
log.info(`O ID desta exportação é: ${context._id}`);
|
|
33
34
|
log.info(`Todos os resultados serão guardados aqui: ${context.output}`);
|
|
34
35
|
//
|
|
36
|
+
// Prompt for the access key
|
|
37
|
+
await promptAccessKey();
|
|
38
|
+
//
|
|
35
39
|
// Request the export types and which filters to apply
|
|
36
40
|
const exportTypes = await promptExportTypes();
|
|
37
41
|
const filterTypes = await promptFilterTypes();
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function promptAccessKey(): Promise<void>;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { deleteCredential, getCredential, setCredential } from '../utils/credential-storage.js';
|
|
3
|
+
import { cancel, isCancel, note, spinner, text } from '@clack/prompts';
|
|
4
|
+
/* * */
|
|
5
|
+
const CREDENTIAL_KEY = 'access-key';
|
|
6
|
+
/* * */
|
|
7
|
+
export async function promptAccessKey() {
|
|
8
|
+
//
|
|
9
|
+
//
|
|
10
|
+
// Check if there is an access key already saved in the computer
|
|
11
|
+
const s = spinner();
|
|
12
|
+
s.start('A verificar credenciais guardadas...');
|
|
13
|
+
const existingKey = await getCredential(CREDENTIAL_KEY);
|
|
14
|
+
//
|
|
15
|
+
// If --delete-credentials flag was passed, delete existing credentials
|
|
16
|
+
if (process.argv.includes('--delete-credentials') && existingKey) {
|
|
17
|
+
s.stop('Flag --delete-credentials detectada.');
|
|
18
|
+
s.start('A remover credenciais guardadas...');
|
|
19
|
+
await deleteCredential(CREDENTIAL_KEY);
|
|
20
|
+
s.stop('Credenciais removidas. A começar de novo...');
|
|
21
|
+
}
|
|
22
|
+
else if (existingKey) {
|
|
23
|
+
s.stop('Chave de acesso encontrada no armazenamento seguro do sistema.');
|
|
24
|
+
process.env.DATABASE_URI = existingKey;
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
s.stop('Nenhuma chave de acesso encontrada.');
|
|
29
|
+
}
|
|
30
|
+
//
|
|
31
|
+
// If no key exists, ask the user to enter the access key
|
|
32
|
+
note('A chave de acesso será guardada de forma segura:\n'
|
|
33
|
+
+ ' • macOS: Keychain\n'
|
|
34
|
+
+ ' • Windows: Credential Manager\n'
|
|
35
|
+
+ ' • Linux: Secret Service (ou ficheiro encriptado)');
|
|
36
|
+
const accessKey = await text({
|
|
37
|
+
message: 'Por favor introduz a chave de acesso:',
|
|
38
|
+
placeholder: 'Chave de acesso...',
|
|
39
|
+
validate: (value) => {
|
|
40
|
+
if (!value || value.trim().length === 0) {
|
|
41
|
+
return 'A chave de acesso não pode estar vazia.';
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
//
|
|
46
|
+
// Verify if the user cancelled the operation
|
|
47
|
+
if (isCancel(accessKey)) {
|
|
48
|
+
cancel('Operação cancelada pelo utilizador.');
|
|
49
|
+
process.exit(0);
|
|
50
|
+
}
|
|
51
|
+
if (!accessKey || typeof accessKey !== 'string') {
|
|
52
|
+
cancel('Chave de acesso inválida.');
|
|
53
|
+
process.exit(1);
|
|
54
|
+
}
|
|
55
|
+
//
|
|
56
|
+
// Save the access key to the computer's secure storage
|
|
57
|
+
s.start('A guardar chave de acesso...');
|
|
58
|
+
try {
|
|
59
|
+
await setCredential(CREDENTIAL_KEY, accessKey);
|
|
60
|
+
s.stop('Chave de acesso guardada com sucesso no armazenamento seguro do sistema.');
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
s.stop('Erro ao guardar a chave de acesso.');
|
|
64
|
+
cancel(`Erro: ${error instanceof Error ? error.message : 'Erro desconhecido'}`);
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
process.env.DATABASE_URI = accessKey;
|
|
68
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import { type ExportType } from '../types.js';
|
|
2
|
-
export declare function promptExportTypes(): Promise<
|
|
2
|
+
export declare function promptExportTypes(): Promise<ExportType[]>;
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* * */
|
|
2
|
-
import {
|
|
2
|
+
import { note, text } from '@clack/prompts';
|
|
3
3
|
/* * */
|
|
4
4
|
export async function promptFilterByAgencyIds() {
|
|
5
5
|
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
note('FILTRAR POR AGENCY ID:\n'
|
|
7
|
+
+ ' • Introduz os Agency IDs separados por vírgulas. Exemplo: 41,42,etc...\n'
|
|
8
|
+
+ ' • Se não introduzires nenhum Agency ID, este filtro não será aplicado.');
|
|
9
9
|
const value = await text({
|
|
10
10
|
message: 'Agency IDs:',
|
|
11
11
|
placeholder: '41,42,etc...',
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/* * */
|
|
2
|
-
import { cancel, isCancel,
|
|
2
|
+
import { cancel, isCancel, note, text } from '@clack/prompts';
|
|
3
3
|
import { validateOperationalDate } from '@tmlmobilidade/types';
|
|
4
4
|
/* * */
|
|
5
5
|
export async function promptFilterByDates() {
|
|
6
6
|
//
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
note('FILTRAR POR DATAS:\n'
|
|
8
|
+
+ ' • Introduz as datas operacionais no formato ano-mês-dia. Exemplo: 20250101 ou 2025-01-01\n'
|
|
9
|
+
+ ' • Devido ao enorme volume de dados, filtrar por datas é obrigatório.\n'
|
|
10
|
+
+ ' • A data de início não pode ser anterior a 1 Jan. 2024 (20240101).');
|
|
11
11
|
const startDate = await text({
|
|
12
12
|
initialValue: '20250101',
|
|
13
13
|
message: 'Data de Início:',
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* * */
|
|
2
|
-
import {
|
|
2
|
+
import { note, text } from '@clack/prompts';
|
|
3
3
|
/* * */
|
|
4
4
|
export async function promptFilterByLineIds() {
|
|
5
5
|
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
note('FILTRAR POR LINE ID:\n'
|
|
7
|
+
+ ' • Introduz os Line IDs separados por vírgulas. Exemplo: 1001,1002,etc...\n'
|
|
8
|
+
+ ' • Se não introduzires nenhum Line ID, este filtro não será aplicado.');
|
|
9
9
|
const value = await text({
|
|
10
10
|
message: 'Line IDs:',
|
|
11
11
|
placeholder: '1001,1002,etc...',
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* * */
|
|
2
|
-
import {
|
|
2
|
+
import { note, text } from '@clack/prompts';
|
|
3
3
|
/* * */
|
|
4
4
|
export async function promptFilterByPatternIds() {
|
|
5
5
|
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
note('FILTRAR POR PATTERN ID:\n'
|
|
7
|
+
+ ' • Introduz os Pattern IDs separados por vírgulas. Exemplo: 1001_0_1,1001_0_2,etc...\n'
|
|
8
|
+
+ ' • Se não introduzires nenhum Pattern ID, este filtro não será aplicado.');
|
|
9
9
|
const value = await text({
|
|
10
10
|
message: 'Pattern IDs:',
|
|
11
11
|
placeholder: '1001_0_1,1001_0_2,etc...',
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
/* * */
|
|
2
|
-
import {
|
|
2
|
+
import { note, text } from '@clack/prompts';
|
|
3
3
|
/* * */
|
|
4
4
|
export async function promptFilterByStopIds() {
|
|
5
5
|
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
6
|
+
note('FILTRAR POR STOP ID:\n'
|
|
7
|
+
+ ' • Introduz os Stop IDs separados por vírgulas. Exemplo: 010101,020202,etc...\n'
|
|
8
|
+
+ ' • Não te esqueças do zero à esquerda.\n'
|
|
9
|
+
+ ' • Se não introduzires nenhum Stop ID, este filtro não será aplicado.');
|
|
10
10
|
const value = await text({
|
|
11
11
|
message: 'Stop IDs:',
|
|
12
12
|
placeholder: '010101,020202,etc...',
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
/* * */
|
|
2
|
-
import {
|
|
2
|
+
import { note, text } from '@clack/prompts';
|
|
3
3
|
/* * */
|
|
4
4
|
export async function promptFilterByVehicleIds() {
|
|
5
5
|
//
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
note('FILTRAR POR VEHICLE ID:\n'
|
|
7
|
+
+ ' • Introduz os Vehicle IDs separados por vírgulas. Exemplo: 1234,9876,etc...\n'
|
|
8
|
+
+ ' • Se não introduzires nenhum Vehicle ID, este filtro não será aplicado.');
|
|
9
9
|
const value = await text({
|
|
10
10
|
message: 'Vehicle IDs:',
|
|
11
11
|
placeholder: '1234,9876,etc...',
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get a credential from the OS-native secure storage
|
|
3
|
+
* - macOS: Uses Keychain via `security` command
|
|
4
|
+
* - Windows: Uses Credential Manager via PowerShell
|
|
5
|
+
* - Linux: Uses secret-tool or fallback to encrypted file
|
|
6
|
+
*/
|
|
7
|
+
export declare function getCredential(key: string): Promise<null | string>;
|
|
8
|
+
/**
|
|
9
|
+
* Store a credential in the OS-native secure storage
|
|
10
|
+
* - macOS: Uses Keychain via `security` command
|
|
11
|
+
* - Windows: Uses Credential Manager via PowerShell
|
|
12
|
+
* - Linux: Uses secret-tool or fallback to encrypted file
|
|
13
|
+
*/
|
|
14
|
+
export declare function setCredential(key: string, value: string): Promise<void>;
|
|
15
|
+
/**
|
|
16
|
+
* Delete a credential from the OS-native secure storage
|
|
17
|
+
*/
|
|
18
|
+
export declare function deleteCredential(key: string): Promise<void>;
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
/* * */
|
|
2
|
+
import { exec } from 'node:child_process';
|
|
3
|
+
import crypto from 'node:crypto';
|
|
4
|
+
import fs from 'node:fs/promises';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { promisify } from 'node:util';
|
|
8
|
+
/* * */
|
|
9
|
+
const execAsync = promisify(exec);
|
|
10
|
+
/* * */
|
|
11
|
+
const SERVICE_NAME = 'TMLMobilidadeGO';
|
|
12
|
+
const ACCOUNT_NAME = 'export-data-access-key';
|
|
13
|
+
/**
|
|
14
|
+
* Get a credential from the OS-native secure storage
|
|
15
|
+
* - macOS: Uses Keychain via `security` command
|
|
16
|
+
* - Windows: Uses Credential Manager via PowerShell
|
|
17
|
+
* - Linux: Uses secret-tool or fallback to encrypted file
|
|
18
|
+
*/
|
|
19
|
+
export async function getCredential(key) {
|
|
20
|
+
const platform = os.platform();
|
|
21
|
+
try {
|
|
22
|
+
if (platform === 'darwin') {
|
|
23
|
+
// macOS Keychain
|
|
24
|
+
return await getMacOSCredential(key);
|
|
25
|
+
}
|
|
26
|
+
else if (platform === 'win32') {
|
|
27
|
+
// Windows Credential Manager
|
|
28
|
+
return await getWindowsCredential(key);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
// Linux (and other Unix-like systems)
|
|
32
|
+
return await getLinuxCredential(key);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// If credential doesn't exist or there's an error, return null
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Store a credential in the OS-native secure storage
|
|
42
|
+
* - macOS: Uses Keychain via `security` command
|
|
43
|
+
* - Windows: Uses Credential Manager via PowerShell
|
|
44
|
+
* - Linux: Uses secret-tool or fallback to encrypted file
|
|
45
|
+
*/
|
|
46
|
+
export async function setCredential(key, value) {
|
|
47
|
+
const platform = os.platform();
|
|
48
|
+
if (platform === 'darwin') {
|
|
49
|
+
// macOS Keychain
|
|
50
|
+
await setMacOSCredential(key, value);
|
|
51
|
+
}
|
|
52
|
+
else if (platform === 'win32') {
|
|
53
|
+
// Windows Credential Manager
|
|
54
|
+
await setWindowsCredential(key, value);
|
|
55
|
+
}
|
|
56
|
+
else {
|
|
57
|
+
// Linux (and other Unix-like systems)
|
|
58
|
+
await setLinuxCredential(key, value);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Delete a credential from the OS-native secure storage
|
|
63
|
+
*/
|
|
64
|
+
export async function deleteCredential(key) {
|
|
65
|
+
try {
|
|
66
|
+
const platform = os.platform();
|
|
67
|
+
if (platform === 'darwin') {
|
|
68
|
+
await execAsync(`security delete-generic-password -s "${SERVICE_NAME}" -a "${key}"`);
|
|
69
|
+
}
|
|
70
|
+
else if (platform === 'win32') {
|
|
71
|
+
const targetName = `${SERVICE_NAME}:${key}`;
|
|
72
|
+
await execAsync(`powershell -Command "cmdkey /delete:'${targetName}'"`);
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
await execAsync(`secret-tool clear service "${SERVICE_NAME}" account "${key}"`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
// Credential might not exist, which is fine
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Get credential from macOS Keychain.
|
|
84
|
+
* @param key The credential key.
|
|
85
|
+
* @returns The credential value or null if not found.
|
|
86
|
+
*/
|
|
87
|
+
async function getMacOSCredential(key) {
|
|
88
|
+
try {
|
|
89
|
+
const { stdout } = await execAsync(`security find-generic-password -s "${SERVICE_NAME}" -a "${key}" -w`);
|
|
90
|
+
return stdout.trim();
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Store credential in macOS Keychain.
|
|
98
|
+
* @param key The credential key.
|
|
99
|
+
* @param value The credential value.
|
|
100
|
+
*/
|
|
101
|
+
async function setMacOSCredential(key, value) {
|
|
102
|
+
// First, try to delete existing credential to avoid duplicates
|
|
103
|
+
await deleteCredential(key);
|
|
104
|
+
// Add new credential
|
|
105
|
+
await execAsync(`security add-generic-password -s "${SERVICE_NAME}" -a "${key}" -w "${value}" -U`);
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Get credential from Windows Credential Manager.
|
|
109
|
+
* @param key The credential key.
|
|
110
|
+
* @returns The credential value or null if not found.
|
|
111
|
+
*/
|
|
112
|
+
async function getWindowsCredential(key) {
|
|
113
|
+
try {
|
|
114
|
+
const targetName = `${SERVICE_NAME}:${key}`;
|
|
115
|
+
const { stdout } = await execAsync(`powershell -Command "$cred = cmdkey /list:'${targetName}' 2>$null; if($cred) { [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String((Get-StoredCredential -Target '${targetName}').Password)) }"`);
|
|
116
|
+
// If the above doesn't work, use a simpler approach with a custom PowerShell script
|
|
117
|
+
if (!stdout || stdout.trim() === '') {
|
|
118
|
+
// Fallback: Use Windows Data Protection API (DPAPI) with a file
|
|
119
|
+
return await getWindowsCredentialFallback(key);
|
|
120
|
+
}
|
|
121
|
+
return stdout.trim();
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Try fallback method
|
|
125
|
+
return await getWindowsCredentialFallback(key);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
/**
|
|
129
|
+
* Store credential in Windows Credential Manager.
|
|
130
|
+
* @param key The credential key.
|
|
131
|
+
* @param value The credential value.
|
|
132
|
+
*/
|
|
133
|
+
async function setWindowsCredential(key, value) {
|
|
134
|
+
try {
|
|
135
|
+
// Use PowerShell to store the credential
|
|
136
|
+
const targetName = `${SERVICE_NAME}:${key}`;
|
|
137
|
+
// Create a PowerShell script to store the credential
|
|
138
|
+
const script = `
|
|
139
|
+
$password = ConvertTo-SecureString "${value}" -AsPlainText -Force
|
|
140
|
+
$credential = New-Object System.Management.Automation.PSCredential("${ACCOUNT_NAME}", $password)
|
|
141
|
+
cmdkey /generic:"${targetName}" /user:"${ACCOUNT_NAME}" /pass:"${value}"
|
|
142
|
+
`;
|
|
143
|
+
// Execute the PowerShell script
|
|
144
|
+
await execAsync(`powershell -Command "${script.replace(/\n/g, ' ')}"`);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// Fallback to file-based storage with DPAPI
|
|
148
|
+
await setWindowsCredentialFallback(key, value);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Fallback method to get credential from encrypted file on Windows.
|
|
153
|
+
* @param key The credential key.
|
|
154
|
+
* @returns The credential value or null if not found.
|
|
155
|
+
*/
|
|
156
|
+
async function getWindowsCredentialFallback(key) {
|
|
157
|
+
try {
|
|
158
|
+
const credPath = getCredentialFilePath(key);
|
|
159
|
+
const encrypted = await fs.readFile(credPath, 'utf8');
|
|
160
|
+
return decrypt(encrypted);
|
|
161
|
+
}
|
|
162
|
+
catch {
|
|
163
|
+
return null;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Fallback method to set credential in encrypted file on Windows.
|
|
168
|
+
* @param key The credential key.
|
|
169
|
+
* @param value The credential value.
|
|
170
|
+
*/
|
|
171
|
+
async function setWindowsCredentialFallback(key, value) {
|
|
172
|
+
const credPath = getCredentialFilePath(key);
|
|
173
|
+
const encrypted = encrypt(value);
|
|
174
|
+
// Ensure directory exists
|
|
175
|
+
await fs.mkdir(path.dirname(credPath), { recursive: true });
|
|
176
|
+
// Write with restricted permissions (owner only)
|
|
177
|
+
await fs.writeFile(credPath, encrypted, { mode: 0o600 });
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Get credential from Linux secure storage (secret-tool) or fallback to encrypted file.
|
|
181
|
+
* @param key The credential key.
|
|
182
|
+
* @returns The credential value or null if not found.
|
|
183
|
+
*/
|
|
184
|
+
async function getLinuxCredential(key) {
|
|
185
|
+
try {
|
|
186
|
+
const { stdout } = await execAsync(`secret-tool lookup service "${SERVICE_NAME}" account "${key}"`);
|
|
187
|
+
return stdout.trim();
|
|
188
|
+
}
|
|
189
|
+
catch {
|
|
190
|
+
// Fallback to encrypted file if secret-tool is not available
|
|
191
|
+
return await getLinuxCredentialFallback(key);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Store credential in Linux secure storage (secret-tool) or fallback to encrypted file.
|
|
196
|
+
* @param key The credential key.
|
|
197
|
+
* @param value The credential value.
|
|
198
|
+
*/
|
|
199
|
+
async function setLinuxCredential(key, value) {
|
|
200
|
+
try {
|
|
201
|
+
// Try using secret-tool first
|
|
202
|
+
// Note: secret-tool reads password from stdin, so we use echo with pipe
|
|
203
|
+
await execAsync(`echo "${value}" | secret-tool store --label="${SERVICE_NAME} - ${key}" service "${SERVICE_NAME}" account "${key}"`);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
// Fallback to encrypted file
|
|
207
|
+
await setLinuxCredentialFallback(key, value);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
/**
|
|
211
|
+
* Fallback method to get credential from encrypted file on Linux.
|
|
212
|
+
* @param key The credential key.
|
|
213
|
+
* @returns The credential value or null if not found.
|
|
214
|
+
*/
|
|
215
|
+
async function getLinuxCredentialFallback(key) {
|
|
216
|
+
try {
|
|
217
|
+
const credPath = getCredentialFilePath(key);
|
|
218
|
+
const encrypted = await fs.readFile(credPath, 'utf8');
|
|
219
|
+
return decrypt(encrypted);
|
|
220
|
+
}
|
|
221
|
+
catch {
|
|
222
|
+
return null;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
/**
|
|
226
|
+
* Fallback method to set credential in encrypted file on Linux.
|
|
227
|
+
* @param key The credential key.
|
|
228
|
+
* @param value The credential value.
|
|
229
|
+
*/
|
|
230
|
+
async function setLinuxCredentialFallback(key, value) {
|
|
231
|
+
const credPath = getCredentialFilePath(key);
|
|
232
|
+
const encrypted = encrypt(value);
|
|
233
|
+
// Ensure directory exists
|
|
234
|
+
await fs.mkdir(path.dirname(credPath), { recursive: true });
|
|
235
|
+
// Write with restricted permissions (owner only)
|
|
236
|
+
await fs.writeFile(credPath, encrypted, { mode: 0o600 });
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Get the file path for storing encrypted credentials.
|
|
240
|
+
* @param key The credential key.
|
|
241
|
+
* @returns The file path.
|
|
242
|
+
*/
|
|
243
|
+
function getCredentialFilePath(key) {
|
|
244
|
+
const homeDir = os.homedir();
|
|
245
|
+
const configDir = process.platform === 'win32'
|
|
246
|
+
? path.join(homeDir, 'AppData', 'Local', 'TMLMobilidadeGO')
|
|
247
|
+
: path.join(homeDir, '.config', 'tmlmobilidade-go');
|
|
248
|
+
return path.join(configDir, `${key}.enc`);
|
|
249
|
+
}
|
|
250
|
+
/**
|
|
251
|
+
* Generate a machine-specific encryption key.
|
|
252
|
+
* @returns The encryption key as a Buffer.
|
|
253
|
+
*/
|
|
254
|
+
function getEncryptionKey() {
|
|
255
|
+
// Use machine-specific identifier as part of the encryption key
|
|
256
|
+
// This provides basic obfuscation (not true security, but better than plaintext)
|
|
257
|
+
const machineId = os.hostname() + os.userInfo().username;
|
|
258
|
+
return crypto.createHash('sha256').update(machineId).digest();
|
|
259
|
+
}
|
|
260
|
+
/**
|
|
261
|
+
* Encrypt text using AES-256-GCM.
|
|
262
|
+
* @param text The plaintext to encrypt.
|
|
263
|
+
* @returns The encrypted text in hex format.
|
|
264
|
+
*/
|
|
265
|
+
function encrypt(text) {
|
|
266
|
+
const algorithm = 'aes-256-gcm';
|
|
267
|
+
const key = getEncryptionKey();
|
|
268
|
+
const iv = crypto.randomBytes(16);
|
|
269
|
+
const cipher = crypto.createCipheriv(algorithm, key, iv);
|
|
270
|
+
let encrypted = cipher.update(text, 'utf8', 'hex');
|
|
271
|
+
encrypted += cipher.final('hex');
|
|
272
|
+
const authTag = cipher.getAuthTag();
|
|
273
|
+
// Return IV + authTag + encrypted data
|
|
274
|
+
return iv.toString('hex') + ':' + authTag.toString('hex') + ':' + encrypted;
|
|
275
|
+
}
|
|
276
|
+
/**
|
|
277
|
+
* Decrypt text using AES-256-GCM.
|
|
278
|
+
* @param encryptedText The encrypted text in hex format.
|
|
279
|
+
* @returns The decrypted plaintext.
|
|
280
|
+
*/
|
|
281
|
+
function decrypt(encryptedText) {
|
|
282
|
+
const algorithm = 'aes-256-gcm';
|
|
283
|
+
const key = getEncryptionKey();
|
|
284
|
+
const parts = encryptedText.split(':');
|
|
285
|
+
const iv = Buffer.from(parts[0], 'hex');
|
|
286
|
+
const authTag = Buffer.from(parts[1], 'hex');
|
|
287
|
+
const encrypted = parts[2];
|
|
288
|
+
const decipher = crypto.createDecipheriv(algorithm, key, iv);
|
|
289
|
+
decipher.setAuthTag(authTag);
|
|
290
|
+
let decrypted = decipher.update(encrypted, 'hex', 'utf8');
|
|
291
|
+
decrypted += decipher.final('utf8');
|
|
292
|
+
return decrypted;
|
|
293
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tmlmobilidade/export-data",
|
|
3
3
|
"description": "CLI tool to export data from GO.",
|
|
4
|
-
"version": "20251229.
|
|
4
|
+
"version": "20251229.1636.59",
|
|
5
5
|
"author": {
|
|
6
6
|
"email": "iso@tmlmobilidade.pt",
|
|
7
7
|
"name": "TML-ISO"
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
},
|
|
32
32
|
"scripts": {
|
|
33
33
|
"build": "tsc && resolve-tspaths",
|
|
34
|
-
"dev": "
|
|
34
|
+
"dev": "tsx src/index.ts",
|
|
35
35
|
"lint": "eslint .",
|
|
36
36
|
"lint:fix": "eslint . --fix"
|
|
37
37
|
},
|