@openpalm/lib 0.11.0-beta.9 → 0.11.0-rc.18
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 +1 -1
- package/package.json +1 -1
- package/src/control-plane/akm-sources.test.ts +206 -0
- package/src/control-plane/akm-sources.ts +234 -0
- package/src/control-plane/akm-user-env.test.ts +142 -0
- package/src/control-plane/akm-user-env.ts +167 -0
- package/src/control-plane/backup.ts +14 -5
- package/src/control-plane/channels.ts +48 -29
- package/src/control-plane/cleanup-guardrails.test.ts +1 -1
- package/src/control-plane/compose-args.test.ts +69 -30
- package/src/control-plane/compose-args.ts +62 -8
- package/src/control-plane/config-persistence.ts +102 -136
- package/src/control-plane/core-assets.ts +45 -60
- package/src/control-plane/defaults.ts +16 -0
- package/src/control-plane/docker.ts +15 -14
- package/src/control-plane/env.test.ts +10 -10
- package/src/control-plane/env.ts +16 -1
- package/src/control-plane/extends-support.test.ts +8 -8
- package/src/control-plane/fs-atomic.ts +15 -0
- package/src/control-plane/home.ts +34 -46
- package/src/control-plane/host-akm-sharing.test.ts +145 -0
- package/src/control-plane/host-akm-sharing.ts +129 -0
- package/src/control-plane/host-opencode.test.ts +82 -10
- package/src/control-plane/host-opencode.ts +42 -13
- package/src/control-plane/install-edge-cases.test.ts +100 -136
- package/src/control-plane/install-lock.ts +7 -7
- package/src/control-plane/lifecycle.ts +45 -40
- package/src/control-plane/markdown-task.ts +30 -50
- package/src/control-plane/migrations.test.ts +272 -0
- package/src/control-plane/migrations.ts +423 -0
- package/src/control-plane/opencode-client.ts +1 -1
- package/src/control-plane/paths.ts +61 -46
- package/src/control-plane/profile-ids.ts +21 -0
- package/src/control-plane/provider-models.ts +3 -3
- package/src/control-plane/registry.test.ts +107 -90
- package/src/control-plane/registry.ts +301 -110
- package/src/control-plane/rollback.ts +8 -38
- package/src/control-plane/scheduler.ts +10 -7
- package/src/control-plane/secret-audit.test.ts +159 -0
- package/src/control-plane/secret-audit.ts +255 -0
- package/src/control-plane/secret-mappings.ts +2 -2
- package/src/control-plane/secrets-files.test.ts +99 -0
- package/src/control-plane/secrets-files.ts +113 -0
- package/src/control-plane/secrets.ts +113 -86
- package/src/control-plane/setup-config.schema.json +1 -1
- package/src/control-plane/setup-status.ts +6 -11
- package/src/control-plane/setup.test.ts +137 -61
- package/src/control-plane/setup.ts +82 -63
- package/src/control-plane/skeleton-guardrail.test.ts +66 -56
- package/src/control-plane/spec-to-env.test.ts +63 -26
- package/src/control-plane/spec-to-env.ts +51 -14
- package/src/control-plane/task-files.test.ts +45 -0
- package/src/control-plane/task-files.ts +51 -0
- package/src/control-plane/types.ts +2 -4
- package/src/control-plane/ui-assets.test.ts +333 -0
- package/src/control-plane/ui-assets.ts +290 -142
- package/src/control-plane/validate.ts +13 -15
- package/src/index.ts +96 -26
- package/src/control-plane/akm-vault.test.ts +0 -105
- package/src/control-plane/akm-vault.ts +0 -311
- package/src/control-plane/core-assets.test.ts +0 -104
- package/src/control-plane/migrate-0110.test.ts +0 -177
- package/src/control-plane/migrate-0110.ts +0 -99
- package/src/control-plane/registry-components.test.ts +0 -391
- package/src/control-plane/stack-spec.test.ts +0 -94
- package/src/control-plane/stack-spec.ts +0 -67
|
@@ -1,104 +0,0 @@
|
|
|
1
|
-
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
2
|
-
import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from "node:fs";
|
|
3
|
-
import { tmpdir } from "node:os";
|
|
4
|
-
import { join } from "node:path";
|
|
5
|
-
import { seedStashAssets } from "./core-assets.js";
|
|
6
|
-
|
|
7
|
-
describe("seedStashAssets", () => {
|
|
8
|
-
let homeDir: string;
|
|
9
|
-
const originalHome = process.env.OP_HOME;
|
|
10
|
-
|
|
11
|
-
beforeEach(() => {
|
|
12
|
-
homeDir = mkdtempSync(join(tmpdir(), "stash-seed-test-"));
|
|
13
|
-
process.env.OP_HOME = homeDir;
|
|
14
|
-
mkdirSync(join(homeDir, "stash"), { recursive: true });
|
|
15
|
-
});
|
|
16
|
-
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
process.env.OP_HOME = originalHome;
|
|
19
|
-
// Restore writable mode in case a test chmod'd the stash dir.
|
|
20
|
-
try {
|
|
21
|
-
chmodSync(join(homeDir, "stash"), 0o755);
|
|
22
|
-
} catch {
|
|
23
|
-
// ignore — dir may not exist
|
|
24
|
-
}
|
|
25
|
-
rmSync(homeDir, { recursive: true, force: true });
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it("writes every seed under stash/ on first run", () => {
|
|
29
|
-
const seeds = {
|
|
30
|
-
"skills/test-skill/SKILL.md": "---\nname: test-skill\ntype: skill\n---\nhello\n",
|
|
31
|
-
"commands/test-cmd.md": "---\nname: test-cmd\ntype: command\n---\nrun me\n",
|
|
32
|
-
};
|
|
33
|
-
const written = seedStashAssets(seeds);
|
|
34
|
-
|
|
35
|
-
expect(written.sort()).toEqual(Object.keys(seeds).sort());
|
|
36
|
-
for (const [rel, content] of Object.entries(seeds)) {
|
|
37
|
-
const target = join(homeDir, "stash", rel);
|
|
38
|
-
expect(existsSync(target)).toBe(true);
|
|
39
|
-
expect(readFileSync(target, "utf-8")).toBe(content);
|
|
40
|
-
}
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it("does not overwrite existing files (user edits win)", () => {
|
|
44
|
-
const seeds = { "skills/keep-mine/SKILL.md": "ORIGINAL SEED\n" };
|
|
45
|
-
const userEdit = "USER EDIT — must not be overwritten\n";
|
|
46
|
-
|
|
47
|
-
// Simulate a previous install: seed first.
|
|
48
|
-
seedStashAssets(seeds);
|
|
49
|
-
const target = join(homeDir, "stash/skills/keep-mine/SKILL.md");
|
|
50
|
-
expect(readFileSync(target, "utf-8")).toBe("ORIGINAL SEED\n");
|
|
51
|
-
|
|
52
|
-
// User edits the file.
|
|
53
|
-
writeFileSync(target, userEdit);
|
|
54
|
-
|
|
55
|
-
// Re-run: must return [] and leave the user's content intact.
|
|
56
|
-
const written = seedStashAssets(seeds);
|
|
57
|
-
expect(written).toEqual([]);
|
|
58
|
-
expect(readFileSync(target, "utf-8")).toBe(userEdit);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("creates nested directories under stash/ as needed", () => {
|
|
62
|
-
const seeds = { "skills/deep/nested/asset/SKILL.md": "x" };
|
|
63
|
-
seedStashAssets(seeds);
|
|
64
|
-
expect(existsSync(join(homeDir, "stash/skills/deep/nested/asset/SKILL.md"))).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it("returns an empty list when called with no seeds", () => {
|
|
68
|
-
expect(seedStashAssets({})).toEqual([]);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it("rejects seed keys that escape the stash directory", () => {
|
|
72
|
-
// Path-traversal guard: ../ sequences in keys must throw rather than
|
|
73
|
-
// silently writing outside stash/.
|
|
74
|
-
expect(() =>
|
|
75
|
-
seedStashAssets({ "../../etc/cron.d/evil": "owned\n" }),
|
|
76
|
-
).toThrow(/escapes stash dir/);
|
|
77
|
-
|
|
78
|
-
// Confirm the malicious payload was NOT written anywhere relative to
|
|
79
|
-
// the temp home.
|
|
80
|
-
expect(existsSync(join(homeDir, "..", "..", "etc", "cron.d", "evil"))).toBe(false);
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it("rejects seed keys that traverse through the stash dir back out", () => {
|
|
84
|
-
expect(() =>
|
|
85
|
-
seedStashAssets({ "skills/../../../escape.md": "x" }),
|
|
86
|
-
).toThrow(/escapes stash dir/);
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it("surfaces errors when the stash directory is read-only", () => {
|
|
90
|
-
// Skip when running as root (chmod is a no-op for the superuser).
|
|
91
|
-
const uid = process.getuid?.();
|
|
92
|
-
if (uid === 0) return;
|
|
93
|
-
|
|
94
|
-
const stashDir = join(homeDir, "stash");
|
|
95
|
-
chmodSync(stashDir, 0o555);
|
|
96
|
-
try {
|
|
97
|
-
expect(() =>
|
|
98
|
-
seedStashAssets({ "skills/readonly/SKILL.md": "nope\n" }),
|
|
99
|
-
).toThrow();
|
|
100
|
-
} finally {
|
|
101
|
-
chmodSync(stashDir, 0o755);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
});
|
|
@@ -1,177 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Tests for the 0.11.0 auth-migration shim.
|
|
3
|
-
*/
|
|
4
|
-
import { describe, expect, it, beforeEach, afterEach } from "bun:test";
|
|
5
|
-
import {
|
|
6
|
-
existsSync,
|
|
7
|
-
mkdirSync,
|
|
8
|
-
mkdtempSync,
|
|
9
|
-
readFileSync,
|
|
10
|
-
rmSync,
|
|
11
|
-
statSync,
|
|
12
|
-
writeFileSync,
|
|
13
|
-
chmodSync,
|
|
14
|
-
} from "node:fs";
|
|
15
|
-
import { tmpdir } from "node:os";
|
|
16
|
-
import { join } from "node:path";
|
|
17
|
-
import { migrateAuth0110 } from "./migrate-0110.js";
|
|
18
|
-
import type { ControlPlaneState } from "./types.js";
|
|
19
|
-
|
|
20
|
-
function makeState(homeDir: string): ControlPlaneState {
|
|
21
|
-
return {
|
|
22
|
-
homeDir,
|
|
23
|
-
configDir: join(homeDir, "config"),
|
|
24
|
-
stashDir: join(homeDir, "stash"),
|
|
25
|
-
workspaceDir: join(homeDir, "workspace"),
|
|
26
|
-
cacheDir: join(homeDir, "cache"),
|
|
27
|
-
stateDir: join(homeDir, "state"),
|
|
28
|
-
stackDir: join(homeDir, "config", "stack"),
|
|
29
|
-
services: {},
|
|
30
|
-
artifacts: { compose: "" },
|
|
31
|
-
artifactMeta: [],
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
function seedStackEnv(stackDir: string, content: string): string {
|
|
36
|
-
mkdirSync(stackDir, { recursive: true });
|
|
37
|
-
const path = join(stackDir, "stack.env");
|
|
38
|
-
writeFileSync(path, content, { encoding: "utf-8", mode: 0o600 });
|
|
39
|
-
chmodSync(path, 0o600);
|
|
40
|
-
return path;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
describe("migrateAuth0110", () => {
|
|
44
|
-
let homeDir: string;
|
|
45
|
-
|
|
46
|
-
beforeEach(() => {
|
|
47
|
-
homeDir = mkdtempSync(join(tmpdir(), "openpalm-migrate-0110-"));
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
afterEach(() => {
|
|
51
|
-
rmSync(homeDir, { recursive: true, force: true });
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it("no-ops on a fresh install (no stack.env)", () => {
|
|
55
|
-
const state = makeState(homeDir);
|
|
56
|
-
const result = migrateAuth0110(state);
|
|
57
|
-
expect(result.migrated).toBe(false);
|
|
58
|
-
expect(result.reason).toContain("fresh install");
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it("promotes OP_UI_TOKEN → OP_UI_LOGIN_PASSWORD and removes legacy keys", () => {
|
|
62
|
-
const state = makeState(homeDir);
|
|
63
|
-
const stackEnvPath = seedStackEnv(
|
|
64
|
-
state.stackDir,
|
|
65
|
-
[
|
|
66
|
-
"# header",
|
|
67
|
-
"OP_UI_TOKEN=legacy-token-value",
|
|
68
|
-
"OP_ASSISTANT_TOKEN=some-assistant-token",
|
|
69
|
-
"OP_OPENCODE_PASSWORD=opencode-secret",
|
|
70
|
-
"",
|
|
71
|
-
].join("\n"),
|
|
72
|
-
);
|
|
73
|
-
|
|
74
|
-
const result = migrateAuth0110(state);
|
|
75
|
-
expect(result.migrated).toBe(true);
|
|
76
|
-
expect(result.reason).toContain("promoted OP_UI_TOKEN");
|
|
77
|
-
expect(result.reason).toContain("removed OP_UI_TOKEN");
|
|
78
|
-
expect(result.reason).toContain("removed OP_ASSISTANT_TOKEN");
|
|
79
|
-
|
|
80
|
-
const after = readFileSync(stackEnvPath, "utf-8");
|
|
81
|
-
expect(after).toContain("OP_UI_LOGIN_PASSWORD=legacy-token-value");
|
|
82
|
-
expect(after).not.toMatch(/^OP_UI_TOKEN=/m);
|
|
83
|
-
expect(after).not.toMatch(/^OP_ASSISTANT_TOKEN=/m);
|
|
84
|
-
// Unrelated keys preserved
|
|
85
|
-
expect(after).toContain("OP_OPENCODE_PASSWORD=opencode-secret");
|
|
86
|
-
|
|
87
|
-
// Perms preserved
|
|
88
|
-
expect(statSync(stackEnvPath).mode & 0o777).toBe(0o600);
|
|
89
|
-
|
|
90
|
-
// Migration log appended
|
|
91
|
-
const logPath = join(state.stateDir, "logs", "migration-0.11.0.log");
|
|
92
|
-
expect(existsSync(logPath)).toBe(true);
|
|
93
|
-
const log = readFileSync(logPath, "utf-8");
|
|
94
|
-
expect(log).toContain("migrate-auth-0110");
|
|
95
|
-
expect(log).toContain("promoted OP_UI_TOKEN");
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it("does not overwrite an existing OP_UI_LOGIN_PASSWORD", () => {
|
|
99
|
-
const state = makeState(homeDir);
|
|
100
|
-
const stackEnvPath = seedStackEnv(
|
|
101
|
-
state.stackDir,
|
|
102
|
-
[
|
|
103
|
-
"OP_UI_LOGIN_PASSWORD=new-password",
|
|
104
|
-
"OP_UI_TOKEN=legacy-value",
|
|
105
|
-
"",
|
|
106
|
-
].join("\n"),
|
|
107
|
-
);
|
|
108
|
-
|
|
109
|
-
const result = migrateAuth0110(state);
|
|
110
|
-
expect(result.migrated).toBe(true);
|
|
111
|
-
expect(result.reason).not.toContain("promoted");
|
|
112
|
-
expect(result.reason).toContain("removed OP_UI_TOKEN");
|
|
113
|
-
|
|
114
|
-
const after = readFileSync(stackEnvPath, "utf-8");
|
|
115
|
-
expect(after).toContain("OP_UI_LOGIN_PASSWORD=new-password");
|
|
116
|
-
expect(after).not.toMatch(/^OP_UI_TOKEN=/m);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it("removes OP_ASSISTANT_TOKEN even when only it is present", () => {
|
|
120
|
-
const state = makeState(homeDir);
|
|
121
|
-
const stackEnvPath = seedStackEnv(
|
|
122
|
-
state.stackDir,
|
|
123
|
-
[
|
|
124
|
-
"OP_UI_LOGIN_PASSWORD=pw",
|
|
125
|
-
"OP_ASSISTANT_TOKEN=stale",
|
|
126
|
-
"",
|
|
127
|
-
].join("\n"),
|
|
128
|
-
);
|
|
129
|
-
|
|
130
|
-
const result = migrateAuth0110(state);
|
|
131
|
-
expect(result.migrated).toBe(true);
|
|
132
|
-
expect(result.reason).toContain("removed OP_ASSISTANT_TOKEN");
|
|
133
|
-
expect(readFileSync(stackEnvPath, "utf-8")).not.toMatch(/^OP_ASSISTANT_TOKEN=/m);
|
|
134
|
-
});
|
|
135
|
-
|
|
136
|
-
it("is idempotent: second run reports already-migrated", () => {
|
|
137
|
-
const state = makeState(homeDir);
|
|
138
|
-
seedStackEnv(
|
|
139
|
-
state.stackDir,
|
|
140
|
-
[
|
|
141
|
-
"OP_UI_TOKEN=t",
|
|
142
|
-
"OP_ASSISTANT_TOKEN=t2",
|
|
143
|
-
"",
|
|
144
|
-
].join("\n"),
|
|
145
|
-
);
|
|
146
|
-
|
|
147
|
-
const first = migrateAuth0110(state);
|
|
148
|
-
expect(first.migrated).toBe(true);
|
|
149
|
-
|
|
150
|
-
const second = migrateAuth0110(state);
|
|
151
|
-
expect(second.migrated).toBe(false);
|
|
152
|
-
expect(second.reason).toContain("already migrated");
|
|
153
|
-
});
|
|
154
|
-
|
|
155
|
-
it("treats an empty OP_UI_TOKEN value as not-set (no promotion)", () => {
|
|
156
|
-
const state = makeState(homeDir);
|
|
157
|
-
const stackEnvPath = seedStackEnv(
|
|
158
|
-
state.stackDir,
|
|
159
|
-
[
|
|
160
|
-
"OP_UI_TOKEN=",
|
|
161
|
-
"OP_ASSISTANT_TOKEN=foo",
|
|
162
|
-
"",
|
|
163
|
-
].join("\n"),
|
|
164
|
-
);
|
|
165
|
-
|
|
166
|
-
const result = migrateAuth0110(state);
|
|
167
|
-
expect(result.migrated).toBe(true);
|
|
168
|
-
// Empty-string OP_UI_TOKEN should NOT be promoted as a password.
|
|
169
|
-
expect(result.reason).not.toContain("promoted");
|
|
170
|
-
|
|
171
|
-
const after = readFileSync(stackEnvPath, "utf-8");
|
|
172
|
-
// The empty OP_UI_TOKEN line is still removed.
|
|
173
|
-
expect(after).not.toMatch(/^OP_UI_TOKEN=/m);
|
|
174
|
-
// No OP_UI_LOGIN_PASSWORD added (would be an empty value).
|
|
175
|
-
expect(after).not.toMatch(/^OP_UI_LOGIN_PASSWORD=/m);
|
|
176
|
-
});
|
|
177
|
-
});
|
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* One-shot migration for the 0.11.0 auth refactor.
|
|
3
|
-
*
|
|
4
|
-
* Existing installs have OP_UI_TOKEN and OP_ASSISTANT_TOKEN in
|
|
5
|
-
* config/stack/stack.env. The 0.11.0 refactor (auth-and-proxy-refactor-plan.md)
|
|
6
|
-
* replaces them with a single OP_UI_LOGIN_PASSWORD. If we don't migrate,
|
|
7
|
-
* operators get locked out the moment they run the new UI build because the
|
|
8
|
-
* login route compares the cookie against process.env.OP_UI_LOGIN_PASSWORD,
|
|
9
|
-
* which is empty on existing installs.
|
|
10
|
-
*
|
|
11
|
-
* Migration logic (idempotent):
|
|
12
|
-
* - If OP_UI_LOGIN_PASSWORD is unset AND OP_UI_TOKEN is set, copy
|
|
13
|
-
* OP_UI_TOKEN's value into OP_UI_LOGIN_PASSWORD.
|
|
14
|
-
* - Remove OP_UI_TOKEN and OP_ASSISTANT_TOKEN from stack.env (they're
|
|
15
|
-
* no longer used).
|
|
16
|
-
* - Append a one-line summary to state/logs/migration-0.11.0.log.
|
|
17
|
-
* - If OP_UI_LOGIN_PASSWORD is already set, leave it alone — the operator
|
|
18
|
-
* already migrated or set up fresh.
|
|
19
|
-
*
|
|
20
|
-
* Called from ensureSecrets so it runs before any auth-required code path
|
|
21
|
-
* gets a chance to see the half-migrated state.
|
|
22
|
-
*/
|
|
23
|
-
import {
|
|
24
|
-
existsSync,
|
|
25
|
-
readFileSync,
|
|
26
|
-
writeFileSync,
|
|
27
|
-
chmodSync,
|
|
28
|
-
appendFileSync,
|
|
29
|
-
mkdirSync,
|
|
30
|
-
} from "node:fs";
|
|
31
|
-
import { dirname } from "node:path";
|
|
32
|
-
import { parseEnvContent, removeEnvKey, upsertEnvValue } from "./env.js";
|
|
33
|
-
import { migration0110LogPath } from "./paths.js";
|
|
34
|
-
import type { ControlPlaneState } from "./types.js";
|
|
35
|
-
|
|
36
|
-
export type MigrateAuth0110Result = {
|
|
37
|
-
/** True if any change was written to stack.env. */
|
|
38
|
-
migrated: boolean;
|
|
39
|
-
/** Human-readable description of what changed (or why nothing did). */
|
|
40
|
-
reason: string;
|
|
41
|
-
};
|
|
42
|
-
|
|
43
|
-
export function migrateAuth0110(state: ControlPlaneState): MigrateAuth0110Result {
|
|
44
|
-
const stackEnvPath = `${state.stackDir}/stack.env`;
|
|
45
|
-
if (!existsSync(stackEnvPath)) {
|
|
46
|
-
return { migrated: false, reason: "no stack.env yet (fresh install)" };
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const before = readFileSync(stackEnvPath, "utf-8");
|
|
50
|
-
const parsed = parseEnvContent(before);
|
|
51
|
-
const hasLoginPw = typeof parsed.OP_UI_LOGIN_PASSWORD === "string" && parsed.OP_UI_LOGIN_PASSWORD.length > 0;
|
|
52
|
-
const hasUiToken = typeof parsed.OP_UI_TOKEN === "string" && parsed.OP_UI_TOKEN.length > 0;
|
|
53
|
-
const hasAssistantToken = "OP_ASSISTANT_TOKEN" in parsed;
|
|
54
|
-
const hasUiTokenLine = "OP_UI_TOKEN" in parsed;
|
|
55
|
-
|
|
56
|
-
if (hasLoginPw && !hasUiTokenLine && !hasAssistantToken) {
|
|
57
|
-
return { migrated: false, reason: "already migrated" };
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
let content = before;
|
|
61
|
-
const changes: string[] = [];
|
|
62
|
-
|
|
63
|
-
if (!hasLoginPw && hasUiToken) {
|
|
64
|
-
content = upsertEnvValue(content, "OP_UI_LOGIN_PASSWORD", parsed.OP_UI_TOKEN);
|
|
65
|
-
changes.push("promoted OP_UI_TOKEN → OP_UI_LOGIN_PASSWORD");
|
|
66
|
-
}
|
|
67
|
-
if (hasUiTokenLine) {
|
|
68
|
-
content = removeEnvKey(content, "OP_UI_TOKEN");
|
|
69
|
-
changes.push("removed OP_UI_TOKEN");
|
|
70
|
-
}
|
|
71
|
-
if (hasAssistantToken) {
|
|
72
|
-
content = removeEnvKey(content, "OP_ASSISTANT_TOKEN");
|
|
73
|
-
changes.push("removed OP_ASSISTANT_TOKEN");
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
if (changes.length === 0) {
|
|
77
|
-
return { migrated: false, reason: "no changes needed" };
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
// Preserve the 0600 mode the existing file should already have.
|
|
81
|
-
writeFileSync(stackEnvPath, content, { encoding: "utf-8", mode: 0o600 });
|
|
82
|
-
try { chmodSync(stackEnvPath, 0o600); } catch { /* best-effort */ }
|
|
83
|
-
|
|
84
|
-
// Best-effort audit line. The migration log is small and append-only;
|
|
85
|
-
// if it fails (perm error, fs full), we don't roll back the migration.
|
|
86
|
-
try {
|
|
87
|
-
const logPath = migration0110LogPath(state);
|
|
88
|
-
mkdirSync(dirname(logPath), { recursive: true });
|
|
89
|
-
appendFileSync(
|
|
90
|
-
logPath,
|
|
91
|
-
`${new Date().toISOString()} migrate-auth-0110 ${changes.join("; ")}\n`,
|
|
92
|
-
"utf-8",
|
|
93
|
-
);
|
|
94
|
-
} catch {
|
|
95
|
-
/* best-effort */
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
return { migrated: true, reason: changes.join("; ") };
|
|
99
|
-
}
|