@aopslabs/aops 0.2.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/LICENSE +19 -0
- package/NOTICE +7 -0
- package/README.md +203 -0
- package/assets/agent-assets/core/SKILL.md +49 -0
- package/assets/agent-assets/core/references/agentspace/SKILL.md +78 -0
- package/assets/agent-assets/core/references/aops-cli-core/SKILL.md +108 -0
- package/assets/agent-assets/core/references/chatv3/SKILL.md +48 -0
- package/assets/agent-assets/core/references/collaborative-work/SKILL.md +85 -0
- package/assets/agent-assets/core/references/discuss/SKILL.md +66 -0
- package/assets/agent-assets/core/references/docman/SKILL.md +69 -0
- package/assets/agent-assets/core/references/hosted-chat/SKILL.md +64 -0
- package/assets/agent-assets/core/references/mission/SKILL.md +54 -0
- package/assets/agent-assets/core/references/projectman/SKILL.md +86 -0
- package/assets/agent-assets/core/references/sys/SKILL.md +36 -0
- package/assets/agent-assets/core/references/view/SKILL.md +58 -0
- package/assets/agent-assets/core/references/working-disciplines/SKILL.md +69 -0
- package/assets/agent-assets/core/user-guides/agent-assets.md +81 -0
- package/assets/agent-assets/core/user-guides/agentspace.md +1098 -0
- package/assets/agent-assets/core/user-guides/aops-cli.md +1107 -0
- package/assets/agent-assets/core/user-guides/aops-system.md +63 -0
- package/assets/agent-assets/core/user-guides/chatv3.md +113 -0
- package/assets/agent-assets/core/user-guides/docman.md +162 -0
- package/assets/agent-assets/core/user-guides/projectman.md +212 -0
- package/assets/agent-assets/core/user-guides/sys.md +94 -0
- package/assets/agent-assets/core/user-guides/working-disciplines.md +588 -0
- package/assets/agent-assets/gateway/aops/SKILL.md +14 -0
- package/assets/r6-remote-session-baseline.json +19 -0
- package/assets/skills/aops-install/SKILL.md +231 -0
- package/assets/skills/aops-install/agents/openai.yaml +4 -0
- package/dist/commands/activity.js +352 -0
- package/dist/commands/agent-profile.js +653 -0
- package/dist/commands/agent.js +710 -0
- package/dist/commands/agents-md.js +125 -0
- package/dist/commands/api.js +84 -0
- package/dist/commands/archive.js +873 -0
- package/dist/commands/artifact.js +535 -0
- package/dist/commands/assets.js +1414 -0
- package/dist/commands/auth/device-login.js +1 -0
- package/dist/commands/auth/legacy-export.js +177 -0
- package/dist/commands/auth/login.js +426 -0
- package/dist/commands/chat.js +1363 -0
- package/dist/commands/chatv3.js +1684 -0
- package/dist/commands/checkpoint.js +549 -0
- package/dist/commands/commercial-artifact.js +475 -0
- package/dist/commands/commercial-license.js +594 -0
- package/dist/commands/community-auth.js +62 -0
- package/dist/commands/community-cockpit.js +185 -0
- package/dist/commands/community-console.js +109 -0
- package/dist/commands/community-doctor.js +335 -0
- package/dist/commands/community-server.js +2062 -0
- package/dist/commands/community-setup.js +284 -0
- package/dist/commands/discuss.js +1448 -0
- package/dist/commands/doc.js +5219 -0
- package/dist/commands/experience.js +922 -0
- package/dist/commands/host.js +1127 -0
- package/dist/commands/init.js +118 -0
- package/dist/commands/memory.js +3129 -0
- package/dist/commands/mission.js +562 -0
- package/dist/commands/official-catalog.js +134 -0
- package/dist/commands/plan.js +84 -0
- package/dist/commands/playbook.js +415 -0
- package/dist/commands/pm/context.js +6 -0
- package/dist/commands/pm/index.js +701 -0
- package/dist/commands/pm/projectman.js +6266 -0
- package/dist/commands/project.js +1080 -0
- package/dist/commands/prompt.js +1024 -0
- package/dist/commands/repo-sync.js +1907 -0
- package/dist/commands/resource.js +527 -0
- package/dist/commands/skill.js +1211 -0
- package/dist/commands/start-disciplines.js +510 -0
- package/dist/commands/start.js +1343 -0
- package/dist/commands/target.js +309 -0
- package/dist/commands/version.js +42 -0
- package/dist/commands/view.js +1852 -0
- package/dist/launchers/cockpit.js +28 -0
- package/dist/launchers/options.js +74 -0
- package/dist/launchers/server.js +41 -0
- package/dist/lib/agent-assets/envelope.js +83 -0
- package/dist/lib/agent-assets/gateway.js +15 -0
- package/dist/lib/agent-assets/guards.js +23 -0
- package/dist/lib/agent-assets/hosted-discovery.js +148 -0
- package/dist/lib/agent-assets/hosted-package-input.js +154 -0
- package/dist/lib/agent-assets/legacy-pointer-migration.js +677 -0
- package/dist/lib/agent-assets/native-fs.js +589 -0
- package/dist/lib/agent-assets/package-manifest.js +324 -0
- package/dist/lib/agent-assets/portable-path.js +67 -0
- package/dist/lib/agent-assets/release-input.js +177 -0
- package/dist/lib/agent-assets/roots.js +45 -0
- package/dist/lib/agent-assets/runtime-binding-reader.js +545 -0
- package/dist/lib/agent-assets/runtime-targets.js +50 -0
- package/dist/lib/agent-assets/store-reader.js +1212 -0
- package/dist/lib/agent-assets/store-types.js +1 -0
- package/dist/lib/agent-assets/store-writer.js +1484 -0
- package/dist/lib/agent-assets/types.js +1 -0
- package/dist/lib/agent-assets/unicode-case-folding-v15-1.generated.js +1548 -0
- package/dist/lib/commercial-final-readiness.js +504 -0
- package/dist/lib/commercial-lifecycle-smoke-isolation.js +236 -0
- package/dist/lib/commercial-managed-postgres-docker-adapter.js +274 -0
- package/dist/lib/commercial-setup-readiness-adapter.js +179 -0
- package/dist/lib/community-client-contract.js +164 -0
- package/dist/lib/community-cockpit-lifecycle.js +601 -0
- package/dist/lib/community-command-abort.js +59 -0
- package/dist/lib/community-diagnostic.js +237 -0
- package/dist/lib/community-docker-adapter.js +498 -0
- package/dist/lib/community-home.js +31 -0
- package/dist/lib/community-instance-contract.js +148 -0
- package/dist/lib/community-instance-layout.js +97 -0
- package/dist/lib/community-lifecycle.js +1631 -0
- package/dist/lib/community-migration-snapshot.js +770 -0
- package/dist/lib/community-native-application-recovery.js +425 -0
- package/dist/lib/community-native-child.js +240 -0
- package/dist/lib/community-native-database-recovery.js +456 -0
- package/dist/lib/community-native-lifecycle.js +2458 -0
- package/dist/lib/community-native-migration.js +339 -0
- package/dist/lib/community-native-postgres.js +596 -0
- package/dist/lib/community-operation-journal.js +773 -0
- package/dist/lib/community-operation-lock.js +596 -0
- package/dist/lib/community-release-resolver.js +291 -0
- package/dist/lib/community-release-verifier.js +475 -0
- package/dist/lib/community-repo-discovery.js +252 -0
- package/dist/lib/community-setup-server-env.js +203 -0
- package/dist/lib/external-url.js +110 -0
- package/dist/lib/official-catalog-gateway.js +108 -0
- package/dist/lib/official-catalog.js +364 -0
- package/dist/lib/server-bootstrap.js +240 -0
- package/dist/lib/setup-agent-assets-bridge.js +176 -0
- package/dist/lib/setup-agent-assets-release.js +52 -0
- package/dist/lib/setup-external-postgres.js +108 -0
- package/dist/lib/setup-init-orchestrator.js +990 -0
- package/dist/lib/setup-install-guide.js +52 -0
- package/dist/lib/setup-installer-release.js +47 -0
- package/dist/lib/setup-local-postgres.js +329 -0
- package/dist/lib/setup-official-catalog-bridge.js +148 -0
- package/dist/lib/setup-readiness.js +888 -0
- package/dist/lib/tui-launcher.js +59 -0
- package/dist/main.js +150 -0
- package/dist/seeds/agents-md.js +54 -0
- package/dist/utils/agent-gateway.js +275 -0
- package/dist/utils/agents-md.js +281 -0
- package/dist/utils/api.js +255 -0
- package/dist/utils/chatv3-session-store.js +185 -0
- package/dist/utils/command.js +67 -0
- package/dist/utils/community-host-runtime.js +376 -0
- package/dist/utils/config.js +1015 -0
- package/dist/utils/discussion-workspace.js +552 -0
- package/dist/utils/experience-workspace.js +326 -0
- package/dist/utils/guide-paths.js +67 -0
- package/dist/utils/hosted-sugar.js +56 -0
- package/dist/utils/hosted-workspace.js +229 -0
- package/dist/utils/memory-workspace.js +537 -0
- package/dist/utils/operator-runtime.js +199 -0
- package/dist/utils/password-crypto.js +39 -0
- package/dist/utils/playbook-workspace.js +479 -0
- package/dist/utils/project-context.js +255 -0
- package/dist/utils/prompts.js +70 -0
- package/dist/utils/refresh-race.js +28 -0
- package/dist/utils/repo-config.js +109 -0
- package/dist/utils/repo-first-projectman.js +194 -0
- package/dist/utils/repo-first-storage.js +13 -0
- package/dist/utils/repo-first-view.js +305 -0
- package/dist/utils/session-guidance.js +278 -0
- package/dist/utils/session-state.js +137 -0
- package/dist/utils/table.js +39 -0
- package/dist/utils/transient-fs.js +49 -0
- package/dist/utils/workspace-root.js +1 -0
- package/launchers/aops-cockpit.ps1 +15 -0
- package/launchers/aops-cockpit.sh +16 -0
- package/launchers/aops-server.ps1 +15 -0
- package/launchers/aops-server.sh +16 -0
- package/native/tui/win32-x64/aops-tui.exe +0 -0
- package/package.json +81 -0
|
@@ -0,0 +1,1015 @@
|
|
|
1
|
+
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import os from 'node:os';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
const CONFIG_FILENAME = 'aops.config.json';
|
|
6
|
+
const TOKEN_KEY_FILENAME = 'aops.mcp.key';
|
|
7
|
+
const TARGET_NAME = /^[a-z0-9](?:[a-z0-9-]{0,30}[a-z0-9])?$/;
|
|
8
|
+
const CREDENTIAL_REVISION = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
9
|
+
const CONFIG_LOCK_STALE_MS = 30_000;
|
|
10
|
+
const cachedTokensByTarget = new Map();
|
|
11
|
+
const loadedTargets = new Set();
|
|
12
|
+
function tokenCacheKey(name) {
|
|
13
|
+
return `${getAopsCliConfigFilePath()}\0${name}`;
|
|
14
|
+
}
|
|
15
|
+
function normalizeNonEmpty(value) {
|
|
16
|
+
if (typeof value !== 'string')
|
|
17
|
+
return undefined;
|
|
18
|
+
const trimmed = value.trim();
|
|
19
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
20
|
+
}
|
|
21
|
+
function isWindowsDrivePath(value) {
|
|
22
|
+
return /^[A-Za-z]:[\\/]/.test(value);
|
|
23
|
+
}
|
|
24
|
+
export function getAopsCliConfigFilePath() {
|
|
25
|
+
const envPath = process.env.AOPS_CLI_CONFIG_PATH?.trim() || process.env.AGENT_OPS_CONFIG_PATH?.trim();
|
|
26
|
+
if (envPath && (process.platform === 'win32' || !isWindowsDrivePath(envPath))) {
|
|
27
|
+
return path.extname(envPath).toLowerCase() === '.json'
|
|
28
|
+
? path.resolve(envPath)
|
|
29
|
+
: path.resolve(envPath, CONFIG_FILENAME);
|
|
30
|
+
}
|
|
31
|
+
return path.join(os.homedir(), '.aops', CONFIG_FILENAME);
|
|
32
|
+
}
|
|
33
|
+
function assertSafeExistingPath(candidate, kind) {
|
|
34
|
+
if (!fs.existsSync(candidate))
|
|
35
|
+
return;
|
|
36
|
+
const stat = fs.lstatSync(candidate);
|
|
37
|
+
if (stat.isSymbolicLink())
|
|
38
|
+
throw new Error(`aops_cli_config_${kind}_symlink_refused`);
|
|
39
|
+
if (kind === 'file' && !stat.isFile())
|
|
40
|
+
throw new Error('aops_cli_config_path_not_file');
|
|
41
|
+
if (kind === 'directory' && !stat.isDirectory())
|
|
42
|
+
throw new Error('aops_cli_config_parent_not_directory');
|
|
43
|
+
}
|
|
44
|
+
function enforceOwnerOnlyMode(candidate, mode, kind) {
|
|
45
|
+
if (process.platform === 'win32')
|
|
46
|
+
return;
|
|
47
|
+
try {
|
|
48
|
+
fs.chmodSync(candidate, mode);
|
|
49
|
+
if ((fs.statSync(candidate).mode & 0o077) !== 0) {
|
|
50
|
+
throw new Error('mode_not_owner_only');
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
throw new Error(`aops_cli_config_${kind}_permissions_enforcement_failed`, { cause: error });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
function assertOwnerOnlyMode(candidate, kind) {
|
|
58
|
+
if (process.platform === 'win32')
|
|
59
|
+
return;
|
|
60
|
+
try {
|
|
61
|
+
if ((fs.statSync(candidate).mode & 0o077) !== 0) {
|
|
62
|
+
throw new Error('mode_not_owner_only');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
catch (error) {
|
|
66
|
+
throw new Error(`aops_cli_config_${kind}_permissions_not_owner_only`, { cause: error });
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
function ensureConfigDirectory(configPath) {
|
|
70
|
+
const dir = path.dirname(configPath);
|
|
71
|
+
if (!fs.existsSync(dir))
|
|
72
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
73
|
+
assertSafeExistingPath(dir, 'directory');
|
|
74
|
+
enforceOwnerOnlyMode(dir, 0o700, 'directory');
|
|
75
|
+
}
|
|
76
|
+
function readConfigFile(options = {}) {
|
|
77
|
+
const configPath = getAopsCliConfigFilePath();
|
|
78
|
+
if (!fs.existsSync(configPath))
|
|
79
|
+
return {};
|
|
80
|
+
assertSafeExistingPath(configPath, 'file');
|
|
81
|
+
if (options.readOnly)
|
|
82
|
+
assertOwnerOnlyMode(configPath, 'file');
|
|
83
|
+
else
|
|
84
|
+
enforceOwnerOnlyMode(configPath, 0o600, 'file');
|
|
85
|
+
let parsed;
|
|
86
|
+
try {
|
|
87
|
+
parsed = JSON.parse(fs.readFileSync(configPath, 'utf8'));
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
throw new Error(`aops_cli_config_invalid_json:${configPath}`, { cause: error });
|
|
91
|
+
}
|
|
92
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
93
|
+
throw new Error(`aops_cli_config_invalid_root:${configPath}`);
|
|
94
|
+
}
|
|
95
|
+
return parsed;
|
|
96
|
+
}
|
|
97
|
+
function readOptionalNonEmptyString(value, field) {
|
|
98
|
+
if (value === undefined)
|
|
99
|
+
return undefined;
|
|
100
|
+
if (typeof value !== 'string' || value.trim().length === 0) {
|
|
101
|
+
throw new Error(`aops_cli_config_invalid_${field}`);
|
|
102
|
+
}
|
|
103
|
+
return value.trim();
|
|
104
|
+
}
|
|
105
|
+
export function getAgentAssetsConfig() {
|
|
106
|
+
const raw = readConfig().agentAssets;
|
|
107
|
+
if (raw === undefined)
|
|
108
|
+
return Object.freeze({});
|
|
109
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
110
|
+
throw new Error('aops_cli_config_invalid_agentAssets');
|
|
111
|
+
}
|
|
112
|
+
const record = raw;
|
|
113
|
+
const runtimeHomesRaw = record.runtimeHomes;
|
|
114
|
+
if (runtimeHomesRaw !== undefined &&
|
|
115
|
+
(!runtimeHomesRaw || typeof runtimeHomesRaw !== 'object' || Array.isArray(runtimeHomesRaw))) {
|
|
116
|
+
throw new Error('aops_cli_config_invalid_agentAssets_runtimeHomes');
|
|
117
|
+
}
|
|
118
|
+
const runtimeHomes = (runtimeHomesRaw ?? {});
|
|
119
|
+
return Object.freeze({
|
|
120
|
+
dataRoot: readOptionalNonEmptyString(record.dataRoot, 'agentAssets_dataRoot'),
|
|
121
|
+
runtimeHomes: Object.freeze({
|
|
122
|
+
codex: readOptionalNonEmptyString(runtimeHomes.codex, 'agentAssets_runtimeHomes_codex'),
|
|
123
|
+
claude: readOptionalNonEmptyString(runtimeHomes.claude, 'agentAssets_runtimeHomes_claude'),
|
|
124
|
+
}),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
function writeConfig(config) {
|
|
128
|
+
const configPath = getAopsCliConfigFilePath();
|
|
129
|
+
ensureConfigDirectory(configPath);
|
|
130
|
+
assertSafeExistingPath(configPath, 'file');
|
|
131
|
+
const temporary = path.join(path.dirname(configPath), `.${path.basename(configPath)}.${process.pid}.${crypto.randomUUID()}.tmp`);
|
|
132
|
+
let temporaryFd;
|
|
133
|
+
try {
|
|
134
|
+
temporaryFd = fs.openSync(temporary, 'wx', 0o600);
|
|
135
|
+
fs.writeFileSync(temporaryFd, `${JSON.stringify(config, null, 2)}\n`, 'utf8');
|
|
136
|
+
fs.fsyncSync(temporaryFd);
|
|
137
|
+
fs.closeSync(temporaryFd);
|
|
138
|
+
temporaryFd = undefined;
|
|
139
|
+
fs.renameSync(temporary, configPath);
|
|
140
|
+
enforceOwnerOnlyMode(configPath, 0o600, 'file');
|
|
141
|
+
if (process.platform !== 'win32') {
|
|
142
|
+
let directoryFd;
|
|
143
|
+
try {
|
|
144
|
+
directoryFd = fs.openSync(path.dirname(configPath), 'r');
|
|
145
|
+
fs.fsyncSync(directoryFd);
|
|
146
|
+
}
|
|
147
|
+
finally {
|
|
148
|
+
if (directoryFd !== undefined)
|
|
149
|
+
fs.closeSync(directoryFd);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
finally {
|
|
154
|
+
if (temporaryFd !== undefined) {
|
|
155
|
+
try {
|
|
156
|
+
fs.closeSync(temporaryFd);
|
|
157
|
+
}
|
|
158
|
+
catch { }
|
|
159
|
+
}
|
|
160
|
+
fs.rmSync(temporary, { force: true });
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
function serializeLockReceipt(receipt) {
|
|
164
|
+
return `${JSON.stringify(receipt)}\n`;
|
|
165
|
+
}
|
|
166
|
+
function parseLockReceipt(value) {
|
|
167
|
+
try {
|
|
168
|
+
const parsed = JSON.parse(value);
|
|
169
|
+
if (parsed.schemaVersion !== 1 || !Number.isSafeInteger(parsed.pid) || Number(parsed.pid) < 1 ||
|
|
170
|
+
!Number.isSafeInteger(parsed.createdAtMs) || Number(parsed.createdAtMs) < 0 ||
|
|
171
|
+
typeof parsed.nonce !== 'string' || !/^[a-f0-9-]{36}$/i.test(parsed.nonce))
|
|
172
|
+
return undefined;
|
|
173
|
+
return parsed;
|
|
174
|
+
}
|
|
175
|
+
catch {
|
|
176
|
+
return undefined;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
function isProcessAlive(pid) {
|
|
180
|
+
if (pid === process.pid)
|
|
181
|
+
return true;
|
|
182
|
+
try {
|
|
183
|
+
process.kill(pid, 0);
|
|
184
|
+
return true;
|
|
185
|
+
}
|
|
186
|
+
catch (error) {
|
|
187
|
+
return error.code !== 'ESRCH';
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
function removeStaleConfigLock(lockPath) {
|
|
191
|
+
assertSafeExistingPath(lockPath, 'file');
|
|
192
|
+
let observed;
|
|
193
|
+
try {
|
|
194
|
+
observed = fs.readFileSync(lockPath, 'utf8');
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
if (error.code === 'ENOENT')
|
|
198
|
+
return true;
|
|
199
|
+
throw error;
|
|
200
|
+
}
|
|
201
|
+
const receipt = parseLockReceipt(observed);
|
|
202
|
+
if (!receipt || Date.now() - receipt.createdAtMs <= CONFIG_LOCK_STALE_MS || isProcessAlive(receipt.pid)) {
|
|
203
|
+
return false;
|
|
204
|
+
}
|
|
205
|
+
let current;
|
|
206
|
+
try {
|
|
207
|
+
current = fs.readFileSync(lockPath, 'utf8');
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
if (error.code === 'ENOENT')
|
|
211
|
+
return true;
|
|
212
|
+
throw error;
|
|
213
|
+
}
|
|
214
|
+
if (current !== observed)
|
|
215
|
+
return false;
|
|
216
|
+
try {
|
|
217
|
+
fs.rmSync(lockPath);
|
|
218
|
+
}
|
|
219
|
+
catch (error) {
|
|
220
|
+
if (error.code === 'ENOENT')
|
|
221
|
+
return true;
|
|
222
|
+
throw error;
|
|
223
|
+
}
|
|
224
|
+
return true;
|
|
225
|
+
}
|
|
226
|
+
function withConfigMutation(mutation) {
|
|
227
|
+
const configPath = getAopsCliConfigFilePath();
|
|
228
|
+
ensureConfigDirectory(configPath);
|
|
229
|
+
const lockPath = `${configPath}.lock`;
|
|
230
|
+
const lockReceipt = {
|
|
231
|
+
schemaVersion: 1,
|
|
232
|
+
pid: process.pid,
|
|
233
|
+
createdAtMs: Date.now(),
|
|
234
|
+
nonce: crypto.randomUUID(),
|
|
235
|
+
};
|
|
236
|
+
const lockReceiptText = serializeLockReceipt(lockReceipt);
|
|
237
|
+
let lockFd;
|
|
238
|
+
for (let attempt = 0; attempt < 200; attempt += 1) {
|
|
239
|
+
try {
|
|
240
|
+
lockFd = fs.openSync(lockPath, 'wx', 0o600);
|
|
241
|
+
}
|
|
242
|
+
catch (error) {
|
|
243
|
+
if (error.code !== 'EEXIST')
|
|
244
|
+
throw error;
|
|
245
|
+
if (removeStaleConfigLock(lockPath))
|
|
246
|
+
continue;
|
|
247
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 10);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
try {
|
|
251
|
+
fs.writeFileSync(lockFd, lockReceiptText, 'utf8');
|
|
252
|
+
fs.fsyncSync(lockFd);
|
|
253
|
+
break;
|
|
254
|
+
}
|
|
255
|
+
catch (error) {
|
|
256
|
+
try {
|
|
257
|
+
fs.closeSync(lockFd);
|
|
258
|
+
}
|
|
259
|
+
catch { }
|
|
260
|
+
lockFd = undefined;
|
|
261
|
+
try {
|
|
262
|
+
fs.rmSync(lockPath, { force: true });
|
|
263
|
+
}
|
|
264
|
+
catch { }
|
|
265
|
+
throw error;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
if (lockFd === undefined)
|
|
269
|
+
throw new Error('aops_cli_config_lock_busy');
|
|
270
|
+
try {
|
|
271
|
+
const config = readConfigFile();
|
|
272
|
+
if (hasMissingTargetCredentialRevision(config))
|
|
273
|
+
migrateLegacyTargetCredentialRevisions(config);
|
|
274
|
+
const result = mutation(config);
|
|
275
|
+
writeConfig(config);
|
|
276
|
+
return result;
|
|
277
|
+
}
|
|
278
|
+
finally {
|
|
279
|
+
try {
|
|
280
|
+
fs.closeSync(lockFd);
|
|
281
|
+
}
|
|
282
|
+
catch { }
|
|
283
|
+
try {
|
|
284
|
+
if (fs.readFileSync(lockPath, 'utf8') === lockReceiptText)
|
|
285
|
+
fs.rmSync(lockPath);
|
|
286
|
+
}
|
|
287
|
+
catch (error) {
|
|
288
|
+
if (error.code !== 'ENOENT')
|
|
289
|
+
throw error;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
function hasMissingTargetCredentialRevision(config) {
|
|
294
|
+
const raw = config.clientTargets;
|
|
295
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw))
|
|
296
|
+
return false;
|
|
297
|
+
const targets = raw.targets;
|
|
298
|
+
if (!targets || typeof targets !== 'object' || Array.isArray(targets))
|
|
299
|
+
return false;
|
|
300
|
+
return Object.values(targets).some((target) => {
|
|
301
|
+
if (!target || typeof target !== 'object' || Array.isArray(target))
|
|
302
|
+
return false;
|
|
303
|
+
const credentials = target.credentials;
|
|
304
|
+
return Boolean(credentials && typeof credentials === 'object' && !Array.isArray(credentials) &&
|
|
305
|
+
!Object.hasOwn(credentials, 'credentialRevision'));
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
function isLegacyStoredCredentialsWithoutRevision(value, endpointSha256) {
|
|
309
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
310
|
+
return false;
|
|
311
|
+
const credentials = value;
|
|
312
|
+
return credentials.schemaVersion === 1 &&
|
|
313
|
+
credentials.endpointSha256 === endpointSha256 &&
|
|
314
|
+
!Object.hasOwn(credentials, 'credentialRevision') &&
|
|
315
|
+
Boolean(normalizeNonEmpty(credentials.accessTokenEnc)) &&
|
|
316
|
+
Boolean(normalizeNonEmpty(credentials.refreshTokenEnc)) &&
|
|
317
|
+
(credentials.userId === undefined || typeof credentials.userId === 'string');
|
|
318
|
+
}
|
|
319
|
+
function migrateLegacyTargetCredentialRevisions(config) {
|
|
320
|
+
const raw = config.clientTargets;
|
|
321
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
322
|
+
throw new Error('aops_target_config_schema_invalid');
|
|
323
|
+
}
|
|
324
|
+
const targetConfig = raw;
|
|
325
|
+
if (targetConfig.schemaVersion !== 1 || !targetConfig.targets ||
|
|
326
|
+
typeof targetConfig.targets !== 'object' || Array.isArray(targetConfig.targets)) {
|
|
327
|
+
throw new Error('aops_target_config_schema_invalid');
|
|
328
|
+
}
|
|
329
|
+
const endpointOwners = new Map();
|
|
330
|
+
const credentialsToMigrate = [];
|
|
331
|
+
for (const [name, value] of Object.entries(targetConfig.targets)) {
|
|
332
|
+
if (normalizeApiTargetName(name) !== name || !value || typeof value !== 'object' || Array.isArray(value)) {
|
|
333
|
+
throw new Error(`aops_target_config_invalid:${name}`);
|
|
334
|
+
}
|
|
335
|
+
const target = value;
|
|
336
|
+
let validated;
|
|
337
|
+
try {
|
|
338
|
+
validated = validateApiTarget({
|
|
339
|
+
apiBaseUrl: String(target.apiBaseUrl ?? ''),
|
|
340
|
+
authProvider: target.authProvider,
|
|
341
|
+
tlsPolicy: target.tlsPolicy,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
throw new Error(`aops_target_config_invalid:${name}`);
|
|
346
|
+
}
|
|
347
|
+
if (target.schemaVersion !== 1 || target.endpointSha256 !== validated.endpointSha256) {
|
|
348
|
+
throw new Error(`aops_target_config_invalid:${name}`);
|
|
349
|
+
}
|
|
350
|
+
if (target.credentials !== undefined) {
|
|
351
|
+
if (isStoredCredentials(target.credentials, validated.endpointSha256)) {
|
|
352
|
+
}
|
|
353
|
+
else if (isLegacyStoredCredentialsWithoutRevision(target.credentials, validated.endpointSha256)) {
|
|
354
|
+
credentialsToMigrate.push(target.credentials);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
throw new Error(`aops_target_config_invalid:${name}`);
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
const owner = endpointOwners.get(validated.apiBaseUrl);
|
|
361
|
+
if (owner)
|
|
362
|
+
throw new Error(`aops_target_endpoint_duplicate:${owner}:${name}`);
|
|
363
|
+
endpointOwners.set(validated.apiBaseUrl, name);
|
|
364
|
+
}
|
|
365
|
+
const activeTarget = normalizeNonEmpty(targetConfig.activeTarget);
|
|
366
|
+
if (activeTarget && !Object.hasOwn(targetConfig.targets, activeTarget)) {
|
|
367
|
+
throw new Error('aops_target_active_missing');
|
|
368
|
+
}
|
|
369
|
+
for (const credentials of credentialsToMigrate) {
|
|
370
|
+
credentials.credentialRevision = crypto.randomUUID();
|
|
371
|
+
}
|
|
372
|
+
return credentialsToMigrate.length > 0;
|
|
373
|
+
}
|
|
374
|
+
function readConfig() {
|
|
375
|
+
const config = readConfigFile();
|
|
376
|
+
if (!hasMissingTargetCredentialRevision(config))
|
|
377
|
+
return config;
|
|
378
|
+
return withConfigMutation((latest) => structuredClone(latest));
|
|
379
|
+
}
|
|
380
|
+
function getTokenKeyFilePath() {
|
|
381
|
+
return path.join(path.dirname(getAopsCliConfigFilePath()), TOKEN_KEY_FILENAME);
|
|
382
|
+
}
|
|
383
|
+
function resolveTokenSecret(options) {
|
|
384
|
+
const envSecret = process.env.AOPS_MCP_TOKEN_SECRET?.trim() || process.env.AOPS_MCP_CONFIG_SECRET?.trim();
|
|
385
|
+
if (envSecret)
|
|
386
|
+
return envSecret;
|
|
387
|
+
const keyFilePath = getTokenKeyFilePath();
|
|
388
|
+
if (fs.existsSync(keyFilePath)) {
|
|
389
|
+
assertSafeExistingPath(keyFilePath, 'file');
|
|
390
|
+
if (options?.readOnly)
|
|
391
|
+
assertOwnerOnlyMode(keyFilePath, 'file');
|
|
392
|
+
else
|
|
393
|
+
enforceOwnerOnlyMode(keyFilePath, 0o600, 'file');
|
|
394
|
+
return normalizeNonEmpty(fs.readFileSync(keyFilePath, 'utf8'));
|
|
395
|
+
}
|
|
396
|
+
if (!options?.ensure)
|
|
397
|
+
return undefined;
|
|
398
|
+
ensureConfigDirectory(keyFilePath);
|
|
399
|
+
const secret = crypto.randomBytes(32).toString('base64');
|
|
400
|
+
let keyFd;
|
|
401
|
+
let createdKeyFile = false;
|
|
402
|
+
try {
|
|
403
|
+
keyFd = fs.openSync(keyFilePath, 'wx', 0o600);
|
|
404
|
+
createdKeyFile = true;
|
|
405
|
+
fs.writeFileSync(keyFd, `${secret}\n`, 'utf8');
|
|
406
|
+
fs.fsyncSync(keyFd);
|
|
407
|
+
fs.closeSync(keyFd);
|
|
408
|
+
keyFd = undefined;
|
|
409
|
+
enforceOwnerOnlyMode(keyFilePath, 0o600, 'file');
|
|
410
|
+
return secret;
|
|
411
|
+
}
|
|
412
|
+
catch (error) {
|
|
413
|
+
if (keyFd !== undefined) {
|
|
414
|
+
try {
|
|
415
|
+
fs.closeSync(keyFd);
|
|
416
|
+
}
|
|
417
|
+
catch { }
|
|
418
|
+
}
|
|
419
|
+
if (error.code !== 'EEXIST') {
|
|
420
|
+
if (createdKeyFile) {
|
|
421
|
+
try {
|
|
422
|
+
fs.rmSync(keyFilePath, { force: true });
|
|
423
|
+
}
|
|
424
|
+
catch { }
|
|
425
|
+
}
|
|
426
|
+
throw error;
|
|
427
|
+
}
|
|
428
|
+
assertSafeExistingPath(keyFilePath, 'file');
|
|
429
|
+
enforceOwnerOnlyMode(keyFilePath, 0o600, 'file');
|
|
430
|
+
return normalizeNonEmpty(fs.readFileSync(keyFilePath, 'utf8'));
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
function encodeEncryptedPayload(payload) {
|
|
434
|
+
return Buffer.from(JSON.stringify(payload), 'utf8').toString('base64');
|
|
435
|
+
}
|
|
436
|
+
function decodeEncryptedPayload(value) {
|
|
437
|
+
const payload = JSON.parse(Buffer.from(value, 'base64').toString('utf8'));
|
|
438
|
+
if (payload.v !== 1 || payload.kdf !== 'scrypt' ||
|
|
439
|
+
!normalizeNonEmpty(payload.salt) || !normalizeNonEmpty(payload.iv) ||
|
|
440
|
+
!normalizeNonEmpty(payload.tag) || !normalizeNonEmpty(payload.data)) {
|
|
441
|
+
throw new Error('aops_cli_token_payload_invalid');
|
|
442
|
+
}
|
|
443
|
+
return payload;
|
|
444
|
+
}
|
|
445
|
+
async function deriveTokenKey(secret, salt) {
|
|
446
|
+
return await new Promise((resolve, reject) => {
|
|
447
|
+
crypto.scrypt(secret, salt, 32, (error, key) => {
|
|
448
|
+
if (error)
|
|
449
|
+
reject(error);
|
|
450
|
+
else
|
|
451
|
+
resolve(key);
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
async function encryptToken(value) {
|
|
456
|
+
const secret = resolveTokenSecret({ ensure: true });
|
|
457
|
+
if (!secret)
|
|
458
|
+
throw new Error('aops_cli_token_secret_unavailable');
|
|
459
|
+
const salt = crypto.randomBytes(16);
|
|
460
|
+
const iv = crypto.randomBytes(12);
|
|
461
|
+
const key = await deriveTokenKey(secret, salt);
|
|
462
|
+
const cipher = crypto.createCipheriv('aes-256-gcm', key, iv);
|
|
463
|
+
const data = Buffer.concat([cipher.update(value, 'utf8'), cipher.final()]);
|
|
464
|
+
return encodeEncryptedPayload({
|
|
465
|
+
v: 1,
|
|
466
|
+
kdf: 'scrypt',
|
|
467
|
+
salt: salt.toString('base64'),
|
|
468
|
+
iv: iv.toString('base64'),
|
|
469
|
+
tag: cipher.getAuthTag().toString('base64'),
|
|
470
|
+
data: data.toString('base64'),
|
|
471
|
+
});
|
|
472
|
+
}
|
|
473
|
+
async function decryptToken(value, options = {}) {
|
|
474
|
+
const secret = resolveTokenSecret({ ensure: false, readOnly: options.readOnly });
|
|
475
|
+
if (!secret)
|
|
476
|
+
return undefined;
|
|
477
|
+
try {
|
|
478
|
+
const payload = decodeEncryptedPayload(value);
|
|
479
|
+
const salt = Buffer.from(payload.salt, 'base64');
|
|
480
|
+
const key = await deriveTokenKey(secret, salt);
|
|
481
|
+
const decipher = crypto.createDecipheriv('aes-256-gcm', key, Buffer.from(payload.iv, 'base64'));
|
|
482
|
+
decipher.setAuthTag(Buffer.from(payload.tag, 'base64'));
|
|
483
|
+
return Buffer.concat([
|
|
484
|
+
decipher.update(Buffer.from(payload.data, 'base64')),
|
|
485
|
+
decipher.final(),
|
|
486
|
+
]).toString('utf8');
|
|
487
|
+
}
|
|
488
|
+
catch {
|
|
489
|
+
return undefined;
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
export function normalizeApiTargetName(value) {
|
|
493
|
+
const normalized = value.trim().toLowerCase();
|
|
494
|
+
if (!TARGET_NAME.test(normalized))
|
|
495
|
+
throw new Error('aops_target_name_invalid');
|
|
496
|
+
return normalized;
|
|
497
|
+
}
|
|
498
|
+
function isLoopbackHostname(hostname) {
|
|
499
|
+
const normalized = hostname.toLowerCase().replace(/^\[|\]$/g, '');
|
|
500
|
+
if (normalized === 'localhost' || normalized === '::1')
|
|
501
|
+
return true;
|
|
502
|
+
const ipv4 = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(normalized);
|
|
503
|
+
return Boolean(ipv4 && Number(ipv4[1]) === 127 && ipv4.slice(1).every((part) => Number(part) <= 255));
|
|
504
|
+
}
|
|
505
|
+
export function normalizeApiTargetBaseUrl(value) {
|
|
506
|
+
let parsed;
|
|
507
|
+
try {
|
|
508
|
+
parsed = new URL(value.trim());
|
|
509
|
+
}
|
|
510
|
+
catch {
|
|
511
|
+
throw new Error('aops_target_url_invalid');
|
|
512
|
+
}
|
|
513
|
+
if (!['http:', 'https:'].includes(parsed.protocol))
|
|
514
|
+
throw new Error('aops_target_url_protocol_invalid');
|
|
515
|
+
if (parsed.username || parsed.password || parsed.search || parsed.hash) {
|
|
516
|
+
throw new Error('aops_target_url_credentials_or_suffix_refused');
|
|
517
|
+
}
|
|
518
|
+
if (parsed.pathname !== '/' && parsed.pathname !== '')
|
|
519
|
+
throw new Error('aops_target_url_path_refused');
|
|
520
|
+
parsed.pathname = '';
|
|
521
|
+
return parsed.toString().replace(/\/$/, '');
|
|
522
|
+
}
|
|
523
|
+
export function apiTargetEndpointSha256(apiBaseUrl) {
|
|
524
|
+
return `sha256:${crypto.createHash('sha256').update(normalizeApiTargetBaseUrl(apiBaseUrl)).digest('hex')}`;
|
|
525
|
+
}
|
|
526
|
+
export function normalizeApiTargetAuthProvider(value) {
|
|
527
|
+
if (typeof value !== 'string')
|
|
528
|
+
return undefined;
|
|
529
|
+
const normalized = value.trim().toLowerCase();
|
|
530
|
+
if (normalized === 'trusted-local')
|
|
531
|
+
return 'trusted-local';
|
|
532
|
+
if (normalized === 'remote-session' || normalized === 'authv2-jwt-session') {
|
|
533
|
+
return 'remote-session';
|
|
534
|
+
}
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
export function isRemoteSessionAuthProvider(value) {
|
|
538
|
+
return normalizeApiTargetAuthProvider(value) === 'remote-session';
|
|
539
|
+
}
|
|
540
|
+
export function validateApiTarget(input) {
|
|
541
|
+
const apiBaseUrl = normalizeApiTargetBaseUrl(input.apiBaseUrl);
|
|
542
|
+
const parsed = new URL(apiBaseUrl);
|
|
543
|
+
const loopback = isLoopbackHostname(parsed.hostname);
|
|
544
|
+
const requestedAuthProvider = input.authProvider ?? (loopback ? 'trusted-local' : 'remote-session');
|
|
545
|
+
const authProvider = normalizeApiTargetAuthProvider(requestedAuthProvider);
|
|
546
|
+
const tlsPolicy = input.tlsPolicy ?? (parsed.protocol === 'http:' && loopback
|
|
547
|
+
? 'loopback-http'
|
|
548
|
+
: 'system-ca');
|
|
549
|
+
if (!authProvider)
|
|
550
|
+
throw new Error('aops_target_auth_provider_invalid');
|
|
551
|
+
if (!['loopback-http', 'tailscale-http', 'system-ca'].includes(tlsPolicy)) {
|
|
552
|
+
throw new Error('aops_target_tls_policy_invalid');
|
|
553
|
+
}
|
|
554
|
+
if (authProvider === 'trusted-local' && !loopback)
|
|
555
|
+
throw new Error('aops_target_trusted_local_non_loopback_refused');
|
|
556
|
+
const acceptedLoopbackHttp = loopback && tlsPolicy === 'loopback-http';
|
|
557
|
+
if (parsed.protocol === 'http:' && !acceptedLoopbackHttp) {
|
|
558
|
+
throw new Error('aops_target_plain_http_non_loopback_refused');
|
|
559
|
+
}
|
|
560
|
+
if (parsed.protocol === 'https:' && tlsPolicy !== 'system-ca')
|
|
561
|
+
throw new Error('aops_target_https_tls_policy_invalid');
|
|
562
|
+
if (!loopback && (parsed.protocol !== 'https:' || authProvider !== 'remote-session')) {
|
|
563
|
+
throw new Error('aops_target_remote_requires_https_authv2');
|
|
564
|
+
}
|
|
565
|
+
return {
|
|
566
|
+
schemaVersion: 1,
|
|
567
|
+
apiBaseUrl,
|
|
568
|
+
endpointSha256: apiTargetEndpointSha256(apiBaseUrl),
|
|
569
|
+
authProvider,
|
|
570
|
+
tlsPolicy,
|
|
571
|
+
};
|
|
572
|
+
}
|
|
573
|
+
function isStoredCredentials(value, endpointSha256) {
|
|
574
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
575
|
+
return false;
|
|
576
|
+
const credentials = value;
|
|
577
|
+
return credentials.schemaVersion === 1 &&
|
|
578
|
+
credentials.endpointSha256 === endpointSha256 &&
|
|
579
|
+
typeof credentials.credentialRevision === 'string' && CREDENTIAL_REVISION.test(credentials.credentialRevision) &&
|
|
580
|
+
Boolean(normalizeNonEmpty(credentials.accessTokenEnc)) &&
|
|
581
|
+
Boolean(normalizeNonEmpty(credentials.refreshTokenEnc)) &&
|
|
582
|
+
(credentials.userId === undefined || typeof credentials.userId === 'string');
|
|
583
|
+
}
|
|
584
|
+
function isStoredTarget(value) {
|
|
585
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
586
|
+
return false;
|
|
587
|
+
const target = value;
|
|
588
|
+
try {
|
|
589
|
+
const validated = validateApiTarget({
|
|
590
|
+
apiBaseUrl: String(target.apiBaseUrl ?? ''),
|
|
591
|
+
authProvider: target.authProvider,
|
|
592
|
+
tlsPolicy: target.tlsPolicy,
|
|
593
|
+
});
|
|
594
|
+
return target.schemaVersion === 1 && target.endpointSha256 === validated.endpointSha256 &&
|
|
595
|
+
(target.credentials === undefined || isStoredCredentials(target.credentials, validated.endpointSha256));
|
|
596
|
+
}
|
|
597
|
+
catch {
|
|
598
|
+
return false;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
function hasLegacyCredentialFields(config) {
|
|
602
|
+
return Boolean(normalizeNonEmpty(config.apiAccessToken) || normalizeNonEmpty(config.apiRefreshToken) ||
|
|
603
|
+
normalizeNonEmpty(config.apiAccessTokenEnc) || normalizeNonEmpty(config.apiRefreshTokenEnc));
|
|
604
|
+
}
|
|
605
|
+
function legacyCredentialFingerprint(config) {
|
|
606
|
+
if (!hasLegacyCredentialFields(config))
|
|
607
|
+
return undefined;
|
|
608
|
+
const snapshot = [
|
|
609
|
+
normalizeNonEmpty(config.apiServer),
|
|
610
|
+
normalizeNonEmpty(config.apiAccessToken),
|
|
611
|
+
normalizeNonEmpty(config.apiRefreshToken),
|
|
612
|
+
normalizeNonEmpty(config.apiAccessTokenEnc),
|
|
613
|
+
normalizeNonEmpty(config.apiRefreshTokenEnc),
|
|
614
|
+
normalizeNonEmpty(config.apiUserId),
|
|
615
|
+
];
|
|
616
|
+
return `sha256:${crypto.createHash('sha256').update(JSON.stringify(snapshot)).digest('hex')}`;
|
|
617
|
+
}
|
|
618
|
+
function resolveLegacyCredentialTargetName(config, targetConfig) {
|
|
619
|
+
if (!hasLegacyCredentialFields(config))
|
|
620
|
+
return undefined;
|
|
621
|
+
const legacyTarget = validateApiTarget({
|
|
622
|
+
apiBaseUrl: normalizeNonEmpty(config.apiServer) ?? 'http://localhost:5900',
|
|
623
|
+
});
|
|
624
|
+
const matches = Object.entries(targetConfig.targets)
|
|
625
|
+
.filter(([, target]) => target.apiBaseUrl === legacyTarget.apiBaseUrl)
|
|
626
|
+
.map(([name]) => name);
|
|
627
|
+
if (matches.length !== 1) {
|
|
628
|
+
throw new Error(matches.length === 0
|
|
629
|
+
? 'aops_legacy_credentials_target_conflict'
|
|
630
|
+
: 'aops_legacy_credentials_target_ambiguous');
|
|
631
|
+
}
|
|
632
|
+
return matches[0];
|
|
633
|
+
}
|
|
634
|
+
function readTargetConfig(config) {
|
|
635
|
+
const raw = config.clientTargets;
|
|
636
|
+
const targets = Object.create(null);
|
|
637
|
+
const endpointOwners = new Map();
|
|
638
|
+
if (raw?.schemaVersion === 1 && raw.targets && typeof raw.targets === 'object' && !Array.isArray(raw.targets)) {
|
|
639
|
+
for (const [name, target] of Object.entries(raw.targets)) {
|
|
640
|
+
if (normalizeApiTargetName(name) !== name || !isStoredTarget(target)) {
|
|
641
|
+
throw new Error(`aops_target_config_invalid:${name}`);
|
|
642
|
+
}
|
|
643
|
+
const owner = endpointOwners.get(target.apiBaseUrl);
|
|
644
|
+
if (owner)
|
|
645
|
+
throw new Error(`aops_target_endpoint_duplicate:${owner}:${name}`);
|
|
646
|
+
endpointOwners.set(target.apiBaseUrl, name);
|
|
647
|
+
targets[name] = {
|
|
648
|
+
...structuredClone(target),
|
|
649
|
+
authProvider: normalizeApiTargetAuthProvider(target.authProvider),
|
|
650
|
+
};
|
|
651
|
+
}
|
|
652
|
+
}
|
|
653
|
+
else if (raw !== undefined) {
|
|
654
|
+
throw new Error('aops_target_config_schema_invalid');
|
|
655
|
+
}
|
|
656
|
+
let activeTarget = normalizeNonEmpty(raw?.activeTarget);
|
|
657
|
+
if (activeTarget && !Object.hasOwn(targets, activeTarget))
|
|
658
|
+
throw new Error('aops_target_active_missing');
|
|
659
|
+
const legacyUrl = normalizeNonEmpty(config.apiServer);
|
|
660
|
+
const hasLegacyCredentials = hasLegacyCredentialFields(config);
|
|
661
|
+
if ((legacyUrl || hasLegacyCredentials) && Object.keys(targets).length === 0) {
|
|
662
|
+
const target = validateApiTarget({ apiBaseUrl: legacyUrl ?? 'http://localhost:5900' });
|
|
663
|
+
targets.default = target;
|
|
664
|
+
activeTarget = 'default';
|
|
665
|
+
}
|
|
666
|
+
const targetConfig = { schemaVersion: 1, activeTarget, targets };
|
|
667
|
+
resolveLegacyCredentialTargetName(config, targetConfig);
|
|
668
|
+
return targetConfig;
|
|
669
|
+
}
|
|
670
|
+
function publicSummary(name, target, activeTarget) {
|
|
671
|
+
const { credentials, ...safe } = target;
|
|
672
|
+
return {
|
|
673
|
+
...structuredClone(safe),
|
|
674
|
+
name,
|
|
675
|
+
active: name === activeTarget,
|
|
676
|
+
hasCredentials: Boolean(credentials),
|
|
677
|
+
userId: credentials?.userId,
|
|
678
|
+
};
|
|
679
|
+
}
|
|
680
|
+
export function listApiTargets() {
|
|
681
|
+
const targetConfig = readTargetConfig(readConfig());
|
|
682
|
+
return Object.entries(targetConfig.targets)
|
|
683
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
684
|
+
.map(([name, target]) => publicSummary(name, target, targetConfig.activeTarget));
|
|
685
|
+
}
|
|
686
|
+
export function getApiTarget(name) {
|
|
687
|
+
const targetConfig = readTargetConfig(readConfig());
|
|
688
|
+
const normalized = normalizeApiTargetName(name);
|
|
689
|
+
const target = targetConfig.targets[normalized];
|
|
690
|
+
return target ? publicSummary(normalized, target, targetConfig.activeTarget) : undefined;
|
|
691
|
+
}
|
|
692
|
+
export function getActiveApiTarget() {
|
|
693
|
+
const targetConfig = readTargetConfig(readConfig());
|
|
694
|
+
const name = targetConfig.activeTarget;
|
|
695
|
+
const target = name ? targetConfig.targets[name] : undefined;
|
|
696
|
+
return name && target ? publicSummary(name, target, name) : undefined;
|
|
697
|
+
}
|
|
698
|
+
export function findApiTargetByBaseUrl(apiBaseUrl) {
|
|
699
|
+
const normalizedUrl = normalizeApiTargetBaseUrl(apiBaseUrl);
|
|
700
|
+
return listApiTargets().find((target) => target.apiBaseUrl === normalizedUrl);
|
|
701
|
+
}
|
|
702
|
+
export function listApiTargetsReadOnly() {
|
|
703
|
+
const targetConfig = readTargetConfig(readConfigFile({ readOnly: true }));
|
|
704
|
+
return Object.entries(targetConfig.targets)
|
|
705
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
706
|
+
.map(([name, target]) => publicSummary(name, target, targetConfig.activeTarget));
|
|
707
|
+
}
|
|
708
|
+
export function getApiTargetReadOnly(name) {
|
|
709
|
+
const targetConfig = readTargetConfig(readConfigFile({ readOnly: true }));
|
|
710
|
+
const normalized = normalizeApiTargetName(name);
|
|
711
|
+
const target = targetConfig.targets[normalized];
|
|
712
|
+
return target ? publicSummary(normalized, target, targetConfig.activeTarget) : undefined;
|
|
713
|
+
}
|
|
714
|
+
export function getActiveApiTargetReadOnly() {
|
|
715
|
+
const targetConfig = readTargetConfig(readConfigFile({ readOnly: true }));
|
|
716
|
+
const name = targetConfig.activeTarget;
|
|
717
|
+
const target = name ? targetConfig.targets[name] : undefined;
|
|
718
|
+
return name && target ? publicSummary(name, target, name) : undefined;
|
|
719
|
+
}
|
|
720
|
+
export function findApiTargetByBaseUrlReadOnly(apiBaseUrl) {
|
|
721
|
+
const normalizedUrl = normalizeApiTargetBaseUrl(apiBaseUrl);
|
|
722
|
+
return listApiTargetsReadOnly().find((target) => target.apiBaseUrl === normalizedUrl);
|
|
723
|
+
}
|
|
724
|
+
export function setApiTarget(input) {
|
|
725
|
+
const name = normalizeApiTargetName(input.name);
|
|
726
|
+
const nextTarget = validateApiTarget(input);
|
|
727
|
+
return withConfigMutation((config) => {
|
|
728
|
+
const targetConfig = readTargetConfig(config);
|
|
729
|
+
const existing = targetConfig.targets[name];
|
|
730
|
+
const legacyTargetName = resolveLegacyCredentialTargetName(config, targetConfig);
|
|
731
|
+
if ((existing?.credentials || legacyTargetName === name) &&
|
|
732
|
+
existing?.endpointSha256 !== nextTarget.endpointSha256) {
|
|
733
|
+
throw new Error('aops_target_endpoint_change_requires_remove');
|
|
734
|
+
}
|
|
735
|
+
const duplicate = Object.entries(targetConfig.targets)
|
|
736
|
+
.find(([candidateName, target]) => candidateName !== name && target.apiBaseUrl === nextTarget.apiBaseUrl);
|
|
737
|
+
if (duplicate)
|
|
738
|
+
throw new Error(`aops_target_endpoint_already_bound:${duplicate[0]}`);
|
|
739
|
+
targetConfig.targets[name] = { ...nextTarget, credentials: existing?.credentials };
|
|
740
|
+
targetConfig.activeTarget = input.activate || !targetConfig.activeTarget ? name : targetConfig.activeTarget;
|
|
741
|
+
config.clientTargets = targetConfig;
|
|
742
|
+
return publicSummary(name, targetConfig.targets[name], targetConfig.activeTarget);
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
export function useApiTarget(name) {
|
|
746
|
+
const normalized = normalizeApiTargetName(name);
|
|
747
|
+
return withConfigMutation((config) => {
|
|
748
|
+
const targetConfig = readTargetConfig(config);
|
|
749
|
+
const target = targetConfig.targets[normalized];
|
|
750
|
+
if (!Object.hasOwn(targetConfig.targets, normalized) || !target)
|
|
751
|
+
throw new Error(`aops_target_not_found:${normalized}`);
|
|
752
|
+
targetConfig.activeTarget = normalized;
|
|
753
|
+
config.clientTargets = targetConfig;
|
|
754
|
+
return publicSummary(normalized, target, normalized);
|
|
755
|
+
});
|
|
756
|
+
}
|
|
757
|
+
export function removeApiTarget(name) {
|
|
758
|
+
const normalized = normalizeApiTargetName(name);
|
|
759
|
+
withConfigMutation((config) => {
|
|
760
|
+
const targetConfig = readTargetConfig(config);
|
|
761
|
+
if (!Object.hasOwn(targetConfig.targets, normalized))
|
|
762
|
+
throw new Error(`aops_target_not_found:${normalized}`);
|
|
763
|
+
const legacyTargetName = resolveLegacyCredentialTargetName(config, targetConfig);
|
|
764
|
+
delete targetConfig.targets[normalized];
|
|
765
|
+
if (targetConfig.activeTarget === normalized)
|
|
766
|
+
targetConfig.activeTarget = undefined;
|
|
767
|
+
config.clientTargets = targetConfig;
|
|
768
|
+
if (legacyTargetName === normalized)
|
|
769
|
+
stripLegacyTokenFields(config);
|
|
770
|
+
});
|
|
771
|
+
cachedTokensByTarget.delete(tokenCacheKey(normalized));
|
|
772
|
+
loadedTargets.delete(tokenCacheKey(normalized));
|
|
773
|
+
}
|
|
774
|
+
export function recordApiTargetCompatibility(name, observation) {
|
|
775
|
+
const normalized = normalizeApiTargetName(name);
|
|
776
|
+
return withConfigMutation((config) => {
|
|
777
|
+
const targetConfig = readTargetConfig(config);
|
|
778
|
+
const target = targetConfig.targets[normalized];
|
|
779
|
+
if (!Object.hasOwn(targetConfig.targets, normalized) || !target)
|
|
780
|
+
throw new Error(`aops_target_not_found:${normalized}`);
|
|
781
|
+
target.compatibility = structuredClone(observation);
|
|
782
|
+
config.clientTargets = targetConfig;
|
|
783
|
+
return publicSummary(normalized, target, targetConfig.activeTarget);
|
|
784
|
+
});
|
|
785
|
+
}
|
|
786
|
+
export function getConfigApiServer() {
|
|
787
|
+
return getActiveApiTarget()?.apiBaseUrl;
|
|
788
|
+
}
|
|
789
|
+
function resolveTokenTarget(input) {
|
|
790
|
+
const targetConfig = readTargetConfig(readConfig());
|
|
791
|
+
const explicitName = input.targetName ? normalizeApiTargetName(input.targetName) : undefined;
|
|
792
|
+
if (explicitName) {
|
|
793
|
+
const target = targetConfig.targets[explicitName];
|
|
794
|
+
if (!Object.hasOwn(targetConfig.targets, explicitName) || !target)
|
|
795
|
+
throw new Error(`aops_target_not_found:${explicitName}`);
|
|
796
|
+
if (input.apiServer && normalizeApiTargetBaseUrl(input.apiServer) !== target.apiBaseUrl) {
|
|
797
|
+
throw new Error('aops_target_api_base_url_mismatch');
|
|
798
|
+
}
|
|
799
|
+
return { name: explicitName, target, existed: true, credentialRevision: target.credentials?.credentialRevision };
|
|
800
|
+
}
|
|
801
|
+
if (input.apiServer) {
|
|
802
|
+
const normalizedUrl = normalizeApiTargetBaseUrl(input.apiServer);
|
|
803
|
+
const match = Object.entries(targetConfig.targets).find(([, target]) => target.apiBaseUrl === normalizedUrl);
|
|
804
|
+
if (match)
|
|
805
|
+
return {
|
|
806
|
+
name: match[0],
|
|
807
|
+
target: match[1],
|
|
808
|
+
existed: true,
|
|
809
|
+
credentialRevision: match[1].credentials?.credentialRevision,
|
|
810
|
+
};
|
|
811
|
+
if (Object.keys(targetConfig.targets).length === 0) {
|
|
812
|
+
const target = validateApiTarget({
|
|
813
|
+
apiBaseUrl: normalizedUrl,
|
|
814
|
+
authProvider: input.verifiedAuthProvider,
|
|
815
|
+
});
|
|
816
|
+
return { name: 'default', target, existed: false };
|
|
817
|
+
}
|
|
818
|
+
throw new Error('aops_target_not_found_for_api_base_url:run_aops-cli_target_add');
|
|
819
|
+
}
|
|
820
|
+
const activeName = targetConfig.activeTarget;
|
|
821
|
+
const active = activeName ? targetConfig.targets[activeName] : undefined;
|
|
822
|
+
if (!activeName || !active)
|
|
823
|
+
throw new Error('aops_target_active_missing:run_aops-cli_target_use');
|
|
824
|
+
return { name: activeName, target: active, existed: true, credentialRevision: active.credentials?.credentialRevision };
|
|
825
|
+
}
|
|
826
|
+
function stripLegacyTokenFields(config) {
|
|
827
|
+
delete config.apiServer;
|
|
828
|
+
delete config.apiAccessToken;
|
|
829
|
+
delete config.apiRefreshToken;
|
|
830
|
+
delete config.apiAccessTokenEnc;
|
|
831
|
+
delete config.apiRefreshTokenEnc;
|
|
832
|
+
delete config.apiUserId;
|
|
833
|
+
}
|
|
834
|
+
export async function setApiTokensInConfig(tokens) {
|
|
835
|
+
const accessToken = normalizeNonEmpty(tokens.accessToken);
|
|
836
|
+
const refreshToken = normalizeNonEmpty(tokens.refreshToken);
|
|
837
|
+
if (!accessToken || !refreshToken)
|
|
838
|
+
throw new Error('aops_target_token_pair_required');
|
|
839
|
+
const resolved = resolveTokenTarget(tokens);
|
|
840
|
+
const [accessTokenEnc, refreshTokenEnc] = await Promise.all([
|
|
841
|
+
encryptToken(accessToken),
|
|
842
|
+
encryptToken(refreshToken),
|
|
843
|
+
]);
|
|
844
|
+
const [verifiedAccessToken, verifiedRefreshToken] = await Promise.all([
|
|
845
|
+
decryptToken(accessTokenEnc),
|
|
846
|
+
decryptToken(refreshTokenEnc),
|
|
847
|
+
]);
|
|
848
|
+
if (verifiedAccessToken !== accessToken || verifiedRefreshToken !== refreshToken) {
|
|
849
|
+
throw new Error('aops_target_token_encryption_verification_failed');
|
|
850
|
+
}
|
|
851
|
+
const credentialRevision = crypto.randomUUID();
|
|
852
|
+
const target = withConfigMutation((config) => {
|
|
853
|
+
const targetConfig = readTargetConfig(config);
|
|
854
|
+
if (tokens.expectedLegacyCredentialFingerprint !== undefined &&
|
|
855
|
+
legacyCredentialFingerprint(config) !== tokens.expectedLegacyCredentialFingerprint) {
|
|
856
|
+
throw new Error('aops_legacy_credentials_changed_during_migration');
|
|
857
|
+
}
|
|
858
|
+
const legacyTargetName = resolveLegacyCredentialTargetName(config, targetConfig);
|
|
859
|
+
let currentTarget = targetConfig.targets[resolved.name];
|
|
860
|
+
if (!currentTarget && resolved.existed)
|
|
861
|
+
throw new Error('aops_target_removed_during_token_write');
|
|
862
|
+
currentTarget ??= resolved.target;
|
|
863
|
+
if (currentTarget.endpointSha256 !== resolved.target.endpointSha256)
|
|
864
|
+
throw new Error('aops_target_endpoint_changed_during_token_write');
|
|
865
|
+
const duplicate = Object.entries(targetConfig.targets)
|
|
866
|
+
.find(([candidateName, candidate]) => candidateName !== resolved.name && candidate.apiBaseUrl === currentTarget.apiBaseUrl);
|
|
867
|
+
if (duplicate)
|
|
868
|
+
throw new Error(`aops_target_endpoint_already_bound:${duplicate[0]}`);
|
|
869
|
+
if (tokens.expectedCredentialRevision !== undefined &&
|
|
870
|
+
(currentTarget.credentials?.credentialRevision ?? null) !== tokens.expectedCredentialRevision) {
|
|
871
|
+
throw new Error('aops_target_credentials_changed_during_token_write');
|
|
872
|
+
}
|
|
873
|
+
currentTarget.credentials = {
|
|
874
|
+
schemaVersion: 1,
|
|
875
|
+
endpointSha256: currentTarget.endpointSha256,
|
|
876
|
+
credentialRevision,
|
|
877
|
+
accessTokenEnc,
|
|
878
|
+
refreshTokenEnc,
|
|
879
|
+
userId: normalizeNonEmpty(tokens.userId),
|
|
880
|
+
};
|
|
881
|
+
targetConfig.targets[resolved.name] = currentTarget;
|
|
882
|
+
targetConfig.activeTarget ??= resolved.name;
|
|
883
|
+
config.clientTargets = targetConfig;
|
|
884
|
+
if (legacyTargetName === resolved.name)
|
|
885
|
+
stripLegacyTokenFields(config);
|
|
886
|
+
return currentTarget;
|
|
887
|
+
});
|
|
888
|
+
cachedTokensByTarget.set(tokenCacheKey(resolved.name), {
|
|
889
|
+
targetName: resolved.name,
|
|
890
|
+
apiBaseUrl: target.apiBaseUrl,
|
|
891
|
+
credentialRevision,
|
|
892
|
+
accessToken,
|
|
893
|
+
refreshToken,
|
|
894
|
+
userId: normalizeNonEmpty(tokens.userId),
|
|
895
|
+
});
|
|
896
|
+
loadedTargets.add(tokenCacheKey(resolved.name));
|
|
897
|
+
return credentialRevision;
|
|
898
|
+
}
|
|
899
|
+
export function clearApiTokensInConfig(targetName) {
|
|
900
|
+
const result = withConfigMutation((config) => {
|
|
901
|
+
const targetConfig = readTargetConfig(config);
|
|
902
|
+
const selected = targetName ? normalizeApiTargetName(targetName) : targetConfig.activeTarget;
|
|
903
|
+
if (!selected)
|
|
904
|
+
throw new Error('aops_target_active_missing:run_aops-cli_target_use');
|
|
905
|
+
const target = targetConfig.targets[selected];
|
|
906
|
+
if (!Object.hasOwn(targetConfig.targets, selected) || !target)
|
|
907
|
+
throw new Error(`aops_target_not_found:${selected}`);
|
|
908
|
+
const legacyTargetName = resolveLegacyCredentialTargetName(config, targetConfig);
|
|
909
|
+
const cleared = Boolean(target.credentials) || legacyTargetName === selected;
|
|
910
|
+
delete target.credentials;
|
|
911
|
+
config.clientTargets = targetConfig;
|
|
912
|
+
if (legacyTargetName === selected)
|
|
913
|
+
stripLegacyTokenFields(config);
|
|
914
|
+
return { selected, cleared };
|
|
915
|
+
});
|
|
916
|
+
cachedTokensByTarget.delete(tokenCacheKey(result.selected));
|
|
917
|
+
loadedTargets.add(tokenCacheKey(result.selected));
|
|
918
|
+
return result.cleared;
|
|
919
|
+
}
|
|
920
|
+
async function readStoredTargetTokens(name, target, options = {}) {
|
|
921
|
+
const credentials = target.credentials;
|
|
922
|
+
if (!credentials || credentials.endpointSha256 !== target.endpointSha256)
|
|
923
|
+
return {};
|
|
924
|
+
const [accessToken, refreshToken] = await Promise.all([
|
|
925
|
+
decryptToken(credentials.accessTokenEnc, options),
|
|
926
|
+
decryptToken(credentials.refreshTokenEnc, options),
|
|
927
|
+
]);
|
|
928
|
+
if (!accessToken || !refreshToken)
|
|
929
|
+
return {};
|
|
930
|
+
return {
|
|
931
|
+
targetName: name,
|
|
932
|
+
apiBaseUrl: target.apiBaseUrl,
|
|
933
|
+
credentialRevision: credentials.credentialRevision,
|
|
934
|
+
accessToken,
|
|
935
|
+
refreshToken,
|
|
936
|
+
userId: credentials.userId,
|
|
937
|
+
};
|
|
938
|
+
}
|
|
939
|
+
export async function loadApiTokensFromConfig(targetName) {
|
|
940
|
+
let config = readConfig();
|
|
941
|
+
let targetConfig = readTargetConfig(config);
|
|
942
|
+
const legacyTargetName = resolveLegacyCredentialTargetName(config, targetConfig);
|
|
943
|
+
if (legacyTargetName) {
|
|
944
|
+
const expectedLegacyCredentialFingerprint = legacyCredentialFingerprint(config);
|
|
945
|
+
if (!expectedLegacyCredentialFingerprint)
|
|
946
|
+
throw new Error('aops_legacy_credentials_snapshot_missing');
|
|
947
|
+
if (targetConfig.targets[legacyTargetName]?.credentials) {
|
|
948
|
+
throw new Error('aops_legacy_credentials_target_already_has_credentials');
|
|
949
|
+
}
|
|
950
|
+
const accessTokenEnc = normalizeNonEmpty(config.apiAccessTokenEnc);
|
|
951
|
+
const refreshTokenEnc = normalizeNonEmpty(config.apiRefreshTokenEnc);
|
|
952
|
+
const plaintextAccessToken = normalizeNonEmpty(config.apiAccessToken);
|
|
953
|
+
const plaintextRefreshToken = normalizeNonEmpty(config.apiRefreshToken);
|
|
954
|
+
let accessToken;
|
|
955
|
+
let refreshToken;
|
|
956
|
+
if (accessTokenEnc && refreshTokenEnc) {
|
|
957
|
+
;
|
|
958
|
+
[accessToken, refreshToken] = await Promise.all([
|
|
959
|
+
decryptToken(accessTokenEnc),
|
|
960
|
+
decryptToken(refreshTokenEnc),
|
|
961
|
+
]);
|
|
962
|
+
}
|
|
963
|
+
if ((!accessToken || !refreshToken) && plaintextAccessToken && plaintextRefreshToken) {
|
|
964
|
+
accessToken = plaintextAccessToken;
|
|
965
|
+
refreshToken = plaintextRefreshToken;
|
|
966
|
+
}
|
|
967
|
+
if (!accessToken || !refreshToken) {
|
|
968
|
+
throw new Error('aops_legacy_credentials_incomplete_or_unreadable');
|
|
969
|
+
}
|
|
970
|
+
await setApiTokensInConfig({
|
|
971
|
+
targetName: legacyTargetName,
|
|
972
|
+
accessToken,
|
|
973
|
+
refreshToken,
|
|
974
|
+
userId: normalizeNonEmpty(config.apiUserId),
|
|
975
|
+
expectedCredentialRevision: null,
|
|
976
|
+
expectedLegacyCredentialFingerprint,
|
|
977
|
+
});
|
|
978
|
+
config = readConfig();
|
|
979
|
+
targetConfig = readTargetConfig(config);
|
|
980
|
+
}
|
|
981
|
+
const name = targetName ? normalizeApiTargetName(targetName) : targetConfig.activeTarget;
|
|
982
|
+
if (!name)
|
|
983
|
+
return;
|
|
984
|
+
const target = targetConfig.targets[name];
|
|
985
|
+
if (!target)
|
|
986
|
+
throw new Error(`aops_target_not_found:${name}`);
|
|
987
|
+
cachedTokensByTarget.delete(tokenCacheKey(name));
|
|
988
|
+
loadedTargets.add(tokenCacheKey(name));
|
|
989
|
+
const stored = await readStoredTargetTokens(name, target);
|
|
990
|
+
if (stored.accessToken && stored.refreshToken) {
|
|
991
|
+
cachedTokensByTarget.set(tokenCacheKey(name), stored);
|
|
992
|
+
return;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
export async function ensureApiTokensLoaded(targetName) {
|
|
996
|
+
const name = targetName ? normalizeApiTargetName(targetName) : getActiveApiTarget()?.name;
|
|
997
|
+
if (name && !loadedTargets.has(tokenCacheKey(name)))
|
|
998
|
+
await loadApiTokensFromConfig(name);
|
|
999
|
+
}
|
|
1000
|
+
export function getCachedApiTokens(targetName) {
|
|
1001
|
+
const name = targetName ? normalizeApiTargetName(targetName) : getActiveApiTarget()?.name;
|
|
1002
|
+
return name ? { ...(cachedTokensByTarget.get(tokenCacheKey(name)) ?? {}) } : {};
|
|
1003
|
+
}
|
|
1004
|
+
export async function readApiTokensFromConfigFile(targetName) {
|
|
1005
|
+
const targetConfig = readTargetConfig(readConfig());
|
|
1006
|
+
const name = targetName ? normalizeApiTargetName(targetName) : targetConfig.activeTarget;
|
|
1007
|
+
const target = name ? targetConfig.targets[name] : undefined;
|
|
1008
|
+
return name && target ? readStoredTargetTokens(name, target) : {};
|
|
1009
|
+
}
|
|
1010
|
+
export async function readApiTokensFromConfigFileReadOnly(targetName) {
|
|
1011
|
+
const targetConfig = readTargetConfig(readConfigFile({ readOnly: true }));
|
|
1012
|
+
const name = targetName ? normalizeApiTargetName(targetName) : targetConfig.activeTarget;
|
|
1013
|
+
const target = name ? targetConfig.targets[name] : undefined;
|
|
1014
|
+
return name && target ? readStoredTargetTokens(name, target, { readOnly: true }) : {};
|
|
1015
|
+
}
|