ccprofiles-core 0.2.0 → 0.3.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/dist/devices.js +1 -1
- package/dist/manifest.d.ts +34 -10
- package/dist/manifest.js +5 -3
- package/dist/secrets.d.ts +15 -0
- package/dist/secrets.js +60 -0
- package/package.json +1 -1
package/dist/devices.js
CHANGED
|
@@ -2,7 +2,7 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
import { atomicWrite } from './fsutil.js';
|
|
5
|
-
const IGNORE = ['secrets.enc', 'secret-names.json', 'devices.json', 'backups/', '*.ccp-tmp'];
|
|
5
|
+
const IGNORE = ['secrets.enc', 'secrets.dpapi.json', 'secret-names.json', 'devices.json', 'backups/', '*.ccp-tmp'];
|
|
6
6
|
export async function loadDevices(root) {
|
|
7
7
|
const f = join(root, 'devices.json');
|
|
8
8
|
if (!existsSync(f))
|
package/dist/manifest.d.ts
CHANGED
|
@@ -1,20 +1,32 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
export declare class ManifestError extends Error {
|
|
3
3
|
}
|
|
4
|
-
declare const McpServerSchema: z.ZodObject<{
|
|
5
|
-
command: z.ZodString
|
|
4
|
+
declare const McpServerSchema: z.ZodEffects<z.ZodObject<{
|
|
5
|
+
command: z.ZodOptional<z.ZodString>;
|
|
6
6
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
7
7
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
8
8
|
type: z.ZodOptional<z.ZodString>;
|
|
9
9
|
url: z.ZodOptional<z.ZodString>;
|
|
10
10
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
11
|
-
command: z.ZodString
|
|
11
|
+
command: z.ZodOptional<z.ZodString>;
|
|
12
12
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
13
13
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
14
14
|
type: z.ZodOptional<z.ZodString>;
|
|
15
15
|
url: z.ZodOptional<z.ZodString>;
|
|
16
16
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
17
|
-
command: z.ZodString
|
|
17
|
+
command: z.ZodOptional<z.ZodString>;
|
|
18
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
19
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
20
|
+
type: z.ZodOptional<z.ZodString>;
|
|
21
|
+
url: z.ZodOptional<z.ZodString>;
|
|
22
|
+
}, z.ZodTypeAny, "passthrough">>, z.objectOutputType<{
|
|
23
|
+
command: z.ZodOptional<z.ZodString>;
|
|
24
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
25
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
26
|
+
type: z.ZodOptional<z.ZodString>;
|
|
27
|
+
url: z.ZodOptional<z.ZodString>;
|
|
28
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
29
|
+
command: z.ZodOptional<z.ZodString>;
|
|
18
30
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
19
31
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
20
32
|
type: z.ZodOptional<z.ZodString>;
|
|
@@ -79,20 +91,32 @@ declare const ManifestSchema: z.ZodObject<{
|
|
|
79
91
|
mcp?: string[] | undefined;
|
|
80
92
|
settingsEnv?: Record<string, string> | undefined;
|
|
81
93
|
}>, "many">;
|
|
82
|
-
mcpServers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
83
|
-
command: z.ZodString
|
|
94
|
+
mcpServers: z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
|
|
95
|
+
command: z.ZodOptional<z.ZodString>;
|
|
84
96
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
85
97
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
86
98
|
type: z.ZodOptional<z.ZodString>;
|
|
87
99
|
url: z.ZodOptional<z.ZodString>;
|
|
88
100
|
}, "passthrough", z.ZodTypeAny, z.objectOutputType<{
|
|
89
|
-
command: z.ZodString
|
|
101
|
+
command: z.ZodOptional<z.ZodString>;
|
|
102
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
103
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
104
|
+
type: z.ZodOptional<z.ZodString>;
|
|
105
|
+
url: z.ZodOptional<z.ZodString>;
|
|
106
|
+
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
107
|
+
command: z.ZodOptional<z.ZodString>;
|
|
108
|
+
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
109
|
+
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
110
|
+
type: z.ZodOptional<z.ZodString>;
|
|
111
|
+
url: z.ZodOptional<z.ZodString>;
|
|
112
|
+
}, z.ZodTypeAny, "passthrough">>, z.objectOutputType<{
|
|
113
|
+
command: z.ZodOptional<z.ZodString>;
|
|
90
114
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
91
115
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
92
116
|
type: z.ZodOptional<z.ZodString>;
|
|
93
117
|
url: z.ZodOptional<z.ZodString>;
|
|
94
118
|
}, z.ZodTypeAny, "passthrough">, z.objectInputType<{
|
|
95
|
-
command: z.ZodString
|
|
119
|
+
command: z.ZodOptional<z.ZodString>;
|
|
96
120
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
97
121
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
98
122
|
type: z.ZodOptional<z.ZodString>;
|
|
@@ -112,7 +136,7 @@ declare const ManifestSchema: z.ZodObject<{
|
|
|
112
136
|
settingsEnv: Record<string, string>;
|
|
113
137
|
}[];
|
|
114
138
|
mcpServers: Record<string, z.objectOutputType<{
|
|
115
|
-
command: z.ZodString
|
|
139
|
+
command: z.ZodOptional<z.ZodString>;
|
|
116
140
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
117
141
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
118
142
|
type: z.ZodOptional<z.ZodString>;
|
|
@@ -132,7 +156,7 @@ declare const ManifestSchema: z.ZodObject<{
|
|
|
132
156
|
settingsEnv?: Record<string, string> | undefined;
|
|
133
157
|
}[];
|
|
134
158
|
mcpServers: Record<string, z.objectInputType<{
|
|
135
|
-
command: z.ZodString
|
|
159
|
+
command: z.ZodOptional<z.ZodString>;
|
|
136
160
|
args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
137
161
|
env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
|
|
138
162
|
type: z.ZodOptional<z.ZodString>;
|
package/dist/manifest.js
CHANGED
|
@@ -8,12 +8,12 @@ const exec = promisify(execFile);
|
|
|
8
8
|
export class ManifestError extends Error {
|
|
9
9
|
}
|
|
10
10
|
const McpServerSchema = z.object({
|
|
11
|
-
command: z.string(),
|
|
11
|
+
command: z.string().optional(),
|
|
12
12
|
args: z.array(z.string()).optional(),
|
|
13
13
|
env: z.record(z.string()).optional(),
|
|
14
14
|
type: z.string().optional(),
|
|
15
15
|
url: z.string().optional(),
|
|
16
|
-
}).passthrough();
|
|
16
|
+
}).passthrough().refine(s => s.command !== undefined || s.url !== undefined, { message: 'mcp server must have either "command" (local) or "url" (remote)' });
|
|
17
17
|
const ProfileSchema = z.object({
|
|
18
18
|
name: z.string().min(1),
|
|
19
19
|
dir: z.string().min(1),
|
|
@@ -94,8 +94,10 @@ export async function loadManifest(root) {
|
|
|
94
94
|
return parseManifest(await readFile(join(root, 'manifest.yaml'), 'utf8'));
|
|
95
95
|
}
|
|
96
96
|
export async function saveManifest(root, m) {
|
|
97
|
+
const yaml = serializeManifest(m);
|
|
98
|
+
parseManifest(yaml); // guard: never write a manifest that cannot be reloaded
|
|
97
99
|
await mkdir(root, { recursive: true });
|
|
98
|
-
await writeFile(join(root, 'manifest.yaml'),
|
|
100
|
+
await writeFile(join(root, 'manifest.yaml'), yaml, 'utf8');
|
|
99
101
|
try {
|
|
100
102
|
await exec('git', ['-C', root, 'rev-parse', '--git-dir']);
|
|
101
103
|
await exec('git', ['-C', root, 'add', '-A']);
|
package/dist/secrets.d.ts
CHANGED
|
@@ -35,6 +35,21 @@ export declare class SecretToolBackend implements SecretsBackend {
|
|
|
35
35
|
get(key: string): Promise<string | null>;
|
|
36
36
|
delete(key: string): Promise<void>;
|
|
37
37
|
}
|
|
38
|
+
export type DpapiCrypt = {
|
|
39
|
+
protect(plain: string): Promise<string>;
|
|
40
|
+
unprotect(b64: string): Promise<string>;
|
|
41
|
+
};
|
|
42
|
+
export declare const powershellDpapi: DpapiCrypt;
|
|
43
|
+
export declare class DpapiBackend implements SecretsBackend {
|
|
44
|
+
private filePath;
|
|
45
|
+
private crypt;
|
|
46
|
+
readonly name = "windows-dpapi";
|
|
47
|
+
constructor(filePath: string, crypt?: DpapiCrypt);
|
|
48
|
+
private load;
|
|
49
|
+
get(key: string): Promise<string | null>;
|
|
50
|
+
set(key: string, value: string): Promise<void>;
|
|
51
|
+
delete(key: string): Promise<void>;
|
|
52
|
+
}
|
|
38
53
|
export declare class SecretsStore {
|
|
39
54
|
private backend;
|
|
40
55
|
private indexPath;
|
package/dist/secrets.js
CHANGED
|
@@ -100,6 +100,64 @@ export class SecretToolBackend {
|
|
|
100
100
|
catch { /* absent */ }
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
+
// DPAPI via built-in PowerShell. Secret bytes travel through a spawn env var, never argv.
|
|
104
|
+
export const powershellDpapi = {
|
|
105
|
+
protect: (plain) => runDpapi(
|
|
106
|
+
// [string] cast coerces a $null CCP_IN (empty env var) to '' before GetBytes, which
|
|
107
|
+
// otherwise throws on $null. Only needed here — unprotect's CCP_IN is base64 ciphertext
|
|
108
|
+
// and is never empty for a stored value.
|
|
109
|
+
"$b=[Text.Encoding]::UTF8.GetBytes([string]$env:CCP_IN);" +
|
|
110
|
+
"[Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Protect($b,$null,'CurrentUser'))", plain),
|
|
111
|
+
unprotect: async (b64) => {
|
|
112
|
+
// PS emits base64 of the *decrypted bytes* (not the decoded string) so runDpapi's
|
|
113
|
+
// trim is always ASCII-safe and we never depend on the PowerShell console's
|
|
114
|
+
// stdout encoding to round-trip arbitrary plaintext (e.g. non-ASCII, trailing whitespace).
|
|
115
|
+
const out = await runDpapi("$b=[Convert]::FromBase64String($env:CCP_IN);" +
|
|
116
|
+
"[Convert]::ToBase64String([Security.Cryptography.ProtectedData]::Unprotect($b,$null,'CurrentUser'))", b64);
|
|
117
|
+
return Buffer.from(out, 'base64').toString('utf8');
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
async function runDpapi(script, input) {
|
|
121
|
+
const child = await import('node:child_process');
|
|
122
|
+
return new Promise((resolve, reject) => {
|
|
123
|
+
const proc = child.spawn('powershell', ['-NoProfile', '-NonInteractive', '-Command', 'Add-Type -AssemblyName System.Security;' + script], { env: { ...process.env, CCP_IN: input }, stdio: ['ignore', 'pipe', 'pipe'] });
|
|
124
|
+
let out = '';
|
|
125
|
+
let err = '';
|
|
126
|
+
proc.stdout.on('data', d => { out += d; });
|
|
127
|
+
proc.stderr.on('data', d => { err += d; });
|
|
128
|
+
proc.on('error', reject);
|
|
129
|
+
proc.on('close', code => (code === 0 ? resolve(out.trim()) : reject(new Error(`powershell dpapi exited ${code}: ${err.trim()}`))));
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
export class DpapiBackend {
|
|
133
|
+
filePath;
|
|
134
|
+
crypt;
|
|
135
|
+
name = 'windows-dpapi';
|
|
136
|
+
constructor(filePath, crypt = powershellDpapi) {
|
|
137
|
+
this.filePath = filePath;
|
|
138
|
+
this.crypt = crypt;
|
|
139
|
+
}
|
|
140
|
+
async load() {
|
|
141
|
+
if (!existsSync(this.filePath))
|
|
142
|
+
return { entries: {} };
|
|
143
|
+
return JSON.parse(await readFile(this.filePath, 'utf8'));
|
|
144
|
+
}
|
|
145
|
+
async get(key) {
|
|
146
|
+
const f = await this.load();
|
|
147
|
+
const ct = f.entries[key];
|
|
148
|
+
return ct === undefined ? null : this.crypt.unprotect(ct);
|
|
149
|
+
}
|
|
150
|
+
async set(key, value) {
|
|
151
|
+
const f = await this.load();
|
|
152
|
+
f.entries[key] = await this.crypt.protect(value);
|
|
153
|
+
await atomicWrite(this.filePath, JSON.stringify(f));
|
|
154
|
+
}
|
|
155
|
+
async delete(key) {
|
|
156
|
+
const f = await this.load();
|
|
157
|
+
delete f.entries[key];
|
|
158
|
+
await atomicWrite(this.filePath, JSON.stringify(f));
|
|
159
|
+
}
|
|
160
|
+
}
|
|
103
161
|
export class SecretsStore {
|
|
104
162
|
backend;
|
|
105
163
|
indexPath;
|
|
@@ -137,6 +195,8 @@ export async function defaultBackend(p, opts) {
|
|
|
137
195
|
}
|
|
138
196
|
catch { /* fall through */ }
|
|
139
197
|
}
|
|
198
|
+
if (p.os === 'win32')
|
|
199
|
+
return new DpapiBackend(opts.filePath.replace(/\.enc$/, '.dpapi.json'));
|
|
140
200
|
const pw = opts.passphrase ? await opts.passphrase() : '';
|
|
141
201
|
if (!pw)
|
|
142
202
|
throw new Error('encrypted-file backend requires a passphrase');
|
package/package.json
CHANGED