openpalm 0.9.1 → 0.9.2
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 +2 -2
- package/src/main.test.ts +49 -0
- package/src/main.ts +42 -12
package/package.json
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "openpalm",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"description": "OpenPalm CLI — install and manage a self-hosted OpenPalm stack",
|
|
7
|
-
|
|
7
|
+
"repository": {
|
|
8
8
|
"type": "git",
|
|
9
9
|
"url": "https://github.com/itlackey/openpalm.git",
|
|
10
10
|
"directory": "packages/cli"
|
package/src/main.test.ts
CHANGED
|
@@ -40,6 +40,8 @@ describe('cli main', () => {
|
|
|
40
40
|
const originalDataHome = process.env.OPENPALM_DATA_HOME;
|
|
41
41
|
const originalStateHome = process.env.OPENPALM_STATE_HOME;
|
|
42
42
|
const originalWorkDir = process.env.OPENPALM_WORK_DIR;
|
|
43
|
+
const originalAdminToken = process.env.ADMIN_TOKEN;
|
|
44
|
+
const originalOpenPalmAdminToken = process.env.OPENPALM_ADMIN_TOKEN;
|
|
43
45
|
|
|
44
46
|
afterEach(() => {
|
|
45
47
|
globalThis.fetch = originalFetch;
|
|
@@ -49,6 +51,8 @@ describe('cli main', () => {
|
|
|
49
51
|
process.env.OPENPALM_DATA_HOME = originalDataHome;
|
|
50
52
|
process.env.OPENPALM_STATE_HOME = originalStateHome;
|
|
51
53
|
process.env.OPENPALM_WORK_DIR = originalWorkDir;
|
|
54
|
+
process.env.ADMIN_TOKEN = originalAdminToken;
|
|
55
|
+
process.env.OPENPALM_ADMIN_TOKEN = originalOpenPalmAdminToken;
|
|
52
56
|
});
|
|
53
57
|
|
|
54
58
|
it('calls containers pull for update', async () => {
|
|
@@ -64,6 +68,51 @@ describe('cli main', () => {
|
|
|
64
68
|
expect(calls).toEqual(['http://localhost:8100/admin/containers/pull']);
|
|
65
69
|
});
|
|
66
70
|
|
|
71
|
+
it('uses ADMIN_TOKEN from the environment for admin requests', async () => {
|
|
72
|
+
const adminTokens: string[] = [];
|
|
73
|
+
process.env.ADMIN_TOKEN = 'env-admin-token';
|
|
74
|
+
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
75
|
+
|
|
76
|
+
globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
|
|
77
|
+
const headers = new Headers(init?.headers);
|
|
78
|
+
adminTokens.push(headers.get('X-Admin-Token') ?? '');
|
|
79
|
+
return new Response('{"ok":true}', { status: 200 });
|
|
80
|
+
}) as typeof fetch;
|
|
81
|
+
console.log = mock(() => {}) as typeof console.log;
|
|
82
|
+
|
|
83
|
+
await main(['update']);
|
|
84
|
+
|
|
85
|
+
expect(adminTokens).toEqual(['env-admin-token']);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('falls back to the legacy parent secrets.env for admin requests', async () => {
|
|
89
|
+
const base = mkdtempSync(join(tmpdir(), 'openpalm-config-'));
|
|
90
|
+
const configHome = join(base, 'openpalm');
|
|
91
|
+
const adminTokens: string[] = [];
|
|
92
|
+
|
|
93
|
+
mkdirSync(configHome, { recursive: true });
|
|
94
|
+
writeFileSync(join(configHome, 'secrets.env'), 'ADMIN_TOKEN=\n');
|
|
95
|
+
writeFileSync(join(base, 'secrets.env'), 'export ADMIN_TOKEN="legacy-admin-token"\n');
|
|
96
|
+
|
|
97
|
+
process.env.OPENPALM_CONFIG_HOME = configHome;
|
|
98
|
+
delete process.env.ADMIN_TOKEN;
|
|
99
|
+
delete process.env.OPENPALM_ADMIN_TOKEN;
|
|
100
|
+
|
|
101
|
+
globalThis.fetch = mock(async (_input: string | URL, init?: RequestInit) => {
|
|
102
|
+
const headers = new Headers(init?.headers);
|
|
103
|
+
adminTokens.push(headers.get('X-Admin-Token') ?? '');
|
|
104
|
+
return new Response('{"ok":true}', { status: 200 });
|
|
105
|
+
}) as typeof fetch;
|
|
106
|
+
console.log = mock(() => {}) as typeof console.log;
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
await main(['update']);
|
|
110
|
+
expect(adminTokens).toEqual(['legacy-admin-token']);
|
|
111
|
+
} finally {
|
|
112
|
+
rmSync(base, { recursive: true, force: true });
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
67
116
|
it('calls admin install when stack is already running', async () => {
|
|
68
117
|
const calls: string[] = [];
|
|
69
118
|
globalThis.fetch = mock(async (input: string | URL) => {
|
package/src/main.ts
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { homedir, tmpdir } from 'node:os';
|
|
3
|
-
import { join } from 'node:path';
|
|
3
|
+
import { basename, dirname, join } from 'node:path';
|
|
4
4
|
import { mkdir, unlink, copyFile, mkdtemp, rm } from 'node:fs/promises';
|
|
5
5
|
import cliPkg from '../package.json' with { type: 'json' };
|
|
6
6
|
|
|
7
7
|
const ADMIN_URL = process.env.OPENPALM_ADMIN_API_URL || 'http://localhost:8100';
|
|
8
8
|
const REPO_OWNER = 'itlackey';
|
|
9
9
|
const REPO_NAME = 'openpalm';
|
|
10
|
+
const EXPORT_ENV_PREFIX = 'export ';
|
|
10
11
|
|
|
11
12
|
const IS_WINDOWS = process.platform === 'win32';
|
|
12
13
|
|
|
@@ -134,25 +135,54 @@ async function loadAdminToken(): Promise<string> {
|
|
|
134
135
|
return process.env.OPENPALM_ADMIN_TOKEN;
|
|
135
136
|
}
|
|
136
137
|
|
|
137
|
-
|
|
138
|
+
if (process.env.ADMIN_TOKEN) {
|
|
139
|
+
return process.env.ADMIN_TOKEN;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const configHome = defaultConfigHome();
|
|
143
|
+
const secretsPaths = [join(configHome, 'secrets.env')];
|
|
144
|
+
if (basename(configHome) === 'openpalm') {
|
|
145
|
+
secretsPaths.push(join(dirname(configHome), 'secrets.env'));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
for (const secretsPath of secretsPaths) {
|
|
149
|
+
const token = await readAdminTokenFromFile(secretsPath);
|
|
150
|
+
if (token) return token;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return '';
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
async function readAdminTokenFromFile(secretsPath: string): Promise<string | null> {
|
|
138
157
|
try {
|
|
139
158
|
const text = await Bun.file(secretsPath).text();
|
|
140
|
-
for (const
|
|
159
|
+
for (const rawLine of text.split('\n')) {
|
|
160
|
+
const line = rawLine.trim();
|
|
141
161
|
if (!line || line.startsWith('#')) continue;
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
162
|
+
const lineWithoutExportPrefix = line.startsWith(EXPORT_ENV_PREFIX)
|
|
163
|
+
? line.slice(EXPORT_ENV_PREFIX.length).trimStart()
|
|
164
|
+
: line;
|
|
165
|
+
const [key, ...rest] = lineWithoutExportPrefix.split('=');
|
|
166
|
+
if (key !== 'ADMIN_TOKEN') continue;
|
|
167
|
+
const value = unwrapQuotedEnvValue(rest.join('=').trim());
|
|
168
|
+
if (!value) return null;
|
|
169
|
+
return value;
|
|
150
170
|
}
|
|
151
171
|
} catch {
|
|
152
172
|
// Best effort only.
|
|
153
173
|
}
|
|
154
174
|
|
|
155
|
-
return
|
|
175
|
+
return null;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
function unwrapQuotedEnvValue(value: string): string {
|
|
179
|
+
const isDoubleQuoted = value.startsWith('"') && value.endsWith('"');
|
|
180
|
+
const isSingleQuoted = value.startsWith('\'') && value.endsWith('\'');
|
|
181
|
+
if ((isDoubleQuoted || isSingleQuoted) && value.length >= 2) {
|
|
182
|
+
return value.slice(1, -1);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return value;
|
|
156
186
|
}
|
|
157
187
|
|
|
158
188
|
async function adminRequest(path: string, init?: RequestInit): Promise<unknown> {
|