@oc-forge/secret 0.1.0
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 +115 -0
- package/package.json +16 -0
- package/secret.ts +508 -0
package/README.md
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# @oc-forge/secret
|
|
2
|
+
|
|
3
|
+
**Infisical secret management CLI with local caching — for OpenClaw teams**
|
|
4
|
+
|
|
5
|
+
一个用于管理 [Infisical](https://infisical.com) secrets 的命令行工具,带本地缓存,专为 OpenClaw 团队设计。
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## 安装 / Installation
|
|
10
|
+
|
|
11
|
+
需要 [Bun](https://bun.sh) runtime(v1.0+)。
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
npm i -g @oc-forge/secret
|
|
15
|
+
# 或
|
|
16
|
+
bun add -g @oc-forge/secret
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
安装后即可使用 `secret` 命令。
|
|
20
|
+
|
|
21
|
+
---
|
|
22
|
+
|
|
23
|
+
## 配置 / Configuration
|
|
24
|
+
|
|
25
|
+
### 方式一:配置文件(推荐)
|
|
26
|
+
|
|
27
|
+
创建 `~/.config/openclaw-fleet/config.json`:
|
|
28
|
+
|
|
29
|
+
```json
|
|
30
|
+
{
|
|
31
|
+
"clientId": "your-machine-identity-client-id",
|
|
32
|
+
"clientSecret": "your-machine-identity-client-secret",
|
|
33
|
+
"projectId": "your-infisical-project-id",
|
|
34
|
+
"env": "dev"
|
|
35
|
+
}
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### 方式二:环境变量
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
export INFISICAL_CLIENT_ID="your-client-id"
|
|
42
|
+
export INFISICAL_CLIENT_SECRET="your-client-secret"
|
|
43
|
+
export INFISICAL_PROJECT_ID="your-project-id"
|
|
44
|
+
export INFISICAL_ENV="dev" # 可选,默认 dev
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
> ⚠️ 不要将实际凭证提交到代码仓库。
|
|
48
|
+
|
|
49
|
+
---
|
|
50
|
+
|
|
51
|
+
## 命令 / Commands
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
# 获取 secret(优先读取缓存)
|
|
55
|
+
secret get <KEY>
|
|
56
|
+
secret get <KEY> --fresh # 强制从 Infisical 拉取最新值
|
|
57
|
+
|
|
58
|
+
# 设置 / 更新 secret
|
|
59
|
+
secret set <KEY> <VALUE>
|
|
60
|
+
|
|
61
|
+
# 列出所有 secret keys
|
|
62
|
+
secret list
|
|
63
|
+
secret list --show # 同时显示值
|
|
64
|
+
|
|
65
|
+
# 全量同步到本地缓存
|
|
66
|
+
secret sync
|
|
67
|
+
|
|
68
|
+
# 注入所有 secrets 为环境变量并运行命令
|
|
69
|
+
secret exec -- <command> [args...]
|
|
70
|
+
|
|
71
|
+
# 帮助
|
|
72
|
+
secret --help
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 示例 / Examples
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# 获取数据库密码
|
|
79
|
+
secret get DATABASE_URL
|
|
80
|
+
|
|
81
|
+
# 设置 API key
|
|
82
|
+
secret set OPENAI_API_KEY sk-...
|
|
83
|
+
|
|
84
|
+
# 注入 secrets 运行应用
|
|
85
|
+
secret exec -- node server.js
|
|
86
|
+
|
|
87
|
+
# 同步所有 secrets 到缓存
|
|
88
|
+
secret sync
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## 缓存 / Caching
|
|
94
|
+
|
|
95
|
+
- 缓存文件:`~/.config/openclaw-fleet/cache.json`(权限 600)
|
|
96
|
+
- 默认 TTL:24 小时
|
|
97
|
+
- `secret get` 自动使用缓存;`secret exec` 在缓存过期时自动同步
|
|
98
|
+
- 可在 config.json 中设置 `ttlMs` 自定义缓存时长
|
|
99
|
+
|
|
100
|
+
---
|
|
101
|
+
|
|
102
|
+
## 认证方式 / Authentication
|
|
103
|
+
|
|
104
|
+
使用 Infisical [Universal Auth (Machine Identity)](https://infisical.com/docs/documentation/platform/identities/universal-auth) 认证。
|
|
105
|
+
|
|
106
|
+
配置步骤:
|
|
107
|
+
1. 在 Infisical 控制台创建 Machine Identity
|
|
108
|
+
2. 授予项目访问权限
|
|
109
|
+
3. 将 `clientId` 和 `clientSecret` 填入配置文件
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## License
|
|
114
|
+
|
|
115
|
+
MIT © 小橘 🍊
|
package/package.json
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@oc-forge/secret",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Infisical secret management CLI with local caching — for OpenClaw teams",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"secret": "./secret.ts"
|
|
8
|
+
},
|
|
9
|
+
"files": ["secret.ts", "README.md", "LICENSE"],
|
|
10
|
+
"keywords": ["infisical", "secret", "cli", "openclaw", "oc-forge"],
|
|
11
|
+
"author": "小橘 🍊 <xiaoju@shazhou.work>",
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"engines": {
|
|
14
|
+
"bun": ">=1.0.0"
|
|
15
|
+
}
|
|
16
|
+
}
|
package/secret.ts
ADDED
|
@@ -0,0 +1,508 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// @openclaw/secret - Infisical secret management CLI with local caching
|
|
3
|
+
// Usage: secret <command> [args]
|
|
4
|
+
|
|
5
|
+
// ─── Colors ────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const c = {
|
|
8
|
+
reset: "\x1b[0m",
|
|
9
|
+
bold: "\x1b[1m",
|
|
10
|
+
dim: "\x1b[2m",
|
|
11
|
+
red: "\x1b[31m",
|
|
12
|
+
green: "\x1b[32m",
|
|
13
|
+
yellow: "\x1b[33m",
|
|
14
|
+
blue: "\x1b[34m",
|
|
15
|
+
magenta: "\x1b[35m",
|
|
16
|
+
cyan: "\x1b[36m",
|
|
17
|
+
gray: "\x1b[90m",
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function ok(msg: string) {
|
|
21
|
+
console.log(`${c.green}✓${c.reset} ${msg}`);
|
|
22
|
+
}
|
|
23
|
+
function info(msg: string) {
|
|
24
|
+
console.log(`${c.cyan}ℹ${c.reset} ${msg}`);
|
|
25
|
+
}
|
|
26
|
+
function warn(msg: string) {
|
|
27
|
+
console.log(`${c.yellow}⚠${c.reset} ${msg}`);
|
|
28
|
+
}
|
|
29
|
+
function fail(msg: string) {
|
|
30
|
+
console.error(`${c.red}✗${c.reset} ${msg}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ─── Paths ─────────────────────────────────────────────────────────────────────
|
|
34
|
+
|
|
35
|
+
const HOME = Bun.env.HOME || "~";
|
|
36
|
+
const CONFIG_DIR = `${HOME}/.config/openclaw-fleet`;
|
|
37
|
+
const CONFIG_PATH = `${CONFIG_DIR}/config.json`;
|
|
38
|
+
const CACHE_PATH = `${CONFIG_DIR}/cache.json`;
|
|
39
|
+
|
|
40
|
+
// ─── Types ─────────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
interface Config {
|
|
43
|
+
clientId: string;
|
|
44
|
+
clientSecret: string;
|
|
45
|
+
projectId: string;
|
|
46
|
+
env: string;
|
|
47
|
+
ttlMs: number;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
interface CacheEntry {
|
|
51
|
+
value: string;
|
|
52
|
+
updatedAt: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
interface Cache {
|
|
56
|
+
secrets: Record<string, CacheEntry>;
|
|
57
|
+
lastSync: number;
|
|
58
|
+
ttlMs: number;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Config ────────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
async function loadConfig(): Promise<Config> {
|
|
64
|
+
let fileConfig: Partial<Config> = {};
|
|
65
|
+
|
|
66
|
+
const configFile = Bun.file(CONFIG_PATH);
|
|
67
|
+
if (await configFile.exists()) {
|
|
68
|
+
try {
|
|
69
|
+
fileConfig = await configFile.json();
|
|
70
|
+
} catch {
|
|
71
|
+
warn("config.json is malformed, using env vars only");
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const clientId =
|
|
76
|
+
Bun.env.INFISICAL_CLIENT_ID || fileConfig.clientId || "";
|
|
77
|
+
const clientSecret =
|
|
78
|
+
Bun.env.INFISICAL_CLIENT_SECRET || fileConfig.clientSecret || "";
|
|
79
|
+
const projectId =
|
|
80
|
+
Bun.env.INFISICAL_PROJECT_ID ||
|
|
81
|
+
fileConfig.projectId ||
|
|
82
|
+
"";
|
|
83
|
+
const env = Bun.env.INFISICAL_ENV || fileConfig.env || "dev";
|
|
84
|
+
const ttlMs = fileConfig.ttlMs || 86400000; // 24h
|
|
85
|
+
|
|
86
|
+
if (!clientId || !clientSecret) {
|
|
87
|
+
fail(
|
|
88
|
+
"Missing Infisical credentials. Set INFISICAL_CLIENT_ID/INFISICAL_CLIENT_SECRET or add them to config.json"
|
|
89
|
+
);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (!projectId) {
|
|
94
|
+
fail(
|
|
95
|
+
"Missing Infisical project ID. Set INFISICAL_PROJECT_ID or add projectId to config.json"
|
|
96
|
+
);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { clientId, clientSecret, projectId, env, ttlMs };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// ─── Cache ─────────────────────────────────────────────────────────────────────
|
|
104
|
+
|
|
105
|
+
async function loadCache(): Promise<Cache> {
|
|
106
|
+
const cacheFile = Bun.file(CACHE_PATH);
|
|
107
|
+
if (await cacheFile.exists()) {
|
|
108
|
+
try {
|
|
109
|
+
const data = await cacheFile.json();
|
|
110
|
+
if (data && typeof data.secrets === "object") {
|
|
111
|
+
return data as Cache;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
warn("Cache file corrupted, starting fresh");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return { secrets: {}, lastSync: 0, ttlMs: 86400000 };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function saveCache(cache: Cache): Promise<void> {
|
|
121
|
+
await Bun.write(CACHE_PATH, JSON.stringify(cache, null, 2));
|
|
122
|
+
// Set file permissions to 600
|
|
123
|
+
const proc = Bun.spawn(["chmod", "600", CACHE_PATH]);
|
|
124
|
+
await proc.exited;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function isCacheValid(entry: CacheEntry, ttlMs: number): boolean {
|
|
128
|
+
return Date.now() - entry.updatedAt < ttlMs;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// ─── Infisical API ─────────────────────────────────────────────────────────────
|
|
132
|
+
|
|
133
|
+
const API_BASE = "https://app.infisical.com/api";
|
|
134
|
+
|
|
135
|
+
async function authenticate(config: Config): Promise<string> {
|
|
136
|
+
const res = await fetch(`${API_BASE}/v1/auth/universal-auth/login`, {
|
|
137
|
+
method: "POST",
|
|
138
|
+
headers: { "Content-Type": "application/json" },
|
|
139
|
+
body: JSON.stringify({
|
|
140
|
+
clientId: config.clientId,
|
|
141
|
+
clientSecret: config.clientSecret,
|
|
142
|
+
}),
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
const body = await res.text();
|
|
147
|
+
if (res.status === 401 || res.status === 403) {
|
|
148
|
+
fail("Authentication failed — check your clientId/clientSecret");
|
|
149
|
+
} else {
|
|
150
|
+
fail(`Auth request failed (${res.status}): ${body}`);
|
|
151
|
+
}
|
|
152
|
+
process.exit(1);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const data = (await res.json()) as { accessToken: string };
|
|
156
|
+
return data.accessToken;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
async function fetchAllSecrets(
|
|
160
|
+
token: string,
|
|
161
|
+
config: Config
|
|
162
|
+
): Promise<Array<{ secretKey: string; secretValue: string }>> {
|
|
163
|
+
const url = `${API_BASE}/v3/secrets/raw?environment=${encodeURIComponent(config.env)}&workspaceId=${encodeURIComponent(config.projectId)}`;
|
|
164
|
+
|
|
165
|
+
const res = await fetch(url, {
|
|
166
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
if (!res.ok) {
|
|
170
|
+
const body = await res.text();
|
|
171
|
+
fail(`Failed to fetch secrets (${res.status}): ${body}`);
|
|
172
|
+
process.exit(1);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const data = (await res.json()) as {
|
|
176
|
+
secrets: Array<{ secretKey: string; secretValue: string }>;
|
|
177
|
+
};
|
|
178
|
+
return data.secrets;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function fetchOneSecret(
|
|
182
|
+
token: string,
|
|
183
|
+
config: Config,
|
|
184
|
+
key: string
|
|
185
|
+
): Promise<string> {
|
|
186
|
+
const url = `${API_BASE}/v3/secrets/raw/${encodeURIComponent(key)}?environment=${encodeURIComponent(config.env)}&workspaceId=${encodeURIComponent(config.projectId)}`;
|
|
187
|
+
|
|
188
|
+
const res = await fetch(url, {
|
|
189
|
+
headers: { Authorization: `Bearer ${token}` },
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
if (!res.ok) {
|
|
193
|
+
if (res.status === 404) {
|
|
194
|
+
fail(`Secret "${key}" not found`);
|
|
195
|
+
} else {
|
|
196
|
+
const body = await res.text();
|
|
197
|
+
fail(`Failed to fetch secret (${res.status}): ${body}`);
|
|
198
|
+
}
|
|
199
|
+
process.exit(1);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const data = (await res.json()) as {
|
|
203
|
+
secret: { secretKey: string; secretValue: string };
|
|
204
|
+
};
|
|
205
|
+
return data.secret.secretValue;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
async function upsertSecret(
|
|
209
|
+
token: string,
|
|
210
|
+
config: Config,
|
|
211
|
+
key: string,
|
|
212
|
+
value: string
|
|
213
|
+
): Promise<void> {
|
|
214
|
+
// Try PATCH first (update existing)
|
|
215
|
+
const patchUrl = `${API_BASE}/v3/secrets/raw/${encodeURIComponent(key)}`;
|
|
216
|
+
const body = JSON.stringify({
|
|
217
|
+
workspaceId: config.projectId,
|
|
218
|
+
environment: config.env,
|
|
219
|
+
secretValue: value,
|
|
220
|
+
type: "shared",
|
|
221
|
+
});
|
|
222
|
+
const headers = {
|
|
223
|
+
Authorization: `Bearer ${token}`,
|
|
224
|
+
"Content-Type": "application/json",
|
|
225
|
+
};
|
|
226
|
+
|
|
227
|
+
const patchRes = await fetch(patchUrl, {
|
|
228
|
+
method: "PATCH",
|
|
229
|
+
headers,
|
|
230
|
+
body,
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
if (patchRes.ok) return;
|
|
234
|
+
|
|
235
|
+
// If PATCH fails (404 = doesn't exist), try POST to create
|
|
236
|
+
if (patchRes.status === 400 || patchRes.status === 404) {
|
|
237
|
+
const postRes = await fetch(`${API_BASE}/v3/secrets/raw/${encodeURIComponent(key)}`, {
|
|
238
|
+
method: "POST",
|
|
239
|
+
headers,
|
|
240
|
+
body,
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
if (!postRes.ok) {
|
|
244
|
+
const errBody = await postRes.text();
|
|
245
|
+
fail(`Failed to create secret (${postRes.status}): ${errBody}`);
|
|
246
|
+
process.exit(1);
|
|
247
|
+
}
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const errBody = await patchRes.text();
|
|
252
|
+
fail(`Failed to update secret (${patchRes.status}): ${errBody}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// ─── Commands ──────────────────────────────────────────────────────────────────
|
|
257
|
+
|
|
258
|
+
async function cmdGet(key: string, fresh: boolean) {
|
|
259
|
+
const config = await loadConfig();
|
|
260
|
+
const cache = await loadCache();
|
|
261
|
+
|
|
262
|
+
// Check cache first (unless --fresh)
|
|
263
|
+
if (!fresh && cache.secrets[key] && isCacheValid(cache.secrets[key], config.ttlMs)) {
|
|
264
|
+
const val = cache.secrets[key].value;
|
|
265
|
+
console.log(val);
|
|
266
|
+
info(`${c.dim}(from cache)${c.reset}`);
|
|
267
|
+
return;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Fetch from Infisical
|
|
271
|
+
const token = await authenticate(config);
|
|
272
|
+
const value = await fetchOneSecret(token, config, key);
|
|
273
|
+
|
|
274
|
+
// Update cache
|
|
275
|
+
cache.secrets[key] = { value, updatedAt: Date.now() };
|
|
276
|
+
cache.ttlMs = config.ttlMs;
|
|
277
|
+
await saveCache(cache);
|
|
278
|
+
|
|
279
|
+
console.log(value);
|
|
280
|
+
info(`${c.dim}(from Infisical)${c.reset}`);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
async function cmdSet(key: string, value: string) {
|
|
284
|
+
const config = await loadConfig();
|
|
285
|
+
const token = await authenticate(config);
|
|
286
|
+
|
|
287
|
+
await upsertSecret(token, config, key, value);
|
|
288
|
+
|
|
289
|
+
// Update cache
|
|
290
|
+
const cache = await loadCache();
|
|
291
|
+
cache.secrets[key] = { value, updatedAt: Date.now() };
|
|
292
|
+
cache.ttlMs = config.ttlMs;
|
|
293
|
+
await saveCache(cache);
|
|
294
|
+
|
|
295
|
+
ok(`Set ${c.bold}${key}${c.reset} ✓`);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
async function cmdList(showValues: boolean) {
|
|
299
|
+
const config = await loadConfig();
|
|
300
|
+
const token = await authenticate(config);
|
|
301
|
+
const secrets = await fetchAllSecrets(token, config);
|
|
302
|
+
|
|
303
|
+
if (secrets.length === 0) {
|
|
304
|
+
warn("No secrets found");
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Sort by key
|
|
309
|
+
secrets.sort((a, b) => a.secretKey.localeCompare(b.secretKey));
|
|
310
|
+
|
|
311
|
+
console.log(
|
|
312
|
+
`\n${c.bold}${c.cyan}Secrets${c.reset} ${c.dim}(${secrets.length} total, env: ${config.env})${c.reset}\n`
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
const maxKeyLen = Math.max(...secrets.map((s) => s.secretKey.length));
|
|
316
|
+
|
|
317
|
+
for (const s of secrets) {
|
|
318
|
+
const key = s.secretKey.padEnd(maxKeyLen);
|
|
319
|
+
if (showValues) {
|
|
320
|
+
console.log(` ${c.green}${key}${c.reset} ${c.dim}=${c.reset} ${s.secretValue}`);
|
|
321
|
+
} else {
|
|
322
|
+
console.log(` ${c.green}${key}${c.reset}`);
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
console.log();
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
async function cmdSync() {
|
|
329
|
+
const config = await loadConfig();
|
|
330
|
+
const token = await authenticate(config);
|
|
331
|
+
const secrets = await fetchAllSecrets(token, config);
|
|
332
|
+
|
|
333
|
+
const cache: Cache = {
|
|
334
|
+
secrets: {},
|
|
335
|
+
lastSync: Date.now(),
|
|
336
|
+
ttlMs: config.ttlMs,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
for (const s of secrets) {
|
|
340
|
+
cache.secrets[s.secretKey] = {
|
|
341
|
+
value: s.secretValue,
|
|
342
|
+
updatedAt: Date.now(),
|
|
343
|
+
};
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
await saveCache(cache);
|
|
347
|
+
ok(`Synced ${c.bold}${secrets.length}${c.reset} secrets to cache`);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
async function cmdExec(args: string[]) {
|
|
351
|
+
if (args.length === 0) {
|
|
352
|
+
fail("Usage: secret exec -- <command> [args...]");
|
|
353
|
+
process.exit(1);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
const config = await loadConfig();
|
|
357
|
+
const cache = await loadCache();
|
|
358
|
+
|
|
359
|
+
// Check if cache is fresh enough, otherwise sync
|
|
360
|
+
const hasValidCache =
|
|
361
|
+
Object.keys(cache.secrets).length > 0 &&
|
|
362
|
+
Date.now() - cache.lastSync < config.ttlMs;
|
|
363
|
+
|
|
364
|
+
let secrets: Record<string, string>;
|
|
365
|
+
|
|
366
|
+
if (hasValidCache) {
|
|
367
|
+
secrets = Object.fromEntries(
|
|
368
|
+
Object.entries(cache.secrets).map(([k, v]) => [k, v.value])
|
|
369
|
+
);
|
|
370
|
+
info(`${c.dim}Using cached secrets${c.reset}`);
|
|
371
|
+
} else {
|
|
372
|
+
// Fetch fresh
|
|
373
|
+
const token = await authenticate(config);
|
|
374
|
+
const fetched = await fetchAllSecrets(token, config);
|
|
375
|
+
|
|
376
|
+
secrets = Object.fromEntries(
|
|
377
|
+
fetched.map((s) => [s.secretKey, s.secretValue])
|
|
378
|
+
);
|
|
379
|
+
|
|
380
|
+
// Update cache
|
|
381
|
+
const newCache: Cache = {
|
|
382
|
+
secrets: {},
|
|
383
|
+
lastSync: Date.now(),
|
|
384
|
+
ttlMs: config.ttlMs,
|
|
385
|
+
};
|
|
386
|
+
for (const s of fetched) {
|
|
387
|
+
newCache.secrets[s.secretKey] = {
|
|
388
|
+
value: s.secretValue,
|
|
389
|
+
updatedAt: Date.now(),
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
await saveCache(newCache);
|
|
393
|
+
info(`${c.dim}Synced ${fetched.length} secrets${c.reset}`);
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
// Merge secrets into env and exec
|
|
397
|
+
const env = { ...Bun.env, ...secrets };
|
|
398
|
+
|
|
399
|
+
const proc = Bun.spawn(args, {
|
|
400
|
+
env,
|
|
401
|
+
stdout: "inherit",
|
|
402
|
+
stderr: "inherit",
|
|
403
|
+
stdin: "inherit",
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const exitCode = await proc.exited;
|
|
407
|
+
process.exit(exitCode);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
// ─── Main ──────────────────────────────────────────────────────────────────────
|
|
411
|
+
|
|
412
|
+
function printUsage() {
|
|
413
|
+
console.log(`
|
|
414
|
+
${c.bold}${c.cyan}@openclaw/secret${c.reset} — Infisical secret manager with local caching
|
|
415
|
+
|
|
416
|
+
${c.bold}Usage:${c.reset}
|
|
417
|
+
secret get <KEY> [--fresh] Get a secret value (cache-first)
|
|
418
|
+
secret set <KEY> <VALUE> Set/update a secret
|
|
419
|
+
secret list [--show] List all secret keys
|
|
420
|
+
secret sync Sync all secrets to local cache
|
|
421
|
+
secret exec -- <cmd> [args] Run command with secrets as env vars
|
|
422
|
+
|
|
423
|
+
${c.bold}Config:${c.reset}
|
|
424
|
+
${c.dim}~/.config/openclaw-fleet/config.json${c.reset}
|
|
425
|
+
${c.dim}or INFISICAL_CLIENT_ID / INFISICAL_CLIENT_SECRET env vars${c.reset}
|
|
426
|
+
`);
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
async function main() {
|
|
430
|
+
const args = process.argv.slice(2);
|
|
431
|
+
|
|
432
|
+
if (args.length === 0) {
|
|
433
|
+
printUsage();
|
|
434
|
+
process.exit(0);
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
const command = args[0];
|
|
438
|
+
|
|
439
|
+
try {
|
|
440
|
+
switch (command) {
|
|
441
|
+
case "get": {
|
|
442
|
+
const key = args[1];
|
|
443
|
+
if (!key) {
|
|
444
|
+
fail("Usage: secret get <KEY> [--fresh]");
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
const fresh = args.includes("--fresh");
|
|
448
|
+
await cmdGet(key, fresh);
|
|
449
|
+
break;
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
case "set": {
|
|
453
|
+
const key = args[1];
|
|
454
|
+
const value = args[2];
|
|
455
|
+
if (!key || value === undefined) {
|
|
456
|
+
fail("Usage: secret set <KEY> <VALUE>");
|
|
457
|
+
process.exit(1);
|
|
458
|
+
}
|
|
459
|
+
await cmdSet(key, value);
|
|
460
|
+
break;
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
case "list": {
|
|
464
|
+
const showValues = args.includes("--show");
|
|
465
|
+
await cmdList(showValues);
|
|
466
|
+
break;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
case "sync": {
|
|
470
|
+
await cmdSync();
|
|
471
|
+
break;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
case "exec": {
|
|
475
|
+
// Find "--" separator
|
|
476
|
+
const dashIdx = args.indexOf("--");
|
|
477
|
+
const cmdArgs = dashIdx >= 0 ? args.slice(dashIdx + 1) : args.slice(1);
|
|
478
|
+
await cmdExec(cmdArgs);
|
|
479
|
+
break;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
case "help":
|
|
483
|
+
case "--help":
|
|
484
|
+
case "-h": {
|
|
485
|
+
printUsage();
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
default:
|
|
490
|
+
fail(`Unknown command: ${command}`);
|
|
491
|
+
printUsage();
|
|
492
|
+
process.exit(1);
|
|
493
|
+
}
|
|
494
|
+
} catch (err: unknown) {
|
|
495
|
+
if (err instanceof TypeError && String(err).includes("fetch")) {
|
|
496
|
+
fail("Network error — check your internet connection");
|
|
497
|
+
process.exit(1);
|
|
498
|
+
}
|
|
499
|
+
if (err instanceof Error) {
|
|
500
|
+
fail(err.message);
|
|
501
|
+
} else {
|
|
502
|
+
fail(String(err));
|
|
503
|
+
}
|
|
504
|
+
process.exit(1);
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
main();
|