ccprofiles-core 0.1.1 → 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/adopt.d.ts CHANGED
@@ -2,3 +2,11 @@ import type { LiveProfile } from './discovery.js';
2
2
  import type { Manifest } from './manifest.js';
3
3
  import { type Platform } from './platform.js';
4
4
  export declare function buildManifest(live: LiveProfile[], platform: Platform): Manifest;
5
+ /**
6
+ * Re-migrate secret refs after a snapshot/adopt rebuilds the manifest from live state.
7
+ * Live settings.json holds resolved plaintext values, so a freshly built manifest would
8
+ * re-leak a previously migrated token as plaintext. For each profile that existed before,
9
+ * if the old manifest had a secret:// ref whose resolved value matches the newly-discovered
10
+ * plaintext value, restore the ref instead of the plaintext.
11
+ */
12
+ export declare function preserveSecretRefs(newM: Manifest, oldM: Manifest, getSecret: (name: string) => Promise<string | null>): Promise<void>;
package/dist/adopt.js CHANGED
@@ -32,7 +32,29 @@ export function buildManifest(live, platform) {
32
32
  env: {},
33
33
  links,
34
34
  mcp: Object.keys(lp.mcpServers).sort(),
35
+ settingsEnv: lp.settingsEnv,
35
36
  };
36
37
  });
37
38
  return { version: 1, hub, profiles, mcpServers };
38
39
  }
40
+ /**
41
+ * Re-migrate secret refs after a snapshot/adopt rebuilds the manifest from live state.
42
+ * Live settings.json holds resolved plaintext values, so a freshly built manifest would
43
+ * re-leak a previously migrated token as plaintext. For each profile that existed before,
44
+ * if the old manifest had a secret:// ref whose resolved value matches the newly-discovered
45
+ * plaintext value, restore the ref instead of the plaintext.
46
+ */
47
+ export async function preserveSecretRefs(newM, oldM, getSecret) {
48
+ for (const pr of newM.profiles) {
49
+ const old = oldM.profiles.find(p => p.name === pr.name);
50
+ if (!old)
51
+ continue;
52
+ for (const [k, v] of Object.entries(old.settingsEnv ?? {})) {
53
+ if (!v.startsWith('secret://'))
54
+ continue;
55
+ const resolved = await getSecret(v.slice('secret://'.length));
56
+ if (resolved !== null && pr.settingsEnv[k] === resolved)
57
+ pr.settingsEnv[k] = v;
58
+ }
59
+ }
60
+ }
package/dist/apply.d.ts CHANGED
@@ -16,8 +16,14 @@ export type ApplyAction = {
16
16
  kind: 'rc-block';
17
17
  rcFile: string;
18
18
  block: string;
19
+ } | {
20
+ kind: 'set-settings-env';
21
+ settingsPath: string;
22
+ env: Record<string, string>;
19
23
  };
20
- export declare function planApply(m: Manifest, live: LiveProfile[], p: Platform): ApplyAction[];
24
+ /** Resolve secret:// refs in every profile's settingsEnv. Throws on a missing secret. */
25
+ export declare function resolveSettingsEnv(m: Manifest, getSecret: (name: string) => Promise<string | null>): Promise<Record<string, Record<string, string>>>;
26
+ export declare function planApply(m: Manifest, live: LiveProfile[], p: Platform, resolvedSettingsEnv?: Record<string, Record<string, string>>): ApplyAction[];
21
27
  export declare function executeApply(actions: ApplyAction[], opts: {
22
28
  backupRoot: string;
23
29
  stamp: string;
package/dist/apply.js CHANGED
@@ -10,7 +10,28 @@ function configPathFor(dir, home) {
10
10
  function sortKeys(o) {
11
11
  return Object.fromEntries(Object.entries(o).sort(([a], [b]) => a.localeCompare(b)));
12
12
  }
13
- export function planApply(m, live, p) {
13
+ const SECRET_PREFIX = 'secret://';
14
+ /** Resolve secret:// refs in every profile's settingsEnv. Throws on a missing secret. */
15
+ export async function resolveSettingsEnv(m, getSecret) {
16
+ const out = {};
17
+ for (const pr of m.profiles) {
18
+ const env = {};
19
+ for (const [k, v] of Object.entries(pr.settingsEnv ?? {})) {
20
+ if (v.startsWith(SECRET_PREFIX)) {
21
+ const ref = v.slice(SECRET_PREFIX.length);
22
+ const val = await getSecret(ref);
23
+ if (val === null)
24
+ throw new Error(`profile "${pr.name}": secret not found: ${ref} (for ${k})`);
25
+ env[k] = val;
26
+ }
27
+ else
28
+ env[k] = v;
29
+ }
30
+ out[pr.name] = env;
31
+ }
32
+ return out;
33
+ }
34
+ export function planApply(m, live, p, resolvedSettingsEnv) {
14
35
  const actions = [];
15
36
  const hubProfile = m.profiles.find(x => x.name === m.hub) ?? null;
16
37
  for (const pr of m.profiles) {
@@ -34,6 +55,19 @@ export function planApply(m, live, p) {
34
55
  continue;
35
56
  actions.push({ kind: 'link', from, to });
36
57
  }
58
+ const senv = pr.settingsEnv ?? {}; // literals in older tests may omit the field
59
+ if (Object.keys(senv).length > 0) {
60
+ let desired = resolvedSettingsEnv?.[pr.name];
61
+ if (!desired) {
62
+ if (Object.values(senv).some(v => v.startsWith(SECRET_PREFIX)))
63
+ throw new Error(`profile "${pr.name}": settingsEnv has secret refs — pass resolved settings env to planApply`);
64
+ desired = senv;
65
+ }
66
+ const currentEnv = lp?.settingsEnv ?? null;
67
+ if (!currentEnv || JSON.stringify(sortKeys(currentEnv)) !== JSON.stringify(sortKeys(desired))) {
68
+ actions.push({ kind: 'set-settings-env', settingsPath: join(dir, 'settings.json'), env: desired });
69
+ }
70
+ }
37
71
  }
38
72
  const block = renderRcBlock(m, p);
39
73
  let rcCurrent = '';
@@ -49,7 +83,10 @@ export async function executeApply(actions, opts) {
49
83
  const performed = actions.map(describe);
50
84
  if (opts.dryRun)
51
85
  return { backupDir: null, performed };
52
- const touched = actions.flatMap(a => a.kind === 'set-mcp-servers' ? [a.configPath] : a.kind === 'rc-block' ? [a.rcFile] : []);
86
+ const touched = actions.flatMap(a => a.kind === 'set-mcp-servers' ? [a.configPath]
87
+ : a.kind === 'rc-block' ? [a.rcFile]
88
+ : a.kind === 'set-settings-env' ? [a.settingsPath]
89
+ : []);
53
90
  const backupDir = touched.length ? await backupFiles(touched, opts.backupRoot, opts.stamp) : null;
54
91
  for (const a of actions) {
55
92
  if (a.kind === 'create-profile-dir') {
@@ -89,6 +126,16 @@ export async function executeApply(actions, opts) {
89
126
  await mkdir(dirname(a.rcFile), { recursive: true });
90
127
  await atomicWrite(a.rcFile, upsertManagedBlock(rc, a.block));
91
128
  }
129
+ else if (a.kind === 'set-settings-env') {
130
+ let cfg = {};
131
+ try {
132
+ cfg = JSON.parse(await readFile(a.settingsPath, 'utf8'));
133
+ }
134
+ catch { /* new file */ }
135
+ cfg.env = a.env;
136
+ await mkdir(dirname(a.settingsPath), { recursive: true });
137
+ await atomicWrite(a.settingsPath, JSON.stringify(cfg, null, 2));
138
+ }
92
139
  }
93
140
  return { backupDir, performed };
94
141
  }
@@ -98,5 +145,6 @@ function describe(a) {
98
145
  case 'create-profile-dir': return `create ${a.dir}`;
99
146
  case 'link': return `link ${a.from} -> ${a.to}`;
100
147
  case 'rc-block': return `update managed block in ${a.rcFile}`;
148
+ case 'set-settings-env': return `set settings env (${Object.keys(a.env).length}) in ${a.settingsPath}`;
101
149
  }
102
150
  }
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))
@@ -6,5 +6,6 @@ export interface LiveProfile {
6
6
  account: string | null;
7
7
  mcpServers: Record<string, McpServerDef>;
8
8
  links: Record<string, string>;
9
+ settingsEnv: Record<string, string>;
9
10
  }
10
11
  export declare function discoverProfiles(home: string): Promise<LiveProfile[]>;
package/dist/discovery.js CHANGED
@@ -26,6 +26,15 @@ export async function discoverProfiles(home) {
26
26
  catch { /* skip */ }
27
27
  }
28
28
  }
29
+ const settingsEnv = {};
30
+ try {
31
+ const s = JSON.parse(await readFile(join(dir, 'settings.json'), 'utf8'));
32
+ if (s && typeof s.env === 'object' && s.env !== null)
33
+ for (const [k, v] of Object.entries(s.env))
34
+ if (typeof v === 'string')
35
+ settingsEnv[k] = v;
36
+ }
37
+ catch { /* no settings.json */ }
29
38
  out.push({
30
39
  dirName: e.name,
31
40
  dir,
@@ -33,6 +42,7 @@ export async function discoverProfiles(home) {
33
42
  account: cfg?.oauthAccount?.emailAddress ?? null,
34
43
  mcpServers: cfg?.mcpServers ?? {},
35
44
  links,
45
+ settingsEnv,
36
46
  });
37
47
  }
38
48
  return out.sort((a, b) => a.dirName.localeCompare(b.dirName));
@@ -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>;
@@ -28,6 +40,7 @@ declare const ProfileSchema: z.ZodObject<{
28
40
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
29
41
  links: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
30
42
  mcp: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
43
+ settingsEnv: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
31
44
  }, "strip", z.ZodTypeAny, {
32
45
  env: Record<string, string>;
33
46
  name: string;
@@ -36,6 +49,7 @@ declare const ProfileSchema: z.ZodObject<{
36
49
  auth: "env" | "oauth" | "api-key";
37
50
  links: Record<string, string>;
38
51
  mcp: string[];
52
+ settingsEnv: Record<string, string>;
39
53
  }, {
40
54
  name: string;
41
55
  dir: string;
@@ -44,6 +58,7 @@ declare const ProfileSchema: z.ZodObject<{
44
58
  env?: Record<string, string> | undefined;
45
59
  links?: Record<string, string> | undefined;
46
60
  mcp?: string[] | undefined;
61
+ settingsEnv?: Record<string, string> | undefined;
47
62
  }>;
48
63
  declare const ManifestSchema: z.ZodObject<{
49
64
  version: z.ZodLiteral<1>;
@@ -56,6 +71,7 @@ declare const ManifestSchema: z.ZodObject<{
56
71
  env: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
57
72
  links: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
58
73
  mcp: z.ZodDefault<z.ZodArray<z.ZodString, "many">>;
74
+ settingsEnv: z.ZodDefault<z.ZodRecord<z.ZodString, z.ZodString>>;
59
75
  }, "strip", z.ZodTypeAny, {
60
76
  env: Record<string, string>;
61
77
  name: string;
@@ -64,6 +80,7 @@ declare const ManifestSchema: z.ZodObject<{
64
80
  auth: "env" | "oauth" | "api-key";
65
81
  links: Record<string, string>;
66
82
  mcp: string[];
83
+ settingsEnv: Record<string, string>;
67
84
  }, {
68
85
  name: string;
69
86
  dir: string;
@@ -72,21 +89,34 @@ declare const ManifestSchema: z.ZodObject<{
72
89
  env?: Record<string, string> | undefined;
73
90
  links?: Record<string, string> | undefined;
74
91
  mcp?: string[] | undefined;
92
+ settingsEnv?: Record<string, string> | undefined;
75
93
  }>, "many">;
76
- mcpServers: z.ZodRecord<z.ZodString, z.ZodObject<{
77
- command: z.ZodString;
94
+ mcpServers: z.ZodRecord<z.ZodString, z.ZodEffects<z.ZodObject<{
95
+ command: z.ZodOptional<z.ZodString>;
78
96
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
79
97
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
80
98
  type: z.ZodOptional<z.ZodString>;
81
99
  url: z.ZodOptional<z.ZodString>;
82
100
  }, "passthrough", z.ZodTypeAny, z.objectOutputType<{
83
- 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>;
84
114
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
85
115
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
86
116
  type: z.ZodOptional<z.ZodString>;
87
117
  url: z.ZodOptional<z.ZodString>;
88
118
  }, z.ZodTypeAny, "passthrough">, z.objectInputType<{
89
- command: z.ZodString;
119
+ command: z.ZodOptional<z.ZodString>;
90
120
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
91
121
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
92
122
  type: z.ZodOptional<z.ZodString>;
@@ -103,9 +133,10 @@ declare const ManifestSchema: z.ZodObject<{
103
133
  auth: "env" | "oauth" | "api-key";
104
134
  links: Record<string, string>;
105
135
  mcp: string[];
136
+ settingsEnv: Record<string, string>;
106
137
  }[];
107
138
  mcpServers: Record<string, z.objectOutputType<{
108
- command: z.ZodString;
139
+ command: z.ZodOptional<z.ZodString>;
109
140
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
110
141
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
111
142
  type: z.ZodOptional<z.ZodString>;
@@ -122,9 +153,10 @@ declare const ManifestSchema: z.ZodObject<{
122
153
  env?: Record<string, string> | undefined;
123
154
  links?: Record<string, string> | undefined;
124
155
  mcp?: string[] | undefined;
156
+ settingsEnv?: Record<string, string> | undefined;
125
157
  }[];
126
158
  mcpServers: Record<string, z.objectInputType<{
127
- command: z.ZodString;
159
+ command: z.ZodOptional<z.ZodString>;
128
160
  args: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
129
161
  env: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodString>>;
130
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),
@@ -22,6 +22,7 @@ const ProfileSchema = z.object({
22
22
  env: z.record(z.string()).default({}),
23
23
  links: z.record(z.string()).default({}),
24
24
  mcp: z.array(z.string()).default([]),
25
+ settingsEnv: z.record(z.string()).default({}),
25
26
  });
26
27
  const ManifestSchema = z.object({
27
28
  version: z.literal(1),
@@ -56,6 +57,15 @@ export function assertSafeManifest(m) {
56
57
  throw new ManifestError(`unsafe secret reference in profile "${p.name}": ${JSON.stringify(v)}`);
57
58
  }
58
59
  }
60
+ for (const [k, v] of Object.entries(p.settingsEnv)) {
61
+ if (!SAFE_ENV_KEY.test(k))
62
+ throw new ManifestError(`unsafe settings env var name in profile "${p.name}": ${JSON.stringify(k)}`);
63
+ if (v.startsWith(SECRET_PREFIX)) {
64
+ const ref = v.slice(SECRET_PREFIX.length);
65
+ if (!SAFE_NAME.test(ref))
66
+ throw new ManifestError(`unsafe secret reference in profile "${p.name}": ${JSON.stringify(v)}`);
67
+ }
68
+ }
59
69
  }
60
70
  }
61
71
  export function parseManifest(text) {
@@ -84,8 +94,10 @@ export async function loadManifest(root) {
84
94
  return parseManifest(await readFile(join(root, 'manifest.yaml'), 'utf8'));
85
95
  }
86
96
  export async function saveManifest(root, m) {
97
+ const yaml = serializeManifest(m);
98
+ parseManifest(yaml); // guard: never write a manifest that cannot be reloaded
87
99
  await mkdir(root, { recursive: true });
88
- await writeFile(join(root, 'manifest.yaml'), serializeManifest(m), 'utf8');
100
+ await writeFile(join(root, 'manifest.yaml'), yaml, 'utf8');
89
101
  try {
90
102
  await exec('git', ['-C', root, 'rev-parse', '--git-dir']);
91
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
@@ -1,16 +1,33 @@
1
1
  {
2
2
  "name": "ccprofiles-core",
3
- "version": "0.1.1",
3
+ "version": "0.3.0",
4
4
  "description": "Core library for ccprofiles — discovery, manifest, secrets, apply, and LAN sync for Claude Code multi-account setups",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
8
- "files": ["dist"],
8
+ "files": [
9
+ "dist"
10
+ ],
9
11
  "license": "MIT",
10
- "repository": { "type": "git", "url": "git+https://github.com/tmfadhlul/claude-code-profile.git", "directory": "packages/core" },
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "git+https://github.com/tmfadhlul/claude-code-profile.git",
15
+ "directory": "packages/core"
16
+ },
11
17
  "homepage": "https://github.com/tmfadhlul/claude-code-profile#readme",
12
18
  "bugs": "https://github.com/tmfadhlul/claude-code-profile/issues",
13
- "keywords": ["claude-code", "claude", "profiles", "mcp", "config-management"],
14
- "engines": { "node": ">=20" },
15
- "dependencies": { "yaml": "^2.5.0", "zod": "^3.23.0" }
19
+ "keywords": [
20
+ "claude-code",
21
+ "claude",
22
+ "profiles",
23
+ "mcp",
24
+ "config-management"
25
+ ],
26
+ "engines": {
27
+ "node": ">=20"
28
+ },
29
+ "dependencies": {
30
+ "yaml": "^2.5.0",
31
+ "zod": "^3.23.0"
32
+ }
16
33
  }