@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.
- package/package.json +1 -1
- package/src/control-plane/akm-vault.test.ts +1 -4
- package/src/control-plane/compose-args.test.ts +0 -3
- package/src/control-plane/compose-errors.test.ts +106 -0
- package/src/control-plane/compose-errors.ts +117 -0
- package/src/control-plane/config-persistence.ts +1 -2
- package/src/control-plane/host-opencode.test.ts +0 -3
- package/src/control-plane/install-edge-cases.test.ts +26 -66
- package/src/control-plane/lifecycle.ts +8 -40
- package/src/control-plane/migrate-0110.test.ts +177 -0
- package/src/control-plane/migrate-0110.ts +99 -0
- package/src/control-plane/paths.ts +8 -1
- package/src/control-plane/registry-components.test.ts +3 -2
- package/src/control-plane/registry.test.ts +64 -0
- package/src/control-plane/registry.ts +113 -0
- package/src/control-plane/secret-backend.test.ts +5 -8
- package/src/control-plane/secret-mappings.ts +2 -3
- package/src/control-plane/secrets.ts +13 -7
- package/src/control-plane/setup-config.schema.json +3 -3
- package/src/control-plane/setup-status.ts +6 -1
- package/src/control-plane/setup-validation.ts +2 -2
- package/src/control-plane/setup.test.ts +18 -14
- package/src/control-plane/setup.ts +22 -36
- package/src/control-plane/spec-to-env.ts +12 -1
- package/src/control-plane/types.ts +0 -18
- package/src/control-plane/validate.ts +1 -1
- package/src/index.ts +13 -4
- package/src/logger.ts +1 -1
- package/src/control-plane/audit.ts +0 -41
|
@@ -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: {
|
|
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.
|
|
75
|
+
it("rejects missing security.uiLoginPassword", () => {
|
|
76
76
|
const spec = makeValidSpec();
|
|
77
|
-
spec.security.
|
|
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.
|
|
80
|
+
expect(result.errors.some((e) => e.includes("security.uiLoginPassword"))).toBe(true);
|
|
81
81
|
});
|
|
82
82
|
|
|
83
|
-
it("rejects short security.
|
|
83
|
+
it("rejects short security.uiLoginPassword", () => {
|
|
84
84
|
const spec = makeValidSpec();
|
|
85
|
-
spec.security.
|
|
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
|
|
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
|
-
|
|
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.
|
|
310
|
-
expect(
|
|
311
|
-
expect(secrets.OP_ASSISTANT_TOKEN).
|
|
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
|
-
"
|
|
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: {
|
|
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
|
|
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
|
-
|
|
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
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
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
|
-
*
|
|
136
|
-
*
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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(
|
|
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.
|
|
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
|
-
*
|
|
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 = ["
|
|
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
|
-
*
|
|
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
|
-
}
|