@openpalm/lib 0.11.0-beta.1 → 0.11.0-beta.3

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.
@@ -19,7 +19,7 @@ function makeValidSpec(overrides?: Partial<SetupSpec>): SetupSpec {
19
19
  version: 2,
20
20
  llm: { provider: "openai", model: "gpt-4o", baseUrl: "https://api.openai.com/v1" },
21
21
  embedding: { provider: "openai", model: "text-embedding-3-small", dims: 1536, baseUrl: "https://api.openai.com/v1" },
22
- security: { adminToken: "test-admin-token-12345" },
22
+ security: { uiLoginPassword: "test-admin-token-12345" },
23
23
  owner: { name: "Test User", email: "test@example.com" },
24
24
  connections: [
25
25
  {
@@ -72,17 +72,17 @@ describe("validateSetupSpec", () => {
72
72
  expect(result.errors.some((e) => e.includes("security object is required"))).toBe(true);
73
73
  });
74
74
 
75
- it("rejects missing security.adminToken", () => {
75
+ it("rejects missing security.uiLoginPassword", () => {
76
76
  const spec = makeValidSpec();
77
- spec.security.adminToken = "";
77
+ spec.security.uiLoginPassword = "";
78
78
  const result = validateSetupSpec(spec);
79
79
  expect(result.valid).toBe(false);
80
- expect(result.errors.some((e) => e.includes("security.adminToken"))).toBe(true);
80
+ expect(result.errors.some((e) => e.includes("security.uiLoginPassword"))).toBe(true);
81
81
  });
82
82
 
83
- it("rejects short security.adminToken", () => {
83
+ it("rejects short security.uiLoginPassword", () => {
84
84
  const spec = makeValidSpec();
85
- spec.security.adminToken = "short";
85
+ spec.security.uiLoginPassword = "short";
86
86
  const result = validateSetupSpec(spec);
87
87
  expect(result.valid).toBe(false);
88
88
  expect(result.errors.some((e) => e.includes("at least 8"))).toBe(true);
@@ -199,9 +199,10 @@ describe("validateSetupSpec", () => {
199
199
  // ── Tests: buildSecretsFromSetup ─────────────────────────────────────────
200
200
 
201
201
  describe("buildSecretsFromSetup", () => {
202
- it("does not include admin token in user secrets", () => {
202
+ it("does not include UI login password in user secrets", () => {
203
203
  const spec = makeValidSpec();
204
204
  const secrets = buildSecretsFromSetup(spec.connections, spec.owner);
205
+ expect(secrets.OP_UI_LOGIN_PASSWORD).toBeUndefined();
205
206
  expect(secrets.OP_UI_TOKEN).toBeUndefined();
206
207
  expect(secrets.ADMIN_TOKEN).toBeUndefined();
207
208
  });
@@ -304,11 +305,14 @@ describe("buildAuthJsonFromSetup", () => {
304
305
  });
305
306
 
306
307
  describe("buildSystemSecretsFromSetup", () => {
307
- it("includes distinct admin and assistant credentials", () => {
308
+ // Phase 4: assistant token was removed; the only stack.env secret this
309
+ // helper writes now is OP_UI_LOGIN_PASSWORD. OP_OPENCODE_PASSWORD is
310
+ // generated by ensureSystemSecrets() and persists across reruns.
311
+ it("returns OP_UI_LOGIN_PASSWORD equal to the supplied operator password", () => {
308
312
  const secrets = buildSystemSecretsFromSetup("test-admin-token-12345");
309
- expect(secrets.OP_UI_TOKEN).toBe("test-admin-token-12345");
310
- expect(typeof secrets.OP_ASSISTANT_TOKEN).toBe("string");
311
- expect(secrets.OP_ASSISTANT_TOKEN).not.toBe("test-admin-token-12345");
313
+ expect(secrets.OP_UI_LOGIN_PASSWORD).toBe("test-admin-token-12345");
314
+ expect(secrets.OP_UI_TOKEN).toBeUndefined();
315
+ expect(secrets.OP_ASSISTANT_TOKEN).toBeUndefined();
312
316
  });
313
317
  });
314
318
 
@@ -356,7 +360,7 @@ describe("performSetup", () => {
356
360
  join(stackDir, "stack.env"),
357
361
  [
358
362
  "OP_SETUP_COMPLETE=false",
359
- "OP_UI_TOKEN=",
363
+ "OP_UI_LOGIN_PASSWORD=",
360
364
  "OPENAI_API_KEY=",
361
365
  "OPENAI_BASE_URL=",
362
366
  "ANTHROPIC_API_KEY=",
@@ -384,13 +388,13 @@ describe("performSetup", () => {
384
388
 
385
389
  it("returns an error for invalid input", async () => {
386
390
  const result = await performSetup(
387
- { security: { adminToken: "short" } } as SetupSpec
391
+ { security: { uiLoginPassword: "short" } } as SetupSpec
388
392
  );
389
393
  expect(result.ok).toBe(false);
390
394
  expect(result.error).toBeDefined();
391
395
  });
392
396
 
393
- it("writes stack.env with the admin token", async () => {
397
+ it("writes stack.env with the UI login password", async () => {
394
398
  const result = await performSetup(makeValidSpec());
395
399
  expect(result.ok).toBe(true);
396
400
 
@@ -7,7 +7,6 @@
7
7
  */
8
8
  import { readFileSync, writeFileSync, existsSync, mkdirSync, renameSync } from "node:fs";
9
9
  import { join } from "node:path";
10
- import { randomBytes } from "node:crypto";
11
10
  import { createLogger } from "../logger.js";
12
11
  import {
13
12
  PROVIDER_KEY_MAP,
@@ -70,7 +69,12 @@ export type SetupSpec = {
70
69
  embedding?: { provider: string; model: string; dims: number; baseUrl?: string };
71
70
  tts?: { enabled?: boolean; engine?: string; provider?: string; baseURL?: string; model?: string; voice?: string };
72
71
  stt?: { enabled?: boolean; engine?: string; provider?: string; baseURL?: string; model?: string; language?: string };
73
- security: { adminToken: string };
72
+ /**
73
+ * Operator-supplied UI login password. Persisted to stack.env as
74
+ * `OP_UI_LOGIN_PASSWORD`. Replaces the legacy `adminToken` field
75
+ * (Phase 4 of docs/technical/auth-and-proxy-refactor-plan.md).
76
+ */
77
+ security: { uiLoginPassword: string };
74
78
  owner?: { name?: string; email?: string };
75
79
  connections: SetupConnection[];
76
80
  channelCredentials?: Record<string, Record<string, string>>;
@@ -121,41 +125,26 @@ export function buildAuthJsonFromSetup(
121
125
  }
122
126
 
123
127
  /**
124
- * Build the system-secret env update.
128
+ * Build the system-secret env update for the wizard / CLI install path.
125
129
  *
126
- * `OP_ASSISTANT_TOKEN` is critical: rotating it on a running stack would
127
- * invalidate every container's auth. We therefore distinguish three cases:
128
- * - existing system env has a non-empty token reuse it (idempotent rerun).
129
- * - existing system env explicitly contains `OP_ASSISTANT_TOKEN=` (blank) →
130
- * throw rather than silently rotate. This means a user edited stack.env
131
- * or a previous run wrote it blank; either way silent rotation breaks the
132
- * running stack.
133
- * - the key is absent entirely → generate a fresh token (first install).
130
+ * Phase 4 of the auth/proxy refactor collapsed the legacy
131
+ * `OP_UI_TOKEN` / `OP_ASSISTANT_TOKEN` pair into a single operator login
132
+ * secret (`OP_UI_LOGIN_PASSWORD`). The browser stores the cookie value =
133
+ * password; `requireAdmin()` compares the cookie against
134
+ * `process.env.OP_UI_LOGIN_PASSWORD` via the existing `safeTokenCompare`.
134
135
  *
135
- * If you legitimately need to rotate the token, delete the OP_ASSISTANT_TOKEN
136
- * line from stack.env (rather than blanking it) before re-running setup.
136
+ * `OP_OPENCODE_PASSWORD` is generated by `ensureSystemSecrets()` on first
137
+ * run and persists across reruns it is not regenerated here.
138
+ *
139
+ * `existingSystemEnv` is unused now but the parameter is kept so callers
140
+ * compile unchanged. It can be removed in a follow-up cleanup.
137
141
  */
138
142
  export function buildSystemSecretsFromSetup(
139
- adminToken: string,
140
- existingSystemEnv: Record<string, string> = {}
143
+ uiLoginPassword: string,
144
+ _existingSystemEnv: Record<string, string> = {}
141
145
  ): Record<string, string> {
142
- const hasKey = Object.prototype.hasOwnProperty.call(existingSystemEnv, "OP_ASSISTANT_TOKEN");
143
- const existing = existingSystemEnv.OP_ASSISTANT_TOKEN;
144
- let token: string;
145
- if (existing) {
146
- token = existing;
147
- } else if (hasKey) {
148
- throw new Error(
149
- "OP_ASSISTANT_TOKEN is present but blank in config/stack/stack.env. " +
150
- "Refusing to silently rotate the token (it would break the running stack). " +
151
- "Restore the previous value or remove the line entirely to generate a fresh one.",
152
- );
153
- } else {
154
- token = randomBytes(32).toString("hex");
155
- }
156
146
  return {
157
- OP_UI_TOKEN: adminToken,
158
- OP_ASSISTANT_TOKEN: token,
147
+ OP_UI_LOGIN_PASSWORD: uiLoginPassword,
159
148
  };
160
149
  }
161
150
 
@@ -205,7 +194,7 @@ export async function performSetup(
205
194
  if (!validation.valid) return { ok: false, error: validation.errors.join("; ") };
206
195
 
207
196
  const { llm, embedding, tts, stt, security, owner, connections, channelCredentials, addons, imageTag, hostAkm } = input;
208
- const state = opts?.state ?? createState(security.adminToken);
197
+ const state = opts?.state ?? createState();
209
198
 
210
199
  // Acquire install lock to prevent two concurrent setup runs from racing on
211
200
  // the same config directory. The lock lives in stateDir so it is co-located
@@ -238,7 +227,7 @@ export async function performSetup(
238
227
  }
239
228
  }
240
229
  updateSecretsEnv(state, updates);
241
- updateSystemSecretsEnv(state, buildSystemSecretsFromSetup(security.adminToken, existingSystemEnv));
230
+ updateSystemSecretsEnv(state, buildSystemSecretsFromSetup(security.uiLoginPassword, existingSystemEnv));
242
231
  // Provider API keys land in OpenCode's auth.json (bind-mounted into
243
232
  // the assistant container) — never in stack.env.
244
233
  writeAuthJsonProviderKeys(state, providerKeys);
@@ -248,9 +237,6 @@ export async function performSetup(
248
237
  return { ok: false, error: `Failed to persist setup outputs: ${message}` };
249
238
  }
250
239
 
251
- state.adminToken = security.adminToken;
252
- state.assistantToken = readStackEnv(state.stackDir).OP_ASSISTANT_TOKEN ?? state.assistantToken;
253
-
254
240
  // Everything from here through the OP_SETUP_COMPLETE write is wrapped in a
255
241
  // single try/catch so that a disk-full or permission-denied mid-way returns a
256
242
  // clean error rather than leaving a broken half-installed ~/.openpalm/.
@@ -51,12 +51,18 @@ export function deriveSystemEnvFromSpec(
51
51
  export type VoiceVarsConfig = {
52
52
  tts?: {
53
53
  enabled?: boolean;
54
+ /** Engine name (e.g. "kokoro", "elevenlabs", "browser"). */
55
+ engine?: string;
56
+ /** Optional sub-provider qualifier when an engine fronts multiple providers. */
57
+ provider?: string;
54
58
  baseURL?: string;
55
59
  model?: string;
56
60
  voice?: string;
57
61
  };
58
62
  stt?: {
59
63
  enabled?: boolean;
64
+ engine?: string;
65
+ provider?: string;
60
66
  baseURL?: string;
61
67
  model?: string;
62
68
  language?: string;
@@ -65,7 +71,8 @@ export type VoiceVarsConfig = {
65
71
 
66
72
  /**
67
73
  * Write TTS/STT env vars to stack.env for the voice channel container.
68
- * Only vars with non-empty values are written; missing values are left unchanged.
74
+ * `engine` always writes (even if it's the only field) so picking an
75
+ * engine without filling in URL/model still persists.
69
76
  */
70
77
  export function writeVoiceVars(config: VoiceVarsConfig, stackDir: string): void {
71
78
  const stackEnvPath = `${stackDir}/stack.env`;
@@ -74,11 +81,15 @@ export function writeVoiceVars(config: VoiceVarsConfig, stackDir: string): void
74
81
 
75
82
  const { tts, stt } = config;
76
83
  if (tts?.enabled !== false) {
84
+ if (tts?.engine) vars["TTS_ENGINE"] = tts.engine;
85
+ if (tts?.provider) vars["TTS_PROVIDER"] = tts.provider;
77
86
  if (tts?.baseURL) vars["TTS_BASE_URL"] = tts.baseURL;
78
87
  if (tts?.model) vars["TTS_MODEL"] = tts.model;
79
88
  if (tts?.voice) vars["TTS_VOICE"] = tts.voice;
80
89
  }
81
90
  if (stt?.enabled !== false) {
91
+ if (stt?.engine) vars["STT_ENGINE"] = stt.engine;
92
+ if (stt?.provider) vars["STT_PROVIDER"] = stt.provider;
82
93
  if (stt?.baseURL) vars["STT_BASE_URL"] = stt.baseURL;
83
94
  if (stt?.model) vars["STT_MODEL"] = stt.model;
84
95
  if (stt?.language) vars["STT_LANGUAGE"] = stt.language;
@@ -12,11 +12,6 @@ export type OptionalServiceName = never;
12
12
 
13
13
  export type AccessScope = "host" | "lan";
14
14
  export type CallerType = "assistant" | "cli" | "ui" | "system" | "test" | "unknown";
15
- export type AuditContext = {
16
- actor: string;
17
- requestId?: string;
18
- callerType?: CallerType;
19
- };
20
15
 
21
16
  /** Info about a discovered channel */
22
17
  export type ChannelInfo = {
@@ -24,16 +19,6 @@ export type ChannelInfo = {
24
19
  ymlPath: string;
25
20
  };
26
21
 
27
- export type AuditEntry = {
28
- at: string;
29
- requestId: string;
30
- actor: string;
31
- callerType: CallerType;
32
- action: string;
33
- args: Record<string, unknown>;
34
- ok: boolean;
35
- };
36
-
37
22
  export type ArtifactMeta = {
38
23
  name: string;
39
24
  sha256: string;
@@ -42,8 +27,6 @@ export type ArtifactMeta = {
42
27
  };
43
28
 
44
29
  export type ControlPlaneState = {
45
- adminToken: string;
46
- assistantToken: string;
47
30
  homeDir: string;
48
31
  configDir: string;
49
32
  stashDir: string; // homeDir/stash
@@ -56,7 +39,6 @@ export type ControlPlaneState = {
56
39
  compose: string;
57
40
  };
58
41
  artifactMeta: ArtifactMeta[];
59
- audit: AuditEntry[];
60
42
  };
61
43
 
62
44
  // ── Constants ──────────────────────────────────────────────────────────
@@ -15,7 +15,7 @@ import type { ControlPlaneState } from "./types.js";
15
15
  // Stack-scoped env keys that must always exist and carry a non-empty value
16
16
  // for the platform to boot. Keep this list small — anything optional
17
17
  // belongs in the warning bucket instead.
18
- const REQUIRED_STACK_KEYS = ["OP_UI_TOKEN", "OP_ASSISTANT_TOKEN"] as const;
18
+ const REQUIRED_STACK_KEYS = ["OP_UI_LOGIN_PASSWORD"] as const;
19
19
 
20
20
  /**
21
21
  * Validate the live configuration files.
package/src/index.ts CHANGED
@@ -29,7 +29,6 @@ export type {
29
29
  ChannelInfo,
30
30
  CallerType,
31
31
  ArtifactMeta,
32
- AuditEntry,
33
32
  } from "./control-plane/types.js";
34
33
  export {
35
34
  CORE_SERVICES,
@@ -44,6 +43,7 @@ export {
44
43
  // ── Registry Catalog ─────────────────────────────────────────────────────
45
44
  export type {
46
45
  AddonMutationResult,
46
+ AddonProfile,
47
47
  RegistryAutomationEntry,
48
48
  RegistryComponentEntry,
49
49
  RegistryAddonConfig,
@@ -58,6 +58,9 @@ export {
58
58
  getRegistryAutomation,
59
59
  getRegistryAddonConfig,
60
60
  getAddonServiceNames,
61
+ getAddonProfiles,
62
+ getAddonProfileSelection,
63
+ setAddonProfileSelection,
61
64
  listAvailableAddonIds,
62
65
  listEnabledAddonIds,
63
66
  enableAddon,
@@ -96,9 +99,6 @@ export {
96
99
  RELEASE_TAG_REGEX,
97
100
  } from "./control-plane/env.js";
98
101
 
99
- // ── Audit ───────────────────────────────────────────────────────────────
100
- export { appendAudit } from "./control-plane/audit.js";
101
-
102
102
  // ── OpenCode Client ─────────────────────────────────────────────────────
103
103
  export { createOpenCodeClient } from "./control-plane/opencode-client.js";
104
104
  export type { ProxyResult, OpenCodeProvider } from "./control-plane/opencode-client.js";
@@ -114,6 +114,8 @@ export {
114
114
  maskSecretValue,
115
115
  ensureOpenCodeConfig,
116
116
  } from "./control-plane/secrets.js";
117
+ export { migrateAuth0110 } from "./control-plane/migrate-0110.js";
118
+ export type { MigrateAuth0110Result } from "./control-plane/migrate-0110.js";
117
119
  export {
118
120
  detectSecretBackend,
119
121
  validatePassEntryName,
@@ -234,6 +236,13 @@ export {
234
236
  buildComposeCliArgs,
235
237
  } from "./control-plane/compose-args.js";
236
238
 
239
+ // ── Compose Error Parsing ────────────────────────────────────────────────
240
+ export type { ComposeServiceFailure } from "./control-plane/compose-errors.js";
241
+ export {
242
+ parseComposeStderr,
243
+ summarizeComposeStderr,
244
+ } from "./control-plane/compose-errors.js";
245
+
237
246
  // ── Stack Spec (v2) ──────────────────────────────────────────────────────
238
247
  export type {
239
248
  StackSpec,
package/src/logger.ts CHANGED
@@ -13,7 +13,7 @@ export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
13
13
  * with un-anchored alternations was sloppy enough to invite future bugs).
14
14
  *
15
15
  * Examples:
16
- * OP_UI_TOKEN → sensitive (suffix _TOKEN)
16
+ * OP_UI_LOGIN_PASSWORD → sensitive (suffix _PASSWORD)
17
17
  * CHANNEL_API_KEY → sensitive (suffix _KEY)
18
18
  * CHANNEL_FOO_HMAC → sensitive (suffix _HMAC)
19
19
  * HMAC_KEY → sensitive (prefix HMAC_, suffix _KEY)
@@ -1,41 +0,0 @@
1
- /**
2
- * Audit logging for the OpenPalm control plane.
3
- */
4
- import { mkdirSync, appendFileSync } from "node:fs";
5
- import type { ControlPlaneState, AuditEntry, CallerType } from "./types.js";
6
-
7
- const MAX_AUDIT_MEMORY = 1000;
8
-
9
- export function appendAudit(
10
- state: ControlPlaneState,
11
- actor: string,
12
- action: string,
13
- args: Record<string, unknown>,
14
- ok: boolean,
15
- requestId = "",
16
- callerType: CallerType = "unknown"
17
- ): void {
18
- const entry: AuditEntry = {
19
- at: new Date().toISOString(),
20
- requestId,
21
- actor,
22
- callerType,
23
- action,
24
- args,
25
- ok
26
- };
27
- state.audit.push(entry);
28
- if (state.audit.length > MAX_AUDIT_MEMORY) {
29
- state.audit = state.audit.slice(-MAX_AUDIT_MEMORY);
30
- }
31
- try {
32
- const logsDir = `${state.stateDir}/logs`;
33
- mkdirSync(logsDir, { recursive: true });
34
- appendFileSync(
35
- `${logsDir}/admin-audit.jsonl`,
36
- JSON.stringify(entry) + "\n"
37
- );
38
- } catch {
39
- // best-effort persistence
40
- }
41
- }