@openpalm/lib 0.10.1 → 0.11.0-beta.1
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 +2 -2
- package/package.json +7 -3
- package/src/control-plane/admin-token.ts +73 -0
- package/src/control-plane/akm-vault.test.ts +108 -0
- package/src/control-plane/akm-vault.ts +307 -0
- package/src/control-plane/audit.ts +3 -2
- package/src/control-plane/channels.ts +3 -3
- package/src/control-plane/cleanup-guardrails.test.ts +8 -9
- package/src/control-plane/compose-args.test.ts +25 -21
- package/src/control-plane/config-persistence.ts +103 -64
- package/src/control-plane/core-assets.test.ts +104 -0
- package/src/control-plane/core-assets.ts +54 -57
- package/src/control-plane/docker.ts +55 -21
- package/src/control-plane/env.test.ts +25 -1
- package/src/control-plane/env.ts +80 -0
- package/src/control-plane/home.ts +66 -69
- package/src/control-plane/host-opencode.test.ts +263 -0
- package/src/control-plane/host-opencode.ts +229 -0
- package/src/control-plane/install-edge-cases.test.ts +182 -244
- package/src/control-plane/install-lock.ts +157 -0
- package/src/control-plane/lifecycle.ts +57 -56
- package/src/control-plane/markdown-task.ts +200 -0
- package/src/control-plane/paths.ts +75 -0
- package/src/control-plane/provider-config.ts +2 -2
- package/src/control-plane/provider-models.ts +154 -0
- package/src/control-plane/registry-components.test.ts +102 -25
- package/src/control-plane/registry.test.ts +49 -47
- package/src/control-plane/registry.ts +71 -50
- package/src/control-plane/rollback.ts +17 -16
- package/src/control-plane/scheduler.ts +75 -262
- package/src/control-plane/secret-backend.test.ts +98 -108
- package/src/control-plane/secret-backend.ts +221 -181
- package/src/control-plane/secret-mappings.ts +3 -6
- package/src/control-plane/secrets.ts +83 -47
- package/src/control-plane/setup-config.schema.json +2 -14
- package/src/control-plane/setup-status.ts +4 -29
- package/src/control-plane/setup-validation.ts +21 -21
- package/src/control-plane/setup.test.ts +122 -227
- package/src/control-plane/setup.ts +224 -125
- package/src/control-plane/skeleton-guardrail.test.ts +151 -0
- package/src/control-plane/spec-to-env.test.ts +59 -58
- package/src/control-plane/spec-to-env.ts +39 -140
- package/src/control-plane/spec-validator.ts +2 -99
- package/src/control-plane/stack-spec.test.ts +21 -77
- package/src/control-plane/stack-spec.ts +7 -83
- package/src/control-plane/types.ts +17 -15
- package/src/control-plane/ui-assets.ts +349 -0
- package/src/control-plane/validate.ts +44 -79
- package/src/index.ts +77 -44
- package/src/logger.test.ts +228 -0
- package/src/logger.ts +71 -1
- package/src/provider-constants.ts +22 -1
- package/src/control-plane/env-schema-validation.test.ts +0 -118
- package/src/control-plane/memory-config.ts +0 -298
- package/src/control-plane/redact-schema.ts +0 -50
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { randomBytes } from 'node:crypto';
|
|
2
|
-
import { existsSync,
|
|
2
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
3
3
|
import { execFile as execFileCb, spawn } from 'node:child_process';
|
|
4
4
|
import { promisify } from 'node:util';
|
|
5
5
|
import { join, normalize, resolve } from 'node:path';
|
|
@@ -21,6 +21,7 @@ import {
|
|
|
21
21
|
updateSecretsEnv,
|
|
22
22
|
updateSystemSecretsEnv,
|
|
23
23
|
} from './secrets.js';
|
|
24
|
+
import { readUserVaultSync } from './akm-vault.js';
|
|
24
25
|
|
|
25
26
|
const execFile = promisify(execFileCb);
|
|
26
27
|
|
|
@@ -50,21 +51,23 @@ type ResolvedSecretTarget = {
|
|
|
50
51
|
envKey?: string;
|
|
51
52
|
};
|
|
52
53
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
54
|
+
/**
|
|
55
|
+
* Public shape returned by `detectSecretBackend`. Both the plaintext and
|
|
56
|
+
* pass implementations expose the same surface — a small set of async
|
|
57
|
+
* methods plus a `provider` tag and a flat `capabilities` object.
|
|
58
|
+
*
|
|
59
|
+
* Kept as a `type` alias of an inline object literal so consumers don't
|
|
60
|
+
* have to import a separate interface or capabilities type.
|
|
61
|
+
*/
|
|
62
|
+
export type SecretBackend = {
|
|
60
63
|
readonly provider: 'plaintext' | 'pass';
|
|
61
|
-
readonly capabilities:
|
|
64
|
+
readonly capabilities: { generate: boolean; remove: boolean; rename: boolean };
|
|
62
65
|
list(prefix?: string): Promise<SecretEntryMetadata[]>;
|
|
63
66
|
write(key: string, value: string): Promise<SecretEntryMetadata>;
|
|
64
67
|
generate(key: string, length?: number): Promise<SecretEntryMetadata>;
|
|
65
68
|
remove(key: string): Promise<void>;
|
|
66
69
|
exists(key: string): Promise<boolean>;
|
|
67
|
-
}
|
|
70
|
+
};
|
|
68
71
|
|
|
69
72
|
function generateSecretValue(length = 32): string {
|
|
70
73
|
// Hex encoding produces two output characters per byte. Clamp to at least
|
|
@@ -73,7 +76,7 @@ function generateSecretValue(length = 32): string {
|
|
|
73
76
|
}
|
|
74
77
|
|
|
75
78
|
function resolvePlaintextTarget(state: ControlPlaneState, key: string): ResolvedSecretTarget {
|
|
76
|
-
const systemEnv = readStackEnv(state.
|
|
79
|
+
const systemEnv = readStackEnv(state.stackDir);
|
|
77
80
|
const coreMapping = findCoreSecretByKey(key, systemEnv);
|
|
78
81
|
if (coreMapping) {
|
|
79
82
|
return { key, scope: coreMapping.scope, envKey: coreMapping.envKey };
|
|
@@ -85,101 +88,118 @@ function resolvePlaintextTarget(state: ControlPlaneState, key: string): Resolved
|
|
|
85
88
|
|
|
86
89
|
function currentValueForTarget(state: ControlPlaneState, target: ResolvedSecretTarget): string {
|
|
87
90
|
if (!target.envKey) return '';
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
if (target.scope === 'system') {
|
|
92
|
+
return readStackEnv(state.stackDir)[target.envKey] ?? '';
|
|
93
|
+
}
|
|
94
|
+
// User scope: the akm `vault:user` store is the canonical user-managed env
|
|
95
|
+
// namespace post-#421. Fall back to stack.env for legacy/consolidated
|
|
96
|
+
// secrets so older layouts keep resolving.
|
|
97
|
+
const userEnv = readUserVaultSync(state);
|
|
98
|
+
if (target.envKey in userEnv) return userEnv[target.envKey];
|
|
99
|
+
return readStackEnv(state.stackDir)[target.envKey] ?? '';
|
|
92
100
|
}
|
|
93
101
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
}
|
|
102
|
+
// ── Plaintext backend ─────────────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
export async function plaintextList(state: ControlPlaneState, prefix = 'openpalm/'): Promise<SecretEntryMetadata[]> {
|
|
105
|
+
const systemEnv = readStackEnv(state.stackDir);
|
|
106
|
+
const userEnvFile = readUserVaultSync(state);
|
|
107
|
+
// Legacy/consolidated secrets may live in stack.env even for user scope.
|
|
108
|
+
// Layer the user vault on top so explicit user-managed values win.
|
|
109
|
+
const userEnv: Record<string, string> = { ...systemEnv, ...userEnvFile };
|
|
110
|
+
const index = readPlaintextSecretIndex(state);
|
|
111
|
+
const entries: SecretEntryMetadata[] = [];
|
|
112
|
+
|
|
113
|
+
for (const mapping of getCoreSecretMappings(systemEnv)) {
|
|
114
|
+
if (!mapping.secretKey.startsWith(prefix)) continue;
|
|
115
|
+
const env = mapping.scope === 'system' ? systemEnv : userEnv;
|
|
116
|
+
entries.push({
|
|
117
|
+
key: mapping.secretKey,
|
|
118
|
+
scope: mapping.scope,
|
|
119
|
+
kind: 'core',
|
|
120
|
+
provider: 'plaintext',
|
|
121
|
+
present: Boolean(env[mapping.envKey]),
|
|
122
|
+
envKey: mapping.envKey,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
118
125
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
126
|
+
for (const [key, entry] of Object.entries(index.entries)) {
|
|
127
|
+
if (!key.startsWith(prefix)) continue;
|
|
128
|
+
const env = entry.scope === 'system' ? systemEnv : userEnv;
|
|
129
|
+
entries.push({
|
|
130
|
+
key,
|
|
131
|
+
scope: entry.scope,
|
|
132
|
+
kind: entry.kind,
|
|
133
|
+
provider: 'plaintext',
|
|
134
|
+
present: Boolean(env[entry.envKey]),
|
|
135
|
+
envKey: entry.envKey,
|
|
136
|
+
updatedAt: entry.updatedAt,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
140
|
+
entries.sort((a, b) => a.key.localeCompare(b.key));
|
|
141
|
+
return entries;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export async function plaintextWrite(state: ControlPlaneState, key: string, value: string): Promise<SecretEntryMetadata> {
|
|
145
|
+
const target = resolvePlaintextTarget(state, key);
|
|
146
|
+
if (!target.envKey) {
|
|
147
|
+
throw new Error(`Unable to resolve env key for secret ${key}`);
|
|
135
148
|
}
|
|
136
149
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
150
|
+
if (target.scope === 'system') {
|
|
151
|
+
updateSystemSecretsEnv(state, { [target.envKey]: value });
|
|
152
|
+
} else {
|
|
153
|
+
updateSecretsEnv(state, { [target.envKey]: value });
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return {
|
|
157
|
+
key,
|
|
158
|
+
scope: target.scope,
|
|
159
|
+
kind: key.startsWith('openpalm/component/') ? 'component' : key.startsWith('openpalm/custom/') ? 'custom' : 'core',
|
|
160
|
+
provider: 'plaintext',
|
|
161
|
+
present: true,
|
|
162
|
+
envKey: target.envKey,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
export async function plaintextGenerate(state: ControlPlaneState, key: string, length = 32): Promise<SecretEntryMetadata> {
|
|
167
|
+
return plaintextWrite(state, key, generateSecretValue(length));
|
|
168
|
+
}
|
|
142
169
|
|
|
170
|
+
export async function plaintextRemove(state: ControlPlaneState, key: string): Promise<void> {
|
|
171
|
+
const target = resolvePlaintextTarget(state, key);
|
|
172
|
+
if (target.envKey) {
|
|
143
173
|
if (target.scope === 'system') {
|
|
144
|
-
updateSystemSecretsEnv(
|
|
174
|
+
updateSystemSecretsEnv(state, { [target.envKey]: '' });
|
|
145
175
|
} else {
|
|
146
|
-
updateSecretsEnv(
|
|
176
|
+
updateSecretsEnv(state, { [target.envKey]: '' });
|
|
147
177
|
}
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
key,
|
|
151
|
-
scope: target.scope,
|
|
152
|
-
kind: key.startsWith('openpalm/component/') ? 'component' : key.startsWith('openpalm/custom/') ? 'custom' : 'core',
|
|
153
|
-
provider: this.provider,
|
|
154
|
-
present: true,
|
|
155
|
-
envKey: target.envKey,
|
|
156
|
-
};
|
|
157
178
|
}
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
return this.write(key, generateSecretValue(length));
|
|
179
|
+
if (!findCoreSecretByKey(key, readStackEnv(state.stackDir))) {
|
|
180
|
+
removePlaintextSecretEntry(state, key);
|
|
161
181
|
}
|
|
182
|
+
}
|
|
162
183
|
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
updateSystemSecretsEnv(this.state, { [target.envKey]: '' });
|
|
168
|
-
} else {
|
|
169
|
-
updateSecretsEnv(this.state, { [target.envKey]: '' });
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
if (!findCoreSecretByKey(key, readStackEnv(this.state.vaultDir))) {
|
|
173
|
-
removePlaintextSecretEntry(this.state, key);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
184
|
+
export async function plaintextExists(state: ControlPlaneState, key: string): Promise<boolean> {
|
|
185
|
+
const target = resolvePlaintextTarget(state, key);
|
|
186
|
+
return currentValueForTarget(state, target).length > 0;
|
|
187
|
+
}
|
|
176
188
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
189
|
+
function makePlaintextBackend(state: ControlPlaneState): SecretBackend {
|
|
190
|
+
return {
|
|
191
|
+
provider: 'plaintext',
|
|
192
|
+
capabilities: { generate: true, remove: true, rename: false },
|
|
193
|
+
list: (prefix) => plaintextList(state, prefix),
|
|
194
|
+
write: (key, value) => plaintextWrite(state, key, value),
|
|
195
|
+
generate: (key, length) => plaintextGenerate(state, key, length),
|
|
196
|
+
remove: (key) => plaintextRemove(state, key),
|
|
197
|
+
exists: (key) => plaintextExists(state, key),
|
|
198
|
+
};
|
|
181
199
|
}
|
|
182
200
|
|
|
201
|
+
// ── Pass backend ──────────────────────────────────────────────────────────
|
|
202
|
+
|
|
183
203
|
export function validatePassEntryName(entry: string): string {
|
|
184
204
|
const trimmed = entry.trim().replace(/^\/+|\/+$/g, '');
|
|
185
205
|
if (!trimmed) {
|
|
@@ -211,112 +231,132 @@ function walkPassStore(dir: string, prefix = ''): string[] {
|
|
|
211
231
|
return entries;
|
|
212
232
|
}
|
|
213
233
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Resolved pass-backend configuration. Kept as a small in-file struct so the
|
|
236
|
+
* five `pass*` helpers below share one place where defaults
|
|
237
|
+
* (`${dataDir}/secrets/pass-store`, empty prefix) are applied — inlining the
|
|
238
|
+
* `?? defaults` logic into each helper would multiply the truth source.
|
|
239
|
+
* The two strings travel together everywhere (passEnv, prefixedEntry,
|
|
240
|
+
* passKeyPath) so they're worth bundling.
|
|
241
|
+
*/
|
|
242
|
+
type PassContext = {
|
|
243
|
+
passwordStoreDir: string;
|
|
244
|
+
passPrefix: string;
|
|
245
|
+
};
|
|
225
246
|
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
247
|
+
function passContext(state: ControlPlaneState): PassContext {
|
|
248
|
+
const config = readSecretProviderConfig(state);
|
|
249
|
+
return {
|
|
250
|
+
passwordStoreDir: config?.passwordStoreDir ?? `${state.stateDir}/secrets/pass-store`,
|
|
251
|
+
passPrefix: config?.passPrefix ?? '',
|
|
252
|
+
};
|
|
253
|
+
}
|
|
232
254
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
}
|
|
255
|
+
function passEnv(ctx: PassContext): NodeJS.ProcessEnv {
|
|
256
|
+
return {
|
|
257
|
+
...process.env,
|
|
258
|
+
PASSWORD_STORE_DIR: ctx.passwordStoreDir,
|
|
259
|
+
};
|
|
260
|
+
}
|
|
238
261
|
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
if (!resolvedPath.startsWith(`${resolvedStore}/`)) {
|
|
245
|
-
throw new Error('Secret key resolves outside the password store');
|
|
246
|
-
}
|
|
247
|
-
return resolvedPath;
|
|
248
|
-
}
|
|
262
|
+
/** Prepend passPrefix to a canonical key for pass store operations. */
|
|
263
|
+
function prefixedEntry(ctx: PassContext, canonicalKey: string): string {
|
|
264
|
+
const entry = validatePassEntryName(canonicalKey);
|
|
265
|
+
return ctx.passPrefix ? `${ctx.passPrefix}/${entry}` : entry;
|
|
266
|
+
}
|
|
249
267
|
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
.sort((a, b) => a.localeCompare(b))
|
|
258
|
-
.map((key) => ({
|
|
259
|
-
key,
|
|
260
|
-
scope: classifySecretScope(key),
|
|
261
|
-
kind: classifySecretKey(key),
|
|
262
|
-
provider: this.provider,
|
|
263
|
-
present: true,
|
|
264
|
-
}));
|
|
268
|
+
function passKeyPath(ctx: PassContext, key: string): string {
|
|
269
|
+
const prefixed = prefixedEntry(ctx, key);
|
|
270
|
+
const normalizedEntry = normalize(prefixed);
|
|
271
|
+
const resolvedPath = resolve(ctx.passwordStoreDir, `${normalizedEntry}.gpg`);
|
|
272
|
+
const resolvedStore = resolve(ctx.passwordStoreDir);
|
|
273
|
+
if (!resolvedPath.startsWith(`${resolvedStore}/`)) {
|
|
274
|
+
throw new Error('Secret key resolves outside the password store');
|
|
265
275
|
}
|
|
276
|
+
return resolvedPath;
|
|
277
|
+
}
|
|
266
278
|
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
279
|
+
export async function passList(state: ControlPlaneState, prefix = 'openpalm/'): Promise<SecretEntryMetadata[]> {
|
|
280
|
+
const ctx = passContext(state);
|
|
281
|
+
// Scope walk to the passPrefix subdirectory
|
|
282
|
+
const walkDir = ctx.passPrefix
|
|
283
|
+
? join(ctx.passwordStoreDir, ctx.passPrefix)
|
|
284
|
+
: ctx.passwordStoreDir;
|
|
285
|
+
return walkPassStore(walkDir)
|
|
286
|
+
.filter((entry) => entry.startsWith(prefix))
|
|
287
|
+
.sort((a, b) => a.localeCompare(b))
|
|
288
|
+
.map((key) => ({
|
|
289
|
+
key,
|
|
290
|
+
scope: classifySecretScope(key),
|
|
291
|
+
kind: classifySecretKey(key),
|
|
292
|
+
provider: 'pass',
|
|
276
293
|
present: true,
|
|
277
|
-
};
|
|
278
|
-
|
|
294
|
+
}));
|
|
295
|
+
}
|
|
279
296
|
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
}
|
|
297
|
+
export async function passWrite(state: ControlPlaneState, key: string, value: string): Promise<SecretEntryMetadata> {
|
|
298
|
+
const ctx = passContext(state);
|
|
299
|
+
const canonicalKey = validatePassEntryName(key);
|
|
300
|
+
const storeEntry = prefixedEntry(ctx, canonicalKey);
|
|
301
|
+
await execWithInput('pass', ['insert', '-m', '-f', storeEntry], `${value}\n`, passEnv(ctx));
|
|
302
|
+
return {
|
|
303
|
+
key: canonicalKey,
|
|
304
|
+
scope: classifySecretScope(canonicalKey),
|
|
305
|
+
kind: classifySecretKey(canonicalKey),
|
|
306
|
+
provider: 'pass',
|
|
307
|
+
present: true,
|
|
308
|
+
};
|
|
309
|
+
}
|
|
294
310
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
311
|
+
export async function passGenerate(state: ControlPlaneState, key: string, length = 32): Promise<SecretEntryMetadata> {
|
|
312
|
+
const ctx = passContext(state);
|
|
313
|
+
const canonicalKey = validatePassEntryName(key);
|
|
314
|
+
const storeEntry = prefixedEntry(ctx, canonicalKey);
|
|
315
|
+
await execFile('pass', ['generate', '-n', '-f', storeEntry, String(length)], {
|
|
316
|
+
env: passEnv(ctx),
|
|
317
|
+
});
|
|
318
|
+
return {
|
|
319
|
+
key: canonicalKey,
|
|
320
|
+
scope: classifySecretScope(canonicalKey),
|
|
321
|
+
kind: classifySecretKey(canonicalKey),
|
|
322
|
+
provider: 'pass',
|
|
323
|
+
present: true,
|
|
324
|
+
};
|
|
325
|
+
}
|
|
301
326
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
327
|
+
export async function passRemove(state: ControlPlaneState, key: string): Promise<void> {
|
|
328
|
+
const ctx = passContext(state);
|
|
329
|
+
const storeEntry = prefixedEntry(ctx, key);
|
|
330
|
+
await execFile('pass', ['rm', '-f', storeEntry], {
|
|
331
|
+
env: passEnv(ctx),
|
|
332
|
+
});
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
export async function passExists(state: ControlPlaneState, key: string): Promise<boolean> {
|
|
336
|
+
const ctx = passContext(state);
|
|
337
|
+
return existsSync(passKeyPath(ctx, key));
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function makePassBackend(state: ControlPlaneState): SecretBackend {
|
|
341
|
+
return {
|
|
342
|
+
provider: 'pass',
|
|
343
|
+
capabilities: { generate: true, remove: true, rename: false },
|
|
344
|
+
list: (prefix) => passList(state, prefix),
|
|
345
|
+
write: (key, value) => passWrite(state, key, value),
|
|
346
|
+
generate: (key, length) => passGenerate(state, key, length),
|
|
347
|
+
remove: (key) => passRemove(state, key),
|
|
348
|
+
exists: (key) => passExists(state, key),
|
|
349
|
+
};
|
|
305
350
|
}
|
|
306
351
|
|
|
307
352
|
export function detectSecretBackend(state: ControlPlaneState): SecretBackend {
|
|
308
353
|
const providerConfig = readSecretProviderConfig(state);
|
|
309
354
|
if (providerConfig?.provider === 'pass') {
|
|
310
|
-
return
|
|
311
|
-
}
|
|
312
|
-
|
|
313
|
-
for (const schemaPath of [`${state.vaultDir}/user/user.env.schema`, `${state.vaultDir}/stack/stack.env.schema`]) {
|
|
314
|
-
if (!existsSync(schemaPath)) continue;
|
|
315
|
-
const content = readFileSync(schemaPath, 'utf-8');
|
|
316
|
-
if (content.includes('@varlock/pass-plugin')) {
|
|
317
|
-
return new PassBackend(state);
|
|
318
|
-
}
|
|
355
|
+
return makePassBackend(state);
|
|
319
356
|
}
|
|
320
357
|
|
|
321
|
-
|
|
358
|
+
// Historical fallback: pre-#391 we sniffed `.env.schema` files for a
|
|
359
|
+
// `@varlock/pass-plugin` marker. Schemas are gone; operators who want
|
|
360
|
+
// `pass` set `secret-provider.json` to `{ "provider": "pass" }`.
|
|
361
|
+
return makePlaintextBackend(state);
|
|
322
362
|
}
|
|
@@ -30,9 +30,8 @@ type CoreSecretMapping = {
|
|
|
30
30
|
|
|
31
31
|
const STATIC_CORE_MAPPINGS: CoreSecretMapping[] = [
|
|
32
32
|
// Core authentication tokens
|
|
33
|
-
{ secretKey: 'openpalm/admin-token', envKey: '
|
|
33
|
+
{ secretKey: 'openpalm/admin-token', envKey: 'OP_UI_TOKEN', scope: 'system' },
|
|
34
34
|
{ secretKey: 'openpalm/assistant-token', envKey: 'OP_ASSISTANT_TOKEN', scope: 'system' },
|
|
35
|
-
{ secretKey: 'openpalm/memory/auth-token', envKey: 'OP_MEMORY_TOKEN', scope: 'system' },
|
|
36
35
|
{ secretKey: 'openpalm/opencode/server-password', envKey: 'OP_OPENCODE_PASSWORD', scope: 'system' },
|
|
37
36
|
// LLM provider API keys
|
|
38
37
|
{ secretKey: 'openpalm/openai/api-key', envKey: 'OPENAI_API_KEY', scope: 'user' },
|
|
@@ -47,8 +46,6 @@ const STATIC_CORE_MAPPINGS: CoreSecretMapping[] = [
|
|
|
47
46
|
{ secretKey: 'openpalm/mcp/api-key', envKey: 'MCP_API_KEY', scope: 'user' },
|
|
48
47
|
{ secretKey: 'openpalm/embedding/api-key', envKey: 'EMBEDDING_API_KEY', scope: 'user' },
|
|
49
48
|
{ secretKey: 'openpalm/lmstudio/api-key', envKey: 'LMSTUDIO_API_KEY', scope: 'user' },
|
|
50
|
-
{ secretKey: 'openpalm/openviking/api-key', envKey: 'OPENVIKING_API_KEY', scope: 'user' },
|
|
51
|
-
{ secretKey: 'openpalm/openviking/vlm-api-key', envKey: 'VLM_API_KEY', scope: 'user' },
|
|
52
49
|
// Channel-specific credentials
|
|
53
50
|
{ secretKey: 'openpalm/discord/bot-token', envKey: 'DISCORD_BOT_TOKEN', scope: 'user' },
|
|
54
51
|
{ secretKey: 'openpalm/slack/bot-token', envKey: 'SLACK_BOT_TOKEN', scope: 'user' },
|
|
@@ -66,7 +63,7 @@ type SecretIndexFile = {
|
|
|
66
63
|
};
|
|
67
64
|
|
|
68
65
|
function secretIndexPath(state: ControlPlaneState): string {
|
|
69
|
-
return `${state.
|
|
66
|
+
return `${state.stateDir}/secrets/plaintext-index.json`;
|
|
70
67
|
}
|
|
71
68
|
|
|
72
69
|
function normalizeIndexedKey(key: string): string {
|
|
@@ -148,7 +145,7 @@ export function readPlaintextSecretIndex(state: ControlPlaneState): SecretIndexF
|
|
|
148
145
|
}
|
|
149
146
|
|
|
150
147
|
export function writePlaintextSecretIndex(state: ControlPlaneState, index: SecretIndexFile): void {
|
|
151
|
-
const dir = `${state.
|
|
148
|
+
const dir = `${state.stateDir}/secrets`;
|
|
152
149
|
mkdirSync(dir, { recursive: true });
|
|
153
150
|
writeFileSync(secretIndexPath(state), JSON.stringify(index, null, 2) + '\n');
|
|
154
151
|
}
|