@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,61 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Runtime configuration validation for the OpenPalm control plane.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Validation is a presence check on the canonical env keys we expect in
|
|
5
|
+
* the live config/stack/stack.env file. The
|
|
6
|
+
* historical schema files and external validation binary were retired in
|
|
7
|
+
* #391; everything advisory is surfaced as a non-blocking warning. The
|
|
8
|
+
* function never shells out and never reads schemas.
|
|
6
9
|
*/
|
|
7
|
-
import { existsSync
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
10
|
-
import { tmpdir } from "node:os";
|
|
11
|
-
import { execFile } from "node:child_process";
|
|
12
|
-
import { promisify } from "node:util";
|
|
10
|
+
import { existsSync } from "node:fs";
|
|
11
|
+
import { readStackEnv } from "./secrets.js";
|
|
12
|
+
import { getCoreSecretMappings } from "./secret-mappings.js";
|
|
13
13
|
import type { ControlPlaneState } from "./types.js";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
const
|
|
19
|
-
let VARLOCK_BIN = "varlock";
|
|
20
|
-
if (envVarlockBin) {
|
|
21
|
-
if (envVarlockBin === "varlock" || envVarlockBin.startsWith("/")) {
|
|
22
|
-
VARLOCK_BIN = envVarlockBin;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
function sanitizeVarlockMessage(msg: string): string {
|
|
27
|
-
return msg
|
|
28
|
-
.replace(/sk-[A-Za-z0-9]{20,}/g, "[REDACTED]")
|
|
29
|
-
.replace(/gsk_[A-Za-z0-9]{30,}/g, "[REDACTED]")
|
|
30
|
-
.replace(/AIza[A-Za-z0-9_\-]{35}/g, "[REDACTED]")
|
|
31
|
-
.replace(/[0-9a-f]{32,}/gi, "[REDACTED]")
|
|
32
|
-
.replace(/value '([^']*)'/g, "value '[REDACTED]'");
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
async function runVarlockLoad(
|
|
36
|
-
schemaFile: string,
|
|
37
|
-
envFile: string,
|
|
38
|
-
): Promise<void> {
|
|
39
|
-
const tmpDir = mkdtempSync(join(tmpdir(), "varlock-"));
|
|
40
|
-
try {
|
|
41
|
-
copyFileSync(schemaFile, join(tmpDir, ".env.schema"));
|
|
42
|
-
copyFileSync(envFile, join(tmpDir, ".env"));
|
|
43
|
-
await execFileAsync(
|
|
44
|
-
VARLOCK_BIN,
|
|
45
|
-
["load", "--path", `${tmpDir}/`],
|
|
46
|
-
{ timeout: 10000 },
|
|
47
|
-
);
|
|
48
|
-
} finally {
|
|
49
|
-
rmSync(tmpDir, { recursive: true, force: true });
|
|
50
|
-
}
|
|
51
|
-
}
|
|
15
|
+
// Stack-scoped env keys that must always exist and carry a non-empty value
|
|
16
|
+
// for the platform to boot. Keep this list small — anything optional
|
|
17
|
+
// belongs in the warning bucket instead.
|
|
18
|
+
const REQUIRED_STACK_KEYS = ["OP_UI_TOKEN", "OP_ASSISTANT_TOKEN"] as const;
|
|
52
19
|
|
|
53
20
|
/**
|
|
54
|
-
* Validate the
|
|
21
|
+
* Validate the live configuration files.
|
|
55
22
|
*
|
|
56
23
|
* Checks:
|
|
57
|
-
* 1.
|
|
58
|
-
*
|
|
24
|
+
* 1. config/stack/stack.env exists and carries every required key with a
|
|
25
|
+
* non-empty value.
|
|
26
|
+
* 2. Every secret env key in getCoreSecretMappings() is present (key only
|
|
27
|
+
* — blank values are warned about, never erred on, because operators
|
|
28
|
+
* may opt out of providers they don't use).
|
|
29
|
+
*
|
|
30
|
+
* Errors fail the result. Warnings do not. The function never reads
|
|
31
|
+
* schema files and never spawns subprocesses.
|
|
59
32
|
*/
|
|
60
33
|
export async function validateProposedState(state: ControlPlaneState): Promise<{
|
|
61
34
|
ok: boolean;
|
|
@@ -64,44 +37,36 @@ export async function validateProposedState(state: ControlPlaneState): Promise<{
|
|
|
64
37
|
}> {
|
|
65
38
|
const errors: string[] = [];
|
|
66
39
|
const warnings: string[] = [];
|
|
67
|
-
let anyFailed = false;
|
|
68
40
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
else if (trimmed.includes("WARN")) warnings.push(trimmed);
|
|
75
|
-
}
|
|
41
|
+
const stackEnvPath = `${state.stackDir}/stack.env`;
|
|
42
|
+
|
|
43
|
+
if (!existsSync(stackEnvPath)) {
|
|
44
|
+
errors.push(`ERROR: stack env file missing at ${stackEnvPath}`);
|
|
45
|
+
return { ok: false, errors, warnings };
|
|
76
46
|
}
|
|
77
47
|
|
|
78
|
-
|
|
79
|
-
const
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
anyFailed = true;
|
|
86
|
-
if (err && typeof err === "object" && "stderr" in err) {
|
|
87
|
-
collectOutput(String((err as { stderr: string }).stderr));
|
|
88
|
-
}
|
|
48
|
+
const stackEnv = readStackEnv(state.stackDir);
|
|
49
|
+
const userEnv: Record<string, string> = {};
|
|
50
|
+
|
|
51
|
+
for (const key of REQUIRED_STACK_KEYS) {
|
|
52
|
+
const value = stackEnv[key];
|
|
53
|
+
if (!value || value.trim().length === 0) {
|
|
54
|
+
errors.push(`ERROR: required key ${key} is missing or empty in config/stack/stack.env`);
|
|
89
55
|
}
|
|
90
56
|
}
|
|
91
57
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
}
|
|
58
|
+
// Every canonical secret should at least appear as a key somewhere in
|
|
59
|
+
// the env files so the operator sees the slot. Missing slots warn (not
|
|
60
|
+
// error) since not every provider is in use on every install.
|
|
61
|
+
for (const mapping of getCoreSecretMappings(stackEnv)) {
|
|
62
|
+
const inStack = Object.prototype.hasOwnProperty.call(stackEnv, mapping.envKey);
|
|
63
|
+
const inUser = Object.prototype.hasOwnProperty.call(userEnv, mapping.envKey);
|
|
64
|
+
if (!inStack && !inUser) {
|
|
65
|
+
warnings.push(
|
|
66
|
+
`WARN: ${mapping.envKey} (akm ${mapping.secretKey}) is not declared in config/stack/stack.env`,
|
|
67
|
+
);
|
|
103
68
|
}
|
|
104
69
|
}
|
|
105
70
|
|
|
106
|
-
return { ok:
|
|
71
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
107
72
|
}
|
package/src/index.ts
CHANGED
|
@@ -10,17 +10,22 @@
|
|
|
10
10
|
export {
|
|
11
11
|
LLM_PROVIDERS,
|
|
12
12
|
EMBEDDING_DIMS,
|
|
13
|
+
lookupEmbeddingDims,
|
|
13
14
|
} from "./provider-constants.js";
|
|
14
15
|
|
|
15
16
|
// ── Logger ──────────────────────────────────────────────────────────────
|
|
16
|
-
export {
|
|
17
|
+
export {
|
|
18
|
+
createLogger,
|
|
19
|
+
isSensitiveEnvKey,
|
|
20
|
+
redactValue,
|
|
21
|
+
redactExtra,
|
|
22
|
+
} from "./logger.js";
|
|
17
23
|
|
|
18
24
|
// ── Types ───────────────────────────────────────────────────────────────
|
|
19
25
|
export type {
|
|
20
26
|
ControlPlaneState,
|
|
21
27
|
CoreServiceName,
|
|
22
28
|
OptionalServiceName,
|
|
23
|
-
AccessScope,
|
|
24
29
|
ChannelInfo,
|
|
25
30
|
CallerType,
|
|
26
31
|
ArtifactMeta,
|
|
@@ -62,25 +67,33 @@ export {
|
|
|
62
67
|
uninstallAutomation,
|
|
63
68
|
} from "./control-plane/registry.js";
|
|
64
69
|
|
|
65
|
-
// ── Home Layout (v0.
|
|
70
|
+
// ── Home Layout (v0.11.0) ───────────────────────────────────────────────
|
|
66
71
|
export {
|
|
67
72
|
resolveOpenPalmHome,
|
|
68
73
|
resolveConfigDir,
|
|
69
|
-
|
|
70
|
-
|
|
74
|
+
resolveStashDir,
|
|
75
|
+
resolveWorkspaceDir,
|
|
76
|
+
resolveCacheDir,
|
|
77
|
+
resolveStateDir,
|
|
78
|
+
resolveStackDir,
|
|
71
79
|
resolveLogsDir,
|
|
72
|
-
resolveCacheHome,
|
|
73
|
-
resolveRegistryDir,
|
|
74
|
-
resolveRegistryAddonsDir,
|
|
75
|
-
resolveRegistryAutomationsDir,
|
|
76
80
|
ensureHomeDirs,
|
|
77
81
|
} from "./control-plane/home.js";
|
|
78
82
|
|
|
83
|
+
// ── Path Resolution ─────────────────────────────────────────────────────
|
|
84
|
+
export * from "./control-plane/paths.js";
|
|
85
|
+
|
|
79
86
|
// ── Env ─────────────────────────────────────────────────────────────────
|
|
80
87
|
export {
|
|
81
88
|
parseEnvContent,
|
|
82
89
|
parseEnvFile,
|
|
90
|
+
expandEnvVars,
|
|
83
91
|
mergeEnvContent,
|
|
92
|
+
removeEnvKey,
|
|
93
|
+
upsertEnvValue,
|
|
94
|
+
resolveRequestedImageTag,
|
|
95
|
+
reconcileStackEnvImageTag,
|
|
96
|
+
RELEASE_TAG_REGEX,
|
|
84
97
|
} from "./control-plane/env.js";
|
|
85
98
|
|
|
86
99
|
// ── Audit ───────────────────────────────────────────────────────────────
|
|
@@ -95,6 +108,7 @@ export {
|
|
|
95
108
|
PLAIN_CONFIG_KEYS,
|
|
96
109
|
ensureSecrets,
|
|
97
110
|
updateSecretsEnv,
|
|
111
|
+
writeAuthJsonProviderKeys,
|
|
98
112
|
readStackEnv,
|
|
99
113
|
patchSecretsEnvFile,
|
|
100
114
|
maskSecretValue,
|
|
@@ -104,6 +118,7 @@ export {
|
|
|
104
118
|
detectSecretBackend,
|
|
105
119
|
validatePassEntryName,
|
|
106
120
|
} from "./control-plane/secret-backend.js";
|
|
121
|
+
export type { SecretBackend } from "./control-plane/secret-backend.js";
|
|
107
122
|
// ── Setup Status ────────────────────────────────────────────────────────
|
|
108
123
|
export {
|
|
109
124
|
isSetupComplete,
|
|
@@ -114,35 +129,24 @@ export {
|
|
|
114
129
|
discoverChannels,
|
|
115
130
|
isAllowedService,
|
|
116
131
|
isValidChannel,
|
|
117
|
-
isChannelAddon,
|
|
118
132
|
} from "./control-plane/channels.js";
|
|
119
133
|
|
|
120
|
-
// ──
|
|
134
|
+
// ── Provider Model Discovery ────────────────────────────────────────────
|
|
121
135
|
export type {
|
|
122
|
-
|
|
123
|
-
|
|
136
|
+
ProviderModelsResult,
|
|
137
|
+
ModelDiscoveryReason,
|
|
138
|
+
} from "./control-plane/provider-models.js";
|
|
124
139
|
export {
|
|
125
|
-
EMBED_PROVIDERS,
|
|
126
|
-
resolveApiKey,
|
|
127
140
|
fetchProviderModels,
|
|
128
|
-
|
|
129
|
-
readMemoryConfig,
|
|
130
|
-
writeMemoryConfig,
|
|
131
|
-
ensureMemoryConfig,
|
|
132
|
-
checkVectorDimensions,
|
|
133
|
-
resetVectorStore,
|
|
134
|
-
provisionMemoryUser,
|
|
135
|
-
} from "./control-plane/memory-config.js";
|
|
141
|
+
} from "./control-plane/provider-models.js";
|
|
136
142
|
|
|
137
143
|
// ── Core Assets ─────────────────────────────────────────────────────────
|
|
138
144
|
export {
|
|
139
|
-
ensureUserEnvSchema,
|
|
140
|
-
ensureSystemEnvSchema,
|
|
141
|
-
ensureMemoryDir,
|
|
142
145
|
ensureCoreCompose,
|
|
143
146
|
readCoreCompose,
|
|
144
147
|
ensureOpenCodeSystemConfig,
|
|
145
148
|
refreshCoreAssets,
|
|
149
|
+
seedStashAssets,
|
|
146
150
|
} from "./control-plane/core-assets.js";
|
|
147
151
|
|
|
148
152
|
// ── Configuration Persistence ────────────────────────────────────────────
|
|
@@ -157,6 +161,7 @@ export {
|
|
|
157
161
|
writeSystemEnv,
|
|
158
162
|
readChannelSecrets,
|
|
159
163
|
writeChannelSecrets,
|
|
164
|
+
ensureComposeVolumeTargets,
|
|
160
165
|
} from "./control-plane/config-persistence.js";
|
|
161
166
|
|
|
162
167
|
// ── Rollback ─────────────────────────────────────────────────────────────
|
|
@@ -184,7 +189,6 @@ export {
|
|
|
184
189
|
buildManagedServices,
|
|
185
190
|
normalizeCaller,
|
|
186
191
|
} from "./control-plane/lifecycle.js";
|
|
187
|
-
export type { UpgradeResult } from "./control-plane/lifecycle.js";
|
|
188
192
|
|
|
189
193
|
// ── Docker ──────────────────────────────────────────────────────────────
|
|
190
194
|
export type { DockerResult } from "./control-plane/docker.js";
|
|
@@ -204,22 +208,20 @@ export {
|
|
|
204
208
|
composePull,
|
|
205
209
|
composeStats,
|
|
206
210
|
getDockerEvents,
|
|
207
|
-
|
|
211
|
+
inspectContainerStatus,
|
|
208
212
|
} from "./control-plane/docker.js";
|
|
209
213
|
|
|
210
214
|
// ── Scheduler ───────────────────────────────────────────────────────────
|
|
211
215
|
export type {
|
|
212
|
-
ActionType,
|
|
213
|
-
AutomationAction,
|
|
214
216
|
AutomationConfig,
|
|
215
|
-
|
|
217
|
+
AutomationRunResult,
|
|
216
218
|
} from "./control-plane/scheduler.js";
|
|
217
219
|
export {
|
|
218
220
|
SCHEDULE_PRESETS,
|
|
219
|
-
resolveSchedule,
|
|
220
|
-
parseAutomationYaml,
|
|
221
221
|
loadAutomations,
|
|
222
|
-
|
|
222
|
+
executeAutomation,
|
|
223
|
+
syncAutomations,
|
|
224
|
+
readAutomationLogs,
|
|
223
225
|
} from "./control-plane/scheduler.js";
|
|
224
226
|
|
|
225
227
|
// ── Model Runner (local provider detection) ─────────────────────────────
|
|
@@ -235,25 +237,17 @@ export {
|
|
|
235
237
|
// ── Stack Spec (v2) ──────────────────────────────────────────────────────
|
|
236
238
|
export type {
|
|
237
239
|
StackSpec,
|
|
238
|
-
StackSpecCapabilities,
|
|
239
|
-
StackSpecEmbeddings,
|
|
240
|
-
StackSpecMemory,
|
|
241
|
-
StackSpecTts,
|
|
242
|
-
StackSpecStt,
|
|
243
|
-
StackSpecReranker,
|
|
244
240
|
} from "./control-plane/stack-spec.js";
|
|
245
241
|
export {
|
|
246
242
|
STACK_SPEC_FILENAME,
|
|
247
243
|
writeStackSpec,
|
|
248
244
|
readStackSpec,
|
|
249
|
-
updateCapability,
|
|
250
|
-
parseCapabilityString,
|
|
251
|
-
formatCapabilityString,
|
|
252
245
|
} from "./control-plane/stack-spec.js";
|
|
253
246
|
|
|
254
247
|
// ── Spec-to-Env Derivation ──────────────────────────────────────────────
|
|
248
|
+
export type { VoiceVarsConfig } from "./control-plane/spec-to-env.js";
|
|
255
249
|
export {
|
|
256
|
-
|
|
250
|
+
writeVoiceVars,
|
|
257
251
|
} from "./control-plane/spec-to-env.js";
|
|
258
252
|
|
|
259
253
|
// ── Setup ────────────────────────────────────────────────────────────────
|
|
@@ -265,3 +259,42 @@ export type {
|
|
|
265
259
|
export {
|
|
266
260
|
performSetup,
|
|
267
261
|
} from "./control-plane/setup.js";
|
|
262
|
+
|
|
263
|
+
// ── Install Lock (shared between performSetup and startDeploy) ───────────
|
|
264
|
+
export type { InstallLockHandle } from "./control-plane/install-lock.js";
|
|
265
|
+
export {
|
|
266
|
+
acquireInstallLock,
|
|
267
|
+
releaseInstallLock,
|
|
268
|
+
} from "./control-plane/install-lock.js";
|
|
269
|
+
|
|
270
|
+
// ── Host OpenCode Import ─────────────────────────────────────────────────
|
|
271
|
+
export type {
|
|
272
|
+
HostOpenCodeStatus,
|
|
273
|
+
HostImportResult,
|
|
274
|
+
} from "./control-plane/host-opencode.js";
|
|
275
|
+
export {
|
|
276
|
+
detectHostOpenCode,
|
|
277
|
+
importHostOpenCode,
|
|
278
|
+
} from "./control-plane/host-opencode.js";
|
|
279
|
+
|
|
280
|
+
// ── AKM Vault ────────────────────────────────────────────────────────────
|
|
281
|
+
export {
|
|
282
|
+
AKM_USER_VAULT_REF,
|
|
283
|
+
buildAkmEnv,
|
|
284
|
+
ensureAkmUserVault,
|
|
285
|
+
writeAkmVaultKey,
|
|
286
|
+
deleteAkmVaultKey,
|
|
287
|
+
readAkmUserVaultFile,
|
|
288
|
+
readUserVaultSync,
|
|
289
|
+
} from "./control-plane/akm-vault.js";
|
|
290
|
+
|
|
291
|
+
// ── UI asset seeding and resolution ─────────────────────────────────────────
|
|
292
|
+
export type { UiBuildUpdateResult } from "./control-plane/ui-assets.js";
|
|
293
|
+
export {
|
|
294
|
+
resolveLocalOpenpalmDir,
|
|
295
|
+
seedOpenPalmDir,
|
|
296
|
+
resolveLocalUiBuild,
|
|
297
|
+
resolveUiBuildDir,
|
|
298
|
+
seedUiBuild,
|
|
299
|
+
checkAndUpdateUiBuild,
|
|
300
|
+
} from "./control-plane/ui-assets.js";
|
|
@@ -0,0 +1,228 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Tests for the in-house log redactor introduced in #391 (replacing varlock).
|
|
3
|
+
*
|
|
4
|
+
* The contract:
|
|
5
|
+
* - keys matching the word-bounded pattern
|
|
6
|
+
* `(^|_)(TOKEN|SECRET|KEY|PASSWORD|HMAC)(_|$)` (case-insensitive)
|
|
7
|
+
* have their value replaced with `'***REDACTED***'`.
|
|
8
|
+
* - substring false positives (e.g. `MONKEY`, `PACKET_SIZE`) are NOT redacted.
|
|
9
|
+
* - non-string sensitive values (numbers, booleans) are still redacted.
|
|
10
|
+
* - non-secret keys are passed through unchanged.
|
|
11
|
+
* - nested objects and arrays are walked recursively.
|
|
12
|
+
* - createLogger() applies the same masking before writing to stdout/stderr
|
|
13
|
+
* at every log level (debug/info/warn/error).
|
|
14
|
+
*/
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, test } from 'bun:test';
|
|
16
|
+
import {
|
|
17
|
+
createLogger,
|
|
18
|
+
isSensitiveEnvKey,
|
|
19
|
+
redactValue,
|
|
20
|
+
redactExtra,
|
|
21
|
+
} from './logger.js';
|
|
22
|
+
|
|
23
|
+
describe('redactValue', () => {
|
|
24
|
+
test('masks values for sensitive key suffixes', () => {
|
|
25
|
+
expect(redactValue('OPENAI_API_KEY', 'sk-abc')).toBe('***REDACTED***');
|
|
26
|
+
expect(redactValue('SLACK_BOT_TOKEN', 'xoxb-123')).toBe('***REDACTED***');
|
|
27
|
+
expect(redactValue('CHANNEL_DISCORD_SECRET', 'hmac-bytes')).toBe('***REDACTED***');
|
|
28
|
+
expect(redactValue('OP_OPENCODE_PASSWORD', 'hunter2')).toBe('***REDACTED***');
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test('matches case-insensitively', () => {
|
|
32
|
+
expect(redactValue('openai_api_key', 'sk-xyz')).toBe('***REDACTED***');
|
|
33
|
+
expect(redactValue('My_Token', 'abc')).toBe('***REDACTED***');
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('leaves non-secret values alone', () => {
|
|
37
|
+
expect(redactValue('OWNER_NAME', 'alice')).toBe('alice');
|
|
38
|
+
expect(redactValue('OP_HOME', '/openpalm')).toBe('/openpalm');
|
|
39
|
+
expect(redactValue('OP_ASSISTANT_PORT', '3800')).toBe('3800');
|
|
40
|
+
});
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
describe('isSensitiveEnvKey', () => {
|
|
44
|
+
test('returns true for token/secret/key/password/hmac suffix keys', () => {
|
|
45
|
+
expect(isSensitiveEnvKey('FOO_TOKEN')).toBe(true);
|
|
46
|
+
expect(isSensitiveEnvKey('FOO_SECRET')).toBe(true);
|
|
47
|
+
expect(isSensitiveEnvKey('FOO_KEY')).toBe(true);
|
|
48
|
+
expect(isSensitiveEnvKey('FOO_PASSWORD')).toBe(true);
|
|
49
|
+
expect(isSensitiveEnvKey('CHANNEL_FOO_HMAC')).toBe(true);
|
|
50
|
+
expect(isSensitiveEnvKey('OP_UI_TOKEN')).toBe(true);
|
|
51
|
+
expect(isSensitiveEnvKey('CHANNEL_API_KEY')).toBe(true);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('returns true for bare or prefix forms', () => {
|
|
55
|
+
expect(isSensitiveEnvKey('TOKEN')).toBe(true);
|
|
56
|
+
expect(isSensitiveEnvKey('SECRET')).toBe(true);
|
|
57
|
+
expect(isSensitiveEnvKey('KEY')).toBe(true);
|
|
58
|
+
expect(isSensitiveEnvKey('HMAC_KEY')).toBe(true);
|
|
59
|
+
expect(isSensitiveEnvKey('PASSWORD_HASH')).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
test('returns false for substring false positives', () => {
|
|
63
|
+
// MONKEY contains the substring KEY but no underscore boundary.
|
|
64
|
+
expect(isSensitiveEnvKey('MONKEY')).toBe(false);
|
|
65
|
+
// PACKET_SIZE: KET is not one of the words; ET_SIZE is unrelated.
|
|
66
|
+
expect(isSensitiveEnvKey('PACKET_SIZE')).toBe(false);
|
|
67
|
+
// MARKETING_KEYWORD: KEYWORD does not have a trailing underscore or EOL.
|
|
68
|
+
expect(isSensitiveEnvKey('MARKETING_KEYWORD')).toBe(false);
|
|
69
|
+
// KEYBOARD: starts with KEY but not followed by underscore or EOL.
|
|
70
|
+
expect(isSensitiveEnvKey('KEYBOARD')).toBe(false);
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
test('returns false for ordinary keys', () => {
|
|
74
|
+
expect(isSensitiveEnvKey('OWNER_NAME')).toBe(false);
|
|
75
|
+
expect(isSensitiveEnvKey('OP_HOME')).toBe(false);
|
|
76
|
+
expect(isSensitiveEnvKey('OP_ASSISTANT_PORT')).toBe(false);
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
|
|
80
|
+
describe('redactExtra', () => {
|
|
81
|
+
test('masks top-level secret string values', () => {
|
|
82
|
+
const result = redactExtra({
|
|
83
|
+
OPENAI_API_KEY: 'sk-abc',
|
|
84
|
+
OWNER_NAME: 'alice',
|
|
85
|
+
});
|
|
86
|
+
expect(result).toEqual({
|
|
87
|
+
OPENAI_API_KEY: '***REDACTED***',
|
|
88
|
+
OWNER_NAME: 'alice',
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
test('walks nested objects', () => {
|
|
93
|
+
const result = redactExtra({
|
|
94
|
+
env: {
|
|
95
|
+
OPENAI_API_KEY: 'sk-abc',
|
|
96
|
+
OWNER_NAME: 'alice',
|
|
97
|
+
},
|
|
98
|
+
});
|
|
99
|
+
expect(result).toEqual({
|
|
100
|
+
env: {
|
|
101
|
+
OPENAI_API_KEY: '***REDACTED***',
|
|
102
|
+
OWNER_NAME: 'alice',
|
|
103
|
+
},
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
test('handles arrays of objects', () => {
|
|
108
|
+
const result = redactExtra({
|
|
109
|
+
items: [
|
|
110
|
+
{ OPENAI_API_KEY: 'sk-1' },
|
|
111
|
+
{ OWNER_NAME: 'bob' },
|
|
112
|
+
],
|
|
113
|
+
});
|
|
114
|
+
expect(result).toEqual({
|
|
115
|
+
items: [
|
|
116
|
+
{ OPENAI_API_KEY: '***REDACTED***' },
|
|
117
|
+
{ OWNER_NAME: 'bob' },
|
|
118
|
+
],
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
test('returns primitive inputs unchanged', () => {
|
|
123
|
+
expect(redactExtra('plain')).toBe('plain');
|
|
124
|
+
expect(redactExtra(42)).toBe(42);
|
|
125
|
+
expect(redactExtra(null)).toBe(null);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('redacts non-string sensitive values (numbers, booleans)', () => {
|
|
129
|
+
const result = redactExtra({
|
|
130
|
+
OP_UI_TOKEN: 12345,
|
|
131
|
+
OPENAI_API_KEY: true,
|
|
132
|
+
OWNER_NAME: 'alice',
|
|
133
|
+
});
|
|
134
|
+
expect(result).toEqual({
|
|
135
|
+
OP_UI_TOKEN: '***REDACTED***',
|
|
136
|
+
OPENAI_API_KEY: '***REDACTED***',
|
|
137
|
+
OWNER_NAME: 'alice',
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
test('does not redact substring false positives', () => {
|
|
142
|
+
const result = redactExtra({
|
|
143
|
+
MONKEY: 'banana',
|
|
144
|
+
PACKET_SIZE: 1500,
|
|
145
|
+
MARKETING_KEYWORD: 'free',
|
|
146
|
+
OPENAI_API_KEY: 'sk-leak',
|
|
147
|
+
});
|
|
148
|
+
expect(result).toEqual({
|
|
149
|
+
MONKEY: 'banana',
|
|
150
|
+
PACKET_SIZE: 1500,
|
|
151
|
+
MARKETING_KEYWORD: 'free',
|
|
152
|
+
OPENAI_API_KEY: '***REDACTED***',
|
|
153
|
+
});
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
test('redacts CHANNEL_FOO_HMAC values', () => {
|
|
157
|
+
const result = redactExtra({ CHANNEL_DISCORD_HMAC: 'hmac-bytes' });
|
|
158
|
+
expect(result).toEqual({ CHANNEL_DISCORD_HMAC: '***REDACTED***' });
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
describe('createLogger', () => {
|
|
163
|
+
const origLog = console.log;
|
|
164
|
+
const origErr = console.error;
|
|
165
|
+
let logged: string[] = [];
|
|
166
|
+
|
|
167
|
+
beforeEach(() => {
|
|
168
|
+
logged = [];
|
|
169
|
+
console.log = (...args: unknown[]) => {
|
|
170
|
+
logged.push(args.map((a) => String(a)).join(' '));
|
|
171
|
+
};
|
|
172
|
+
console.error = (...args: unknown[]) => {
|
|
173
|
+
logged.push(args.map((a) => String(a)).join(' '));
|
|
174
|
+
};
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
afterEach(() => {
|
|
178
|
+
console.log = origLog;
|
|
179
|
+
console.error = origErr;
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
test('redacts sensitive keys in the extra payload before writing the log line', () => {
|
|
183
|
+
const logger = createLogger('test');
|
|
184
|
+
logger.info('msg', { OPENAI_API_KEY: 'sk-leak', OWNER_NAME: 'alice' });
|
|
185
|
+
expect(logged.length).toBe(1);
|
|
186
|
+
expect(logged[0]).toContain('"OPENAI_API_KEY":"***REDACTED***"');
|
|
187
|
+
expect(logged[0]).toContain('"OWNER_NAME":"alice"');
|
|
188
|
+
expect(logged[0]).not.toContain('sk-leak');
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
test('error level still goes through redaction', () => {
|
|
192
|
+
const logger = createLogger('test');
|
|
193
|
+
logger.error('boom', { OP_UI_TOKEN: 'tok-leak' });
|
|
194
|
+
expect(logged[0]).toContain('"OP_UI_TOKEN":"***REDACTED***"');
|
|
195
|
+
expect(logged[0]).not.toContain('tok-leak');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
test('warn level applies redaction', () => {
|
|
199
|
+
const logger = createLogger('test');
|
|
200
|
+
logger.warn('caution', { CHANNEL_API_KEY: 'warn-leak' });
|
|
201
|
+
expect(logged.length).toBe(1);
|
|
202
|
+
expect(logged[0]).toContain('"CHANNEL_API_KEY":"***REDACTED***"');
|
|
203
|
+
expect(logged[0]).not.toContain('warn-leak');
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
test('debug level applies redaction', () => {
|
|
207
|
+
const logger = createLogger('test');
|
|
208
|
+
logger.debug('detail', { CHANNEL_FOO_HMAC: 'debug-leak' });
|
|
209
|
+
expect(logged.length).toBe(1);
|
|
210
|
+
expect(logged[0]).toContain('"CHANNEL_FOO_HMAC":"***REDACTED***"');
|
|
211
|
+
expect(logged[0]).not.toContain('debug-leak');
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
test('substring false positives are not redacted at log time', () => {
|
|
215
|
+
const logger = createLogger('test');
|
|
216
|
+
logger.info('msg', { MONKEY: 'banana', PACKET_SIZE: 1500 });
|
|
217
|
+
expect(logged[0]).toContain('"MONKEY":"banana"');
|
|
218
|
+
expect(logged[0]).toContain('"PACKET_SIZE":1500');
|
|
219
|
+
expect(logged[0]).not.toContain('***REDACTED***');
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
test('non-string sensitive values are redacted at log time', () => {
|
|
223
|
+
const logger = createLogger('test');
|
|
224
|
+
logger.info('msg', { OP_UI_TOKEN: 12345 });
|
|
225
|
+
expect(logged[0]).toContain('"OP_UI_TOKEN":"***REDACTED***"');
|
|
226
|
+
expect(logged[0]).not.toContain('12345');
|
|
227
|
+
});
|
|
228
|
+
});
|