@nestpilot/mcp-app 1.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 +350 -0
- package/dist/cli/doctor.d.ts +1 -0
- package/dist/cli/doctor.js +214 -0
- package/dist/cli/export-import.d.ts +6 -0
- package/dist/cli/export-import.js +132 -0
- package/dist/cli/index.d.ts +2 -0
- package/dist/cli/index.js +168 -0
- package/dist/cli/init.d.ts +1 -0
- package/dist/cli/init.js +171 -0
- package/dist/host-configs/cowork.json +11 -0
- package/dist/host-configs/goose.yaml +22 -0
- package/dist/host-configs/openclaw-manifest.json +16 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +128 -0
- package/dist/mcp-app.html +155 -0
- package/dist/nestpilot-client.d.ts +44 -0
- package/dist/nestpilot-client.js +160 -0
- package/dist/planner.html +222 -0
- package/dist/server.d.ts +19 -0
- package/dist/server.js +245 -0
- package/dist/skills/SKILL.md +162 -0
- package/dist/skills/manifest.json +51 -0
- package/dist/skills/tools/activate_plan.md +36 -0
- package/dist/skills/tools/coach.md +59 -0
- package/dist/skills/tools/comprehensive_plan.md +65 -0
- package/dist/skills/tools/create_plan.md +59 -0
- package/dist/skills/tools/create_saved_plan.md +49 -0
- package/dist/skills/tools/delete_plan.md +42 -0
- package/dist/skills/tools/delete_scenario.md +38 -0
- package/dist/skills/tools/generate_proposal.md +63 -0
- package/dist/skills/tools/generate_retirement_report.md +50 -0
- package/dist/skills/tools/get_active_plan.md +44 -0
- package/dist/skills/tools/get_baseline_forecast.md +47 -0
- package/dist/skills/tools/get_plan.md +44 -0
- package/dist/skills/tools/get_plan_components.md +50 -0
- package/dist/skills/tools/get_scenario.md +46 -0
- package/dist/skills/tools/list_plans.md +44 -0
- package/dist/skills/tools/list_scenarios.md +42 -0
- package/dist/skills/tools/medicare-guardian.md +59 -0
- package/dist/skills/tools/nestpilot_run_plan.md +61 -0
- package/dist/skills/tools/optimize_roth_conversion.md +107 -0
- package/dist/skills/tools/optimize_ss_claiming.md +30 -0
- package/dist/skills/tools/rename_plan.md +34 -0
- package/dist/skills/tools/retirement-planner.md +55 -0
- package/dist/skills/tools/run_forecast.md +65 -0
- package/dist/skills/tools/run_saved_forecast.md +52 -0
- package/dist/skills/tools/run_scenario.md +66 -0
- package/dist/skills/tools/save_plan.md +48 -0
- package/dist/skills/tools/save_scenario.md +50 -0
- package/dist/skills/tools/verify_forecast.md +43 -0
- package/dist/src/config.d.ts +20 -0
- package/dist/src/config.js +44 -0
- package/dist/src/contracts/provenance.d.ts +37 -0
- package/dist/src/contracts/provenance.js +71 -0
- package/dist/src/contracts/tool-contract-registry.d.ts +43 -0
- package/dist/src/contracts/tool-contract-registry.js +282 -0
- package/dist/src/local/cloud-compute-client.d.ts +55 -0
- package/dist/src/local/cloud-compute-client.js +135 -0
- package/dist/src/local/encryption.d.ts +24 -0
- package/dist/src/local/encryption.js +105 -0
- package/dist/src/local/keychain.d.ts +41 -0
- package/dist/src/local/keychain.js +236 -0
- package/dist/src/local/local-config.d.ts +34 -0
- package/dist/src/local/local-config.js +61 -0
- package/dist/src/local/local-data-layer.d.ts +20 -0
- package/dist/src/local/local-data-layer.js +15 -0
- package/dist/src/local/local-plan-store.d.ts +66 -0
- package/dist/src/local/local-plan-store.js +195 -0
- package/dist/src/local/pii-scrubber.d.ts +26 -0
- package/dist/src/local/pii-scrubber.js +219 -0
- package/dist/src/policy/policy-engine.d.ts +44 -0
- package/dist/src/policy/policy-engine.js +119 -0
- package/dist/src/rate-limit.d.ts +17 -0
- package/dist/src/rate-limit.js +41 -0
- package/dist/src/security.d.ts +19 -0
- package/dist/src/security.js +118 -0
- package/dist/src/skills/index.d.ts +12 -0
- package/dist/src/skills/index.js +16 -0
- package/dist/src/skills/retirement-pack-v1.d.ts +28 -0
- package/dist/src/skills/retirement-pack-v1.js +295 -0
- package/dist/src/skills/skill-executor.d.ts +65 -0
- package/dist/src/skills/skill-executor.js +174 -0
- package/dist/src/skills/skill-manifest-schema.d.ts +337 -0
- package/dist/src/skills/skill-manifest-schema.js +94 -0
- package/dist/src/skills/skill-registry.d.ts +71 -0
- package/dist/src/skills/skill-registry.js +116 -0
- package/dist/src/telemetry.d.ts +12 -0
- package/dist/src/telemetry.js +59 -0
- package/dist/src/types.d.ts +46 -0
- package/dist/src/types.js +4 -0
- package/dist/tools/agent-tools.d.ts +12 -0
- package/dist/tools/agent-tools.js +141 -0
- package/dist/tools/forecast-management-tools.d.ts +9 -0
- package/dist/tools/forecast-management-tools.js +133 -0
- package/dist/tools/local-plan-tools.d.ts +8 -0
- package/dist/tools/local-plan-tools.js +357 -0
- package/dist/tools/mcp-helpers.d.ts +52 -0
- package/dist/tools/mcp-helpers.js +177 -0
- package/dist/tools/medicare-tools.d.ts +3 -0
- package/dist/tools/medicare-tools.js +162 -0
- package/dist/tools/optimize-roth-tools-test.d.ts +2 -0
- package/dist/tools/optimize-roth-tools-test.js +36 -0
- package/dist/tools/optimize-roth-tools.d.ts +3 -0
- package/dist/tools/optimize-roth-tools.js +818 -0
- package/dist/tools/plan-management-tools.d.ts +3 -0
- package/dist/tools/plan-management-tools.js +196 -0
- package/dist/tools/planning-tools.d.ts +3 -0
- package/dist/tools/planning-tools.js +290 -0
- package/dist/tools/proposal-tools.d.ts +3 -0
- package/dist/tools/proposal-tools.js +428 -0
- package/dist/tools/report-tools.d.ts +3 -0
- package/dist/tools/report-tools.js +245 -0
- package/dist/tools/scenario-management-tools.d.ts +3 -0
- package/dist/tools/scenario-management-tools.js +136 -0
- package/dist/views/verification-packet.html +211 -0
- package/host-configs/cowork.json +11 -0
- package/host-configs/goose.yaml +22 -0
- package/host-configs/openclaw-manifest.json +16 -0
- package/package.json +66 -0
- package/skills/SKILL.md +162 -0
- package/skills/manifest.json +51 -0
- package/skills/tools/activate_plan.md +36 -0
- package/skills/tools/coach.md +59 -0
- package/skills/tools/comprehensive_plan.md +65 -0
- package/skills/tools/create_plan.md +59 -0
- package/skills/tools/create_saved_plan.md +49 -0
- package/skills/tools/delete_plan.md +42 -0
- package/skills/tools/delete_scenario.md +38 -0
- package/skills/tools/generate_proposal.md +63 -0
- package/skills/tools/generate_retirement_report.md +50 -0
- package/skills/tools/get_active_plan.md +44 -0
- package/skills/tools/get_baseline_forecast.md +47 -0
- package/skills/tools/get_plan.md +44 -0
- package/skills/tools/get_plan_components.md +50 -0
- package/skills/tools/get_scenario.md +46 -0
- package/skills/tools/list_plans.md +44 -0
- package/skills/tools/list_scenarios.md +42 -0
- package/skills/tools/medicare-guardian.md +59 -0
- package/skills/tools/nestpilot_run_plan.md +61 -0
- package/skills/tools/optimize_roth_conversion.md +107 -0
- package/skills/tools/optimize_ss_claiming.md +30 -0
- package/skills/tools/rename_plan.md +34 -0
- package/skills/tools/retirement-planner.md +55 -0
- package/skills/tools/run_forecast.md +65 -0
- package/skills/tools/run_saved_forecast.md +52 -0
- package/skills/tools/run_scenario.md +66 -0
- package/skills/tools/save_plan.md +48 -0
- package/skills/tools/save_scenario.md +50 -0
- package/skills/tools/verify_forecast.md +43 -0
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloud Compute Client — sends PII-free mathematical payloads to the
|
|
3
|
+
* NestPilot cloud API for compute-intensive operations (forecast, scenario,
|
|
4
|
+
* Roth optimization, coaching).
|
|
5
|
+
*
|
|
6
|
+
* All requests include:
|
|
7
|
+
* - `Authorization: Bearer {apiKey}` for metering
|
|
8
|
+
* - `X-Client-Mode: local` to distinguish local-first traffic
|
|
9
|
+
* - 30 second timeout
|
|
10
|
+
*
|
|
11
|
+
* On network failure, the client returns cached results when available.
|
|
12
|
+
*
|
|
13
|
+
* @feature FEAT-0087
|
|
14
|
+
*/
|
|
15
|
+
// ── Configuration ────────────────────────────────────────────────────────
|
|
16
|
+
const DEFAULT_TIMEOUT_MS = 30_000;
|
|
17
|
+
// ── CloudComputeClient ───────────────────────────────────────────────────
|
|
18
|
+
export class CloudComputeClient {
|
|
19
|
+
cloudApiUrl;
|
|
20
|
+
apiKey;
|
|
21
|
+
constructor(cloudApiUrl, apiKey) {
|
|
22
|
+
this.cloudApiUrl = cloudApiUrl;
|
|
23
|
+
this.apiKey = apiKey;
|
|
24
|
+
// Strip trailing slash
|
|
25
|
+
if (this.cloudApiUrl.endsWith("/")) {
|
|
26
|
+
this.cloudApiUrl = this.cloudApiUrl.slice(0, -1);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Run a retirement forecast against the cloud engine.
|
|
31
|
+
*/
|
|
32
|
+
async forecast(payload) {
|
|
33
|
+
return this.post("/api/plan/forecast", payload);
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Run a what-if scenario comparison against the cloud engine.
|
|
37
|
+
*/
|
|
38
|
+
async scenario(payload) {
|
|
39
|
+
return this.post("/api/plan/scenario/run", payload);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Run Roth conversion optimization against the cloud engine.
|
|
43
|
+
*/
|
|
44
|
+
async rothOptimize(payload) {
|
|
45
|
+
return this.post("/api/plan/roth/optimize", payload);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Get AI coaching guidance from the cloud engine.
|
|
49
|
+
*/
|
|
50
|
+
async coach(payload) {
|
|
51
|
+
return this.post("/api/spring_ai/coach", payload);
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Run forecast verification against the cloud engine.
|
|
55
|
+
*/
|
|
56
|
+
async verify(payload) {
|
|
57
|
+
return this.post("/api/plan/forecast/verify", payload);
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Quick-calc for initial plan creation.
|
|
61
|
+
*/
|
|
62
|
+
async quickCalc(payload) {
|
|
63
|
+
return this.post("/api/plan/calc", payload);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* Health check — verifies the cloud API is reachable.
|
|
67
|
+
*/
|
|
68
|
+
async healthCheck() {
|
|
69
|
+
try {
|
|
70
|
+
const response = await fetch(`${this.cloudApiUrl}/actuator/health`, {
|
|
71
|
+
method: "GET",
|
|
72
|
+
headers: this.buildHeaders(),
|
|
73
|
+
signal: AbortSignal.timeout(5_000),
|
|
74
|
+
});
|
|
75
|
+
return response.ok;
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
// ── Internal ──────────────────────────────────────────────────────────
|
|
82
|
+
async post(endpoint, payload) {
|
|
83
|
+
const url = `${this.cloudApiUrl}${endpoint}`;
|
|
84
|
+
try {
|
|
85
|
+
const response = await fetch(url, {
|
|
86
|
+
method: "POST",
|
|
87
|
+
headers: this.buildHeaders(),
|
|
88
|
+
body: JSON.stringify(payload),
|
|
89
|
+
signal: AbortSignal.timeout(DEFAULT_TIMEOUT_MS),
|
|
90
|
+
});
|
|
91
|
+
if (!response.ok) {
|
|
92
|
+
const text = await response.text();
|
|
93
|
+
// Rate limiting
|
|
94
|
+
if (response.status === 429) {
|
|
95
|
+
const retryAfter = response.headers.get("Retry-After");
|
|
96
|
+
return {
|
|
97
|
+
error: true,
|
|
98
|
+
message: `Cloud API rate limit exceeded. ${retryAfter
|
|
99
|
+
? `Retry after ${retryAfter} seconds.`
|
|
100
|
+
: "Please wait and try again."} You may be on the free tier (10 forecasts/month). Upgrade at https://nestpilot.com/pricing.`,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
return {
|
|
104
|
+
error: true,
|
|
105
|
+
message: `Cloud API error (${response.status}): ${text}`,
|
|
106
|
+
};
|
|
107
|
+
}
|
|
108
|
+
const data = (await response.json());
|
|
109
|
+
return { data };
|
|
110
|
+
}
|
|
111
|
+
catch (e) {
|
|
112
|
+
const errorMessage = e instanceof Error ? e.message : String(e);
|
|
113
|
+
// Distinguish timeout from network errors
|
|
114
|
+
if (errorMessage.includes("abort") || errorMessage.includes("timeout")) {
|
|
115
|
+
return {
|
|
116
|
+
error: true,
|
|
117
|
+
message: "Cloud API request timed out after 30 seconds. The compute engine may be under heavy load. Try again shortly.",
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
return {
|
|
121
|
+
error: true,
|
|
122
|
+
message: `Cannot reach NestPilot cloud API at ${this.cloudApiUrl}. Check your internet connection. Error: ${errorMessage}`,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
buildHeaders() {
|
|
127
|
+
return {
|
|
128
|
+
"Content-Type": "application/json",
|
|
129
|
+
Accept: "application/json",
|
|
130
|
+
Authorization: `Bearer ${this.apiKey}`,
|
|
131
|
+
"X-Client-Mode": "local",
|
|
132
|
+
"X-Client-Version": "1.0.0",
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import type { KeychainProvider } from "./keychain.js";
|
|
2
|
+
export declare class EncryptionService {
|
|
3
|
+
private keyProvider;
|
|
4
|
+
private cachedKey;
|
|
5
|
+
constructor(keyProvider: KeychainProvider);
|
|
6
|
+
/**
|
|
7
|
+
* Encrypts a UTF-8 string into an NPE envelope buffer.
|
|
8
|
+
*/
|
|
9
|
+
encrypt(data: string): Promise<Buffer>;
|
|
10
|
+
/**
|
|
11
|
+
* Decrypts an NPE envelope buffer back into a UTF-8 string.
|
|
12
|
+
*/
|
|
13
|
+
decrypt(envelope: Buffer): Promise<string>;
|
|
14
|
+
/**
|
|
15
|
+
* Generates a fresh 256-bit encryption key and stores it in the keychain.
|
|
16
|
+
* Returns the hex-encoded key.
|
|
17
|
+
*/
|
|
18
|
+
generateAndStoreKey(): Promise<string>;
|
|
19
|
+
/**
|
|
20
|
+
* Checks whether an encryption key exists in the keychain.
|
|
21
|
+
*/
|
|
22
|
+
hasKey(): Promise<boolean>;
|
|
23
|
+
private getKey;
|
|
24
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* AES-256-GCM encryption for NestPilot data at rest.
|
|
3
|
+
*
|
|
4
|
+
* All plan data stored under ~/.nestpilot/ is encrypted using a key
|
|
5
|
+
* fetched from the OS keychain. The envelope format is:
|
|
6
|
+
*
|
|
7
|
+
* [3-byte magic "NPE"] [1-byte version] [12-byte IV] [ciphertext] [16-byte auth tag]
|
|
8
|
+
*
|
|
9
|
+
* @feature FEAT-0087
|
|
10
|
+
*/
|
|
11
|
+
import { createCipheriv, createDecipheriv, randomBytes, } from "node:crypto";
|
|
12
|
+
// ── Constants ────────────────────────────────────────────────────────────
|
|
13
|
+
const ALGORITHM = "aes-256-gcm";
|
|
14
|
+
const IV_LENGTH = 12; // 96 bits — GCM recommended
|
|
15
|
+
const AUTH_TAG_LENGTH = 16;
|
|
16
|
+
const MAGIC = Buffer.from("NPE"); // NestPilot Encrypted
|
|
17
|
+
const ENVELOPE_VERSION = 1;
|
|
18
|
+
const HEADER_LENGTH = MAGIC.length + 1 + IV_LENGTH; // 3 + 1 + 12 = 16
|
|
19
|
+
const KEYCHAIN_SERVICE = "nestpilot";
|
|
20
|
+
const KEYCHAIN_ENCRYPTION_ACCOUNT = "encryption-key";
|
|
21
|
+
// ── EncryptionService ────────────────────────────────────────────────────
|
|
22
|
+
export class EncryptionService {
|
|
23
|
+
keyProvider;
|
|
24
|
+
cachedKey = null;
|
|
25
|
+
constructor(keyProvider) {
|
|
26
|
+
this.keyProvider = keyProvider;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Encrypts a UTF-8 string into an NPE envelope buffer.
|
|
30
|
+
*/
|
|
31
|
+
async encrypt(data) {
|
|
32
|
+
const key = await this.getKey();
|
|
33
|
+
const iv = randomBytes(IV_LENGTH);
|
|
34
|
+
const cipher = createCipheriv(ALGORITHM, key, iv, {
|
|
35
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
36
|
+
});
|
|
37
|
+
const encrypted = Buffer.concat([
|
|
38
|
+
cipher.update(data, "utf-8"),
|
|
39
|
+
cipher.final(),
|
|
40
|
+
]);
|
|
41
|
+
const authTag = cipher.getAuthTag();
|
|
42
|
+
// Envelope: MAGIC + VERSION + IV + CIPHERTEXT + AUTH_TAG
|
|
43
|
+
return Buffer.concat([
|
|
44
|
+
MAGIC,
|
|
45
|
+
Buffer.from([ENVELOPE_VERSION]),
|
|
46
|
+
iv,
|
|
47
|
+
encrypted,
|
|
48
|
+
authTag,
|
|
49
|
+
]);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Decrypts an NPE envelope buffer back into a UTF-8 string.
|
|
53
|
+
*/
|
|
54
|
+
async decrypt(envelope) {
|
|
55
|
+
// Validate magic
|
|
56
|
+
if (envelope.length < HEADER_LENGTH + AUTH_TAG_LENGTH ||
|
|
57
|
+
!envelope.subarray(0, MAGIC.length).equals(MAGIC)) {
|
|
58
|
+
throw new Error("Invalid encrypted envelope: bad magic header");
|
|
59
|
+
}
|
|
60
|
+
const version = envelope[MAGIC.length];
|
|
61
|
+
if (version !== ENVELOPE_VERSION) {
|
|
62
|
+
throw new Error(`Unsupported encryption version: ${version}`);
|
|
63
|
+
}
|
|
64
|
+
const iv = envelope.subarray(MAGIC.length + 1, HEADER_LENGTH);
|
|
65
|
+
const authTag = envelope.subarray(envelope.length - AUTH_TAG_LENGTH);
|
|
66
|
+
const ciphertext = envelope.subarray(HEADER_LENGTH, envelope.length - AUTH_TAG_LENGTH);
|
|
67
|
+
const key = await this.getKey();
|
|
68
|
+
const decipher = createDecipheriv(ALGORITHM, key, iv, {
|
|
69
|
+
authTagLength: AUTH_TAG_LENGTH,
|
|
70
|
+
});
|
|
71
|
+
decipher.setAuthTag(authTag);
|
|
72
|
+
return Buffer.concat([
|
|
73
|
+
decipher.update(ciphertext),
|
|
74
|
+
decipher.final(),
|
|
75
|
+
]).toString("utf-8");
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Generates a fresh 256-bit encryption key and stores it in the keychain.
|
|
79
|
+
* Returns the hex-encoded key.
|
|
80
|
+
*/
|
|
81
|
+
async generateAndStoreKey() {
|
|
82
|
+
const key = randomBytes(32).toString("hex");
|
|
83
|
+
await this.keyProvider.set(KEYCHAIN_SERVICE, KEYCHAIN_ENCRYPTION_ACCOUNT, key);
|
|
84
|
+
this.cachedKey = Buffer.from(key, "hex");
|
|
85
|
+
return key;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Checks whether an encryption key exists in the keychain.
|
|
89
|
+
*/
|
|
90
|
+
async hasKey() {
|
|
91
|
+
const raw = await this.keyProvider.get(KEYCHAIN_SERVICE, KEYCHAIN_ENCRYPTION_ACCOUNT);
|
|
92
|
+
return raw !== null;
|
|
93
|
+
}
|
|
94
|
+
// ── Internal ──────────────────────────────────────────────────────────
|
|
95
|
+
async getKey() {
|
|
96
|
+
if (this.cachedKey)
|
|
97
|
+
return this.cachedKey;
|
|
98
|
+
const raw = await this.keyProvider.get(KEYCHAIN_SERVICE, KEYCHAIN_ENCRYPTION_ACCOUNT);
|
|
99
|
+
if (!raw) {
|
|
100
|
+
throw new Error("Encryption key not found in keychain. Run `nestpilot init` to set up encryption.");
|
|
101
|
+
}
|
|
102
|
+
this.cachedKey = Buffer.from(raw, "hex");
|
|
103
|
+
return this.cachedKey;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export interface KeychainProvider {
|
|
2
|
+
get(service: string, account: string): Promise<string | null>;
|
|
3
|
+
set(service: string, account: string, value: string): Promise<void>;
|
|
4
|
+
delete(service: string, account: string): Promise<void>;
|
|
5
|
+
}
|
|
6
|
+
declare class MacOSKeychain implements KeychainProvider {
|
|
7
|
+
get(service: string, account: string): Promise<string | null>;
|
|
8
|
+
set(service: string, account: string, value: string): Promise<void>;
|
|
9
|
+
delete(service: string, account: string): Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
declare class WindowsKeychain implements KeychainProvider {
|
|
12
|
+
private target;
|
|
13
|
+
get(service: string, account: string): Promise<string | null>;
|
|
14
|
+
set(service: string, account: string, value: string): Promise<void>;
|
|
15
|
+
delete(service: string, account: string): Promise<void>;
|
|
16
|
+
}
|
|
17
|
+
declare class LinuxKeychain implements KeychainProvider {
|
|
18
|
+
get(service: string, account: string): Promise<string | null>;
|
|
19
|
+
set(service: string, account: string, value: string): Promise<void>;
|
|
20
|
+
delete(service: string, account: string): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Fallback keychain that stores credentials in a JSON file.
|
|
24
|
+
* Used when no OS keychain is available. The file itself should be
|
|
25
|
+
* in a protected directory with 0600 permissions.
|
|
26
|
+
*/
|
|
27
|
+
declare class FileKeychain implements KeychainProvider {
|
|
28
|
+
private filePath;
|
|
29
|
+
constructor(dataDir?: string);
|
|
30
|
+
get(service: string, account: string): Promise<string | null>;
|
|
31
|
+
set(service: string, account: string, value: string): Promise<void>;
|
|
32
|
+
delete(service: string, account: string): Promise<void>;
|
|
33
|
+
private load;
|
|
34
|
+
private save;
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Creates the appropriate KeychainProvider for the current platform.
|
|
38
|
+
* Falls back to file-based storage if no OS keychain is detected.
|
|
39
|
+
*/
|
|
40
|
+
export declare function createKeychainProvider(dataDir?: string): KeychainProvider;
|
|
41
|
+
export { FileKeychain, LinuxKeychain, MacOSKeychain, WindowsKeychain };
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OS-native keychain integration for NestPilot secrets.
|
|
3
|
+
*
|
|
4
|
+
* Provides a platform-agnostic interface for storing API keys and encryption
|
|
5
|
+
* keys in the operating system's secure credential store.
|
|
6
|
+
*
|
|
7
|
+
* Platform mapping:
|
|
8
|
+
* - macOS : Keychain (via `security` CLI)
|
|
9
|
+
* - Windows: Windows Credential Manager (via `cmdkey` / PowerShell)
|
|
10
|
+
* - Linux : libsecret / Secret Service API (GNOME Keyring / KWallet)
|
|
11
|
+
* - Fallback: Encrypted file at ~/.nestpilot/credentials.enc
|
|
12
|
+
*
|
|
13
|
+
* @feature FEAT-0087
|
|
14
|
+
*/
|
|
15
|
+
import { execFile } from "node:child_process";
|
|
16
|
+
import fs from "node:fs/promises";
|
|
17
|
+
import os from "node:os";
|
|
18
|
+
import path from "node:path";
|
|
19
|
+
import { promisify } from "node:util";
|
|
20
|
+
const execFileAsync = promisify(execFile);
|
|
21
|
+
// ── macOS Keychain ───────────────────────────────────────────────────────
|
|
22
|
+
class MacOSKeychain {
|
|
23
|
+
async get(service, account) {
|
|
24
|
+
try {
|
|
25
|
+
const { stdout } = await execFileAsync("security", [
|
|
26
|
+
"find-generic-password",
|
|
27
|
+
"-s",
|
|
28
|
+
service,
|
|
29
|
+
"-a",
|
|
30
|
+
account,
|
|
31
|
+
"-w",
|
|
32
|
+
]);
|
|
33
|
+
return stdout.trim() || null;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
async set(service, account, value) {
|
|
40
|
+
// Delete first to allow update
|
|
41
|
+
try {
|
|
42
|
+
await execFileAsync("security", [
|
|
43
|
+
"delete-generic-password",
|
|
44
|
+
"-s",
|
|
45
|
+
service,
|
|
46
|
+
"-a",
|
|
47
|
+
account,
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
// Ignore if not found
|
|
52
|
+
}
|
|
53
|
+
await execFileAsync("security", [
|
|
54
|
+
"add-generic-password",
|
|
55
|
+
"-s",
|
|
56
|
+
service,
|
|
57
|
+
"-a",
|
|
58
|
+
account,
|
|
59
|
+
"-w",
|
|
60
|
+
value,
|
|
61
|
+
]);
|
|
62
|
+
}
|
|
63
|
+
async delete(service, account) {
|
|
64
|
+
try {
|
|
65
|
+
await execFileAsync("security", [
|
|
66
|
+
"delete-generic-password",
|
|
67
|
+
"-s",
|
|
68
|
+
service,
|
|
69
|
+
"-a",
|
|
70
|
+
account,
|
|
71
|
+
]);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Ignore if not found
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
// ── Windows Credential Manager ───────────────────────────────────────────
|
|
79
|
+
class WindowsKeychain {
|
|
80
|
+
target(service, account) {
|
|
81
|
+
return `${service}:${account}`;
|
|
82
|
+
}
|
|
83
|
+
async get(service, account) {
|
|
84
|
+
try {
|
|
85
|
+
const target = this.target(service, account);
|
|
86
|
+
const { stdout } = await execFileAsync("powershell.exe", [
|
|
87
|
+
"-NoProfile",
|
|
88
|
+
"-Command",
|
|
89
|
+
`$cred = Get-StoredCredential -Target '${target}' -ErrorAction SilentlyContinue; if ($cred) { $cred.GetNetworkCredential().Password } else { $null }`,
|
|
90
|
+
]);
|
|
91
|
+
const result = stdout.trim();
|
|
92
|
+
return result && result !== "" ? result : null;
|
|
93
|
+
}
|
|
94
|
+
catch {
|
|
95
|
+
// Fallback: try cmdkey
|
|
96
|
+
try {
|
|
97
|
+
const target = this.target(service, account);
|
|
98
|
+
const { stdout } = await execFileAsync("powershell.exe", [
|
|
99
|
+
"-NoProfile",
|
|
100
|
+
"-Command",
|
|
101
|
+
`[System.Runtime.InteropServices.Marshal]::PtrToStringAuto([System.Runtime.InteropServices.Marshal]::SecureStringToBSTR((New-Object System.Management.Automation.PSCredential('_', (cmdkey /generic:${target} /list 2>$null | Out-Null; [System.Security.SecureString]::new()))).Password))`,
|
|
102
|
+
]);
|
|
103
|
+
return stdout.trim() || null;
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
async set(service, account, value) {
|
|
111
|
+
const target = this.target(service, account);
|
|
112
|
+
await execFileAsync("cmdkey", [
|
|
113
|
+
`/generic:${target}`,
|
|
114
|
+
`/user:${account}`,
|
|
115
|
+
`/pass:${value}`,
|
|
116
|
+
]);
|
|
117
|
+
}
|
|
118
|
+
async delete(service, account) {
|
|
119
|
+
try {
|
|
120
|
+
const target = this.target(service, account);
|
|
121
|
+
await execFileAsync("cmdkey", [`/delete:${target}`]);
|
|
122
|
+
}
|
|
123
|
+
catch {
|
|
124
|
+
// Ignore if not found
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
// ── Linux Secret Service ─────────────────────────────────────────────────
|
|
129
|
+
class LinuxKeychain {
|
|
130
|
+
async get(service, account) {
|
|
131
|
+
try {
|
|
132
|
+
const { stdout } = await execFileAsync("secret-tool", [
|
|
133
|
+
"lookup",
|
|
134
|
+
"service",
|
|
135
|
+
service,
|
|
136
|
+
"account",
|
|
137
|
+
account,
|
|
138
|
+
]);
|
|
139
|
+
return stdout.trim() || null;
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
return null;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
async set(service, account, value) {
|
|
146
|
+
const child = execFileAsync("secret-tool", [
|
|
147
|
+
"store",
|
|
148
|
+
"--label",
|
|
149
|
+
`${service}:${account}`,
|
|
150
|
+
"service",
|
|
151
|
+
service,
|
|
152
|
+
"account",
|
|
153
|
+
account,
|
|
154
|
+
]);
|
|
155
|
+
// secret-tool reads the secret from stdin
|
|
156
|
+
child.child.stdin?.write(value);
|
|
157
|
+
child.child.stdin?.end();
|
|
158
|
+
await child;
|
|
159
|
+
}
|
|
160
|
+
async delete(service, account) {
|
|
161
|
+
try {
|
|
162
|
+
await execFileAsync("secret-tool", [
|
|
163
|
+
"clear",
|
|
164
|
+
"service",
|
|
165
|
+
service,
|
|
166
|
+
"account",
|
|
167
|
+
account,
|
|
168
|
+
]);
|
|
169
|
+
}
|
|
170
|
+
catch {
|
|
171
|
+
// Ignore if not found
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
// ── File-based fallback ──────────────────────────────────────────────────
|
|
176
|
+
/**
|
|
177
|
+
* Fallback keychain that stores credentials in a JSON file.
|
|
178
|
+
* Used when no OS keychain is available. The file itself should be
|
|
179
|
+
* in a protected directory with 0600 permissions.
|
|
180
|
+
*/
|
|
181
|
+
class FileKeychain {
|
|
182
|
+
filePath;
|
|
183
|
+
constructor(dataDir) {
|
|
184
|
+
this.filePath = path.join(dataDir ?? path.join(os.homedir(), ".nestpilot"), "credentials.json");
|
|
185
|
+
}
|
|
186
|
+
async get(service, account) {
|
|
187
|
+
const data = await this.load();
|
|
188
|
+
return data[`${service}:${account}`] ?? null;
|
|
189
|
+
}
|
|
190
|
+
async set(service, account, value) {
|
|
191
|
+
const data = await this.load();
|
|
192
|
+
data[`${service}:${account}`] = value;
|
|
193
|
+
await this.save(data);
|
|
194
|
+
}
|
|
195
|
+
async delete(service, account) {
|
|
196
|
+
const data = await this.load();
|
|
197
|
+
delete data[`${service}:${account}`];
|
|
198
|
+
await this.save(data);
|
|
199
|
+
}
|
|
200
|
+
async load() {
|
|
201
|
+
try {
|
|
202
|
+
const raw = await fs.readFile(this.filePath, "utf-8");
|
|
203
|
+
return JSON.parse(raw);
|
|
204
|
+
}
|
|
205
|
+
catch {
|
|
206
|
+
return {};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
async save(data) {
|
|
210
|
+
const dir = path.dirname(this.filePath);
|
|
211
|
+
await fs.mkdir(dir, { recursive: true });
|
|
212
|
+
await fs.writeFile(this.filePath, JSON.stringify(data, null, 2), {
|
|
213
|
+
mode: 0o600, // Owner read/write only
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
// ── Factory ──────────────────────────────────────────────────────────────
|
|
218
|
+
/**
|
|
219
|
+
* Creates the appropriate KeychainProvider for the current platform.
|
|
220
|
+
* Falls back to file-based storage if no OS keychain is detected.
|
|
221
|
+
*/
|
|
222
|
+
export function createKeychainProvider(dataDir) {
|
|
223
|
+
const platform = os.platform();
|
|
224
|
+
switch (platform) {
|
|
225
|
+
case "darwin":
|
|
226
|
+
return new MacOSKeychain();
|
|
227
|
+
case "win32":
|
|
228
|
+
return new WindowsKeychain();
|
|
229
|
+
case "linux":
|
|
230
|
+
return new LinuxKeychain();
|
|
231
|
+
default:
|
|
232
|
+
console.warn(`[keychain] No native keychain support for ${platform}, using file-based fallback`);
|
|
233
|
+
return new FileKeychain(dataDir);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
export { FileKeychain, LinuxKeychain, MacOSKeychain, WindowsKeychain };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
export type NestpilotMode = "cloud" | "local";
|
|
2
|
+
export interface LocalConfig {
|
|
3
|
+
/** Runtime mode — "cloud" proxies to Spring Boot, "local" uses local data layer. */
|
|
4
|
+
mode: NestpilotMode;
|
|
5
|
+
/** Root directory for local data (default: ~/.nestpilot). */
|
|
6
|
+
dataDir: string;
|
|
7
|
+
/** Cloud API URL for PII-free compute calls. */
|
|
8
|
+
cloudApiUrl: string;
|
|
9
|
+
/** API key retrieved from keychain (populated lazily). */
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
/** Whether data-at-rest encryption is active (always true in local mode). */
|
|
12
|
+
encryptionEnabled: boolean;
|
|
13
|
+
}
|
|
14
|
+
export declare const DATA_SUBDIRS: readonly ["plans", "forecasts", "scenarios", "logs"];
|
|
15
|
+
export declare function plansDir(dataDir: string): string;
|
|
16
|
+
export declare function forecastsDir(dataDir: string): string;
|
|
17
|
+
export declare function scenariosDir(dataDir: string): string;
|
|
18
|
+
export declare function logsDir(dataDir: string): string;
|
|
19
|
+
export declare function configFilePath(dataDir: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Loads the local configuration from environment variables.
|
|
22
|
+
*
|
|
23
|
+
* Environment variables:
|
|
24
|
+
* - `NESTPILOT_MODE` — "cloud" (default) or "local"
|
|
25
|
+
* - `NESTPILOT_DATA_DIR` — data root (default: ~/.nestpilot)
|
|
26
|
+
* - `NESTPILOT_CLOUD_API_URL` — cloud compute URL
|
|
27
|
+
* - `NESTPILOT_API_KEY` — API key override (normally loaded from keychain)
|
|
28
|
+
*/
|
|
29
|
+
export declare function loadLocalConfig(env?: NodeJS.ProcessEnv): LocalConfig;
|
|
30
|
+
/**
|
|
31
|
+
* Returns `true` when the server is running in local-first mode.
|
|
32
|
+
* Convenience function used by the server bootstrap.
|
|
33
|
+
*/
|
|
34
|
+
export declare function isLocalMode(env?: NodeJS.ProcessEnv): boolean;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local-first mode detection and configuration.
|
|
3
|
+
*
|
|
4
|
+
* Reads NESTPILOT_MODE ("cloud" | "local") from the environment and
|
|
5
|
+
* builds the LocalConfig object that drives all local-first behavior.
|
|
6
|
+
*
|
|
7
|
+
* @feature FEAT-0087
|
|
8
|
+
*/
|
|
9
|
+
import os from "node:os";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
// ── Well-known paths inside dataDir ──────────────────────────────────────
|
|
12
|
+
export const DATA_SUBDIRS = [
|
|
13
|
+
"plans",
|
|
14
|
+
"forecasts",
|
|
15
|
+
"scenarios",
|
|
16
|
+
"logs",
|
|
17
|
+
];
|
|
18
|
+
export function plansDir(dataDir) {
|
|
19
|
+
return path.join(dataDir, "plans");
|
|
20
|
+
}
|
|
21
|
+
export function forecastsDir(dataDir) {
|
|
22
|
+
return path.join(dataDir, "forecasts");
|
|
23
|
+
}
|
|
24
|
+
export function scenariosDir(dataDir) {
|
|
25
|
+
return path.join(dataDir, "scenarios");
|
|
26
|
+
}
|
|
27
|
+
export function logsDir(dataDir) {
|
|
28
|
+
return path.join(dataDir, "logs");
|
|
29
|
+
}
|
|
30
|
+
export function configFilePath(dataDir) {
|
|
31
|
+
return path.join(dataDir, "config.json");
|
|
32
|
+
}
|
|
33
|
+
// ── Configuration loader ─────────────────────────────────────────────────
|
|
34
|
+
const DEFAULT_CLOUD_API_URL = "https://api.nestpilot.com";
|
|
35
|
+
/**
|
|
36
|
+
* Loads the local configuration from environment variables.
|
|
37
|
+
*
|
|
38
|
+
* Environment variables:
|
|
39
|
+
* - `NESTPILOT_MODE` — "cloud" (default) or "local"
|
|
40
|
+
* - `NESTPILOT_DATA_DIR` — data root (default: ~/.nestpilot)
|
|
41
|
+
* - `NESTPILOT_CLOUD_API_URL` — cloud compute URL
|
|
42
|
+
* - `NESTPILOT_API_KEY` — API key override (normally loaded from keychain)
|
|
43
|
+
*/
|
|
44
|
+
export function loadLocalConfig(env = process.env) {
|
|
45
|
+
const mode = (env.NESTPILOT_MODE ?? "cloud");
|
|
46
|
+
const dataDir = env.NESTPILOT_DATA_DIR ?? path.join(os.homedir(), ".nestpilot");
|
|
47
|
+
return {
|
|
48
|
+
mode,
|
|
49
|
+
dataDir,
|
|
50
|
+
cloudApiUrl: env.NESTPILOT_CLOUD_API_URL ?? DEFAULT_CLOUD_API_URL,
|
|
51
|
+
apiKey: env.NESTPILOT_API_KEY,
|
|
52
|
+
encryptionEnabled: mode === "local",
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Returns `true` when the server is running in local-first mode.
|
|
57
|
+
* Convenience function used by the server bootstrap.
|
|
58
|
+
*/
|
|
59
|
+
export function isLocalMode(env = process.env) {
|
|
60
|
+
return (env.NESTPILOT_MODE ?? "cloud") === "local";
|
|
61
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Data Layer — barrel export for all local-first modules.
|
|
3
|
+
*
|
|
4
|
+
* Provides a single import point for the local runtime:
|
|
5
|
+
*
|
|
6
|
+
* import { LocalPlanStore, CloudComputeClient, ... } from "./local/local-data-layer.js";
|
|
7
|
+
*
|
|
8
|
+
* @feature FEAT-0087
|
|
9
|
+
*/
|
|
10
|
+
export { loadLocalConfig, isLocalMode } from "./local-config.js";
|
|
11
|
+
export type { NestpilotMode, LocalConfig } from "./local-config.js";
|
|
12
|
+
export { EncryptionService } from "./encryption.js";
|
|
13
|
+
export { createKeychainProvider, FileKeychain, } from "./keychain.js";
|
|
14
|
+
export type { KeychainProvider } from "./keychain.js";
|
|
15
|
+
export { LocalPlanStore } from "./local-plan-store.js";
|
|
16
|
+
export type { LocalPlan, PlanSummary } from "./local-plan-store.js";
|
|
17
|
+
export { scrubPII, validateNoPII } from "./pii-scrubber.js";
|
|
18
|
+
export type { ScrubResult } from "./pii-scrubber.js";
|
|
19
|
+
export { CloudComputeClient } from "./cloud-compute-client.js";
|
|
20
|
+
export type { CloudComputeResult } from "./cloud-compute-client.js";
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local Data Layer — barrel export for all local-first modules.
|
|
3
|
+
*
|
|
4
|
+
* Provides a single import point for the local runtime:
|
|
5
|
+
*
|
|
6
|
+
* import { LocalPlanStore, CloudComputeClient, ... } from "./local/local-data-layer.js";
|
|
7
|
+
*
|
|
8
|
+
* @feature FEAT-0087
|
|
9
|
+
*/
|
|
10
|
+
export { loadLocalConfig, isLocalMode } from "./local-config.js";
|
|
11
|
+
export { EncryptionService } from "./encryption.js";
|
|
12
|
+
export { createKeychainProvider, FileKeychain, } from "./keychain.js";
|
|
13
|
+
export { LocalPlanStore } from "./local-plan-store.js";
|
|
14
|
+
export { scrubPII, validateNoPII } from "./pii-scrubber.js";
|
|
15
|
+
export { CloudComputeClient } from "./cloud-compute-client.js";
|