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 CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "openpalm",
3
- "version": "0.9.1",
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
- "repository": {
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
- const secretsPath = join(defaultConfigHome(), 'secrets.env');
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 line of text.split('\n')) {
159
+ for (const rawLine of text.split('\n')) {
160
+ const line = rawLine.trim();
141
161
  if (!line || line.startsWith('#')) continue;
142
- const [key, ...rest] = line.split('=');
143
- if (key === 'ADMIN_TOKEN') {
144
- const value = rest.join('=').trim();
145
- if (value.startsWith('"') && value.endsWith('"') && value.length >= 2) {
146
- return value.slice(1, -1);
147
- }
148
- return value;
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> {