nothumanallowed 1.0.1 → 2.0.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 +12 -4
- package/package.json +2 -2
- package/src/cli.mjs +44 -7
- package/src/commands/ask.mjs +111 -0
- package/src/commands/google-auth.mjs +29 -0
- package/src/commands/ops.mjs +77 -0
- package/src/commands/plan.mjs +45 -0
- package/src/commands/tasks.mjs +132 -0
- package/src/config.mjs +27 -0
- package/src/constants.mjs +1 -1
- package/src/services/google-calendar.mjs +216 -0
- package/src/services/google-gmail.mjs +242 -0
- package/src/services/google-oauth.mjs +272 -0
- package/src/services/llm.mjs +319 -0
- package/src/services/notification.mjs +109 -0
- package/src/services/ops-daemon.mjs +247 -0
- package/src/services/ops-pipeline.mjs +281 -0
- package/src/services/task-store.mjs +156 -0
- package/src/services/token-store.mjs +145 -0
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Encrypted token storage — AES-256-GCM with machine-derived key.
|
|
3
|
+
* Stores OAuth tokens at ~/.nha/ops/tokens.enc
|
|
4
|
+
* Zero dependencies — uses Node.js native crypto.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import crypto from 'crypto';
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import os from 'os';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
import { NHA_DIR } from '../constants.mjs';
|
|
12
|
+
|
|
13
|
+
const OPS_DIR = path.join(NHA_DIR, 'ops');
|
|
14
|
+
const TOKENS_FILE = path.join(OPS_DIR, 'tokens.enc');
|
|
15
|
+
|
|
16
|
+
/** Derive encryption key from machine-specific fingerprint */
|
|
17
|
+
function deriveKey() {
|
|
18
|
+
const fingerprint = os.hostname() + os.userInfo().username + os.homedir();
|
|
19
|
+
return crypto.createHash('sha256').update(fingerprint).digest();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Encrypt JSON data with AES-256-GCM */
|
|
23
|
+
function encrypt(data) {
|
|
24
|
+
const key = deriveKey();
|
|
25
|
+
const iv = crypto.randomBytes(16);
|
|
26
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
27
|
+
const plaintext = JSON.stringify(data);
|
|
28
|
+
const encrypted = Buffer.concat([cipher.update(plaintext, 'utf-8'), cipher.final()]);
|
|
29
|
+
const tag = cipher.getAuthTag();
|
|
30
|
+
return {
|
|
31
|
+
iv: iv.toString('base64'),
|
|
32
|
+
tag: tag.toString('base64'),
|
|
33
|
+
ciphertext: encrypted.toString('base64'),
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Decrypt AES-256-GCM data back to JSON */
|
|
38
|
+
function decrypt(envelope) {
|
|
39
|
+
const key = deriveKey();
|
|
40
|
+
const iv = Buffer.from(envelope.iv, 'base64');
|
|
41
|
+
const tag = Buffer.from(envelope.tag, 'base64');
|
|
42
|
+
const ciphertext = Buffer.from(envelope.ciphertext, 'base64');
|
|
43
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, iv);
|
|
44
|
+
decipher.setAuthTag(tag);
|
|
45
|
+
const decrypted = Buffer.concat([decipher.update(ciphertext), decipher.final()]);
|
|
46
|
+
return JSON.parse(decrypted.toString('utf-8'));
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Save OAuth tokens (encrypted).
|
|
51
|
+
* @param {object} tokens — { access_token, refresh_token, expires_at, scope, email }
|
|
52
|
+
*/
|
|
53
|
+
export function saveTokens(tokens) {
|
|
54
|
+
fs.mkdirSync(OPS_DIR, { recursive: true });
|
|
55
|
+
const envelope = encrypt(tokens);
|
|
56
|
+
fs.writeFileSync(TOKENS_FILE, JSON.stringify(envelope, null, 2), { mode: 0o600 });
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Load OAuth tokens (decrypted).
|
|
61
|
+
* @returns {object|null} tokens or null if not stored
|
|
62
|
+
*/
|
|
63
|
+
export function loadTokens() {
|
|
64
|
+
if (!fs.existsSync(TOKENS_FILE)) return null;
|
|
65
|
+
try {
|
|
66
|
+
const envelope = JSON.parse(fs.readFileSync(TOKENS_FILE, 'utf-8'));
|
|
67
|
+
return decrypt(envelope);
|
|
68
|
+
} catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete stored tokens.
|
|
75
|
+
*/
|
|
76
|
+
export function deleteTokens() {
|
|
77
|
+
if (fs.existsSync(TOKENS_FILE)) fs.rmSync(TOKENS_FILE);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Check if access token is expired (with 5-minute buffer).
|
|
82
|
+
* @param {object} tokens
|
|
83
|
+
* @returns {boolean}
|
|
84
|
+
*/
|
|
85
|
+
export function isExpired(tokens) {
|
|
86
|
+
if (!tokens?.expires_at) return true;
|
|
87
|
+
return Date.now() >= tokens.expires_at - 300_000;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Refresh access token using refresh_token.
|
|
92
|
+
* @param {string} clientId
|
|
93
|
+
* @param {string} clientSecret
|
|
94
|
+
* @param {string} refreshToken
|
|
95
|
+
* @returns {Promise<object>} new token set
|
|
96
|
+
*/
|
|
97
|
+
export async function refreshAccessToken(clientId, clientSecret, refreshToken) {
|
|
98
|
+
const params = new URLSearchParams({
|
|
99
|
+
client_id: clientId,
|
|
100
|
+
client_secret: clientSecret,
|
|
101
|
+
refresh_token: refreshToken,
|
|
102
|
+
grant_type: 'refresh_token',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
const res = await fetch('https://oauth2.googleapis.com/token', {
|
|
106
|
+
method: 'POST',
|
|
107
|
+
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
|
108
|
+
body: params.toString(),
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
if (!res.ok) {
|
|
112
|
+
const err = await res.text();
|
|
113
|
+
throw new Error(`Token refresh failed: ${err}`);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const data = await res.json();
|
|
117
|
+
return {
|
|
118
|
+
access_token: data.access_token,
|
|
119
|
+
refresh_token: refreshToken, // Google doesn't always return a new refresh token
|
|
120
|
+
expires_at: Date.now() + (data.expires_in * 1000),
|
|
121
|
+
scope: data.scope,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Get a valid access token — refreshes automatically if expired.
|
|
127
|
+
* @param {object} config — NHA config with google.clientId etc.
|
|
128
|
+
* @returns {Promise<string>} access_token
|
|
129
|
+
*/
|
|
130
|
+
export async function getAccessToken(config) {
|
|
131
|
+
let tokens = loadTokens();
|
|
132
|
+
if (!tokens) throw new Error('Not authenticated. Run: nha google auth');
|
|
133
|
+
|
|
134
|
+
if (isExpired(tokens)) {
|
|
135
|
+
const clientId = config.google?.clientId;
|
|
136
|
+
const clientSecret = config.google?.clientSecret || '';
|
|
137
|
+
if (!clientId) throw new Error('Google client ID not configured');
|
|
138
|
+
|
|
139
|
+
tokens = await refreshAccessToken(clientId, clientSecret, tokens.refresh_token);
|
|
140
|
+
tokens.email = loadTokens()?.email; // preserve email
|
|
141
|
+
saveTokens(tokens);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return tokens.access_token;
|
|
145
|
+
}
|