@oxygen-agent/cli 1.287.12 → 1.310.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 +1 -1
- package/dist/cli-values.d.ts +18 -0
- package/dist/cli-values.js +66 -0
- package/dist/credentials.d.ts +22 -2
- package/dist/credentials.js +80 -17
- package/dist/help.js +1 -1
- package/dist/http-client.d.ts +2 -0
- package/dist/http-client.js +68 -30
- package/dist/index.js +789 -243
- package/dist/knowledge-mirror.d.ts +10 -0
- package/dist/knowledge-mirror.js +18 -0
- package/dist/run-wait.js +2 -26
- package/dist/runtime.d.ts +63 -4
- package/dist/runtime.js +113 -3
- package/node_modules/@oxygen/shared/dist/deprecation-registry.js +2 -18
- package/node_modules/@oxygen/shared/dist/error-redaction.d.ts +80 -0
- package/node_modules/@oxygen/shared/dist/error-redaction.js +223 -0
- package/node_modules/@oxygen/shared/dist/file-import.js +9 -27
- package/node_modules/@oxygen/shared/dist/identifiers.d.ts +23 -0
- package/node_modules/@oxygen/shared/dist/identifiers.js +48 -0
- package/node_modules/@oxygen/shared/dist/index.d.ts +7 -1
- package/node_modules/@oxygen/shared/dist/index.js +7 -1
- package/node_modules/@oxygen/shared/dist/knowledge-constants.d.ts +2 -0
- package/node_modules/@oxygen/shared/dist/knowledge-constants.js +4 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.d.ts +24 -0
- package/node_modules/@oxygen/shared/dist/knowledge-seed-content.js +301 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.d.ts +19 -0
- package/node_modules/@oxygen/shared/dist/linkedin-url.js +105 -0
- package/node_modules/@oxygen/shared/dist/log.d.ts +3 -0
- package/node_modules/@oxygen/shared/dist/log.js +65 -6
- package/node_modules/@oxygen/shared/dist/redaction.d.ts +1 -0
- package/node_modules/@oxygen/shared/dist/redaction.js +15 -3
- package/node_modules/@oxygen/shared/dist/sequences.d.ts +11 -3
- package/node_modules/@oxygen/shared/dist/sequences.js +11 -2
- package/node_modules/@oxygen/shared/dist/timing.d.ts +10 -0
- package/node_modules/@oxygen/shared/dist/timing.js +12 -0
- package/node_modules/@oxygen/shared/dist/type-guards.d.ts +15 -0
- package/node_modules/@oxygen/shared/dist/type-guards.js +17 -0
- package/node_modules/@oxygen/shared/dist/version.d.ts +2 -1
- package/node_modules/@oxygen/shared/dist/version.js +33 -2
- package/node_modules/@oxygen/workflows/dist/index.d.ts +1 -1
- package/node_modules/@oxygen/workflows/dist/index.js +1 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.d.ts +41 -0
- package/node_modules/@oxygen/workflows/dist/usage-estimate.js +203 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/** Parse a `--limit`/`--interval`-style flag into a positive integer, or undefined when absent. */
|
|
2
|
+
export declare function readPositiveInt(value: string | undefined): number | undefined;
|
|
3
|
+
/** Read a string field from an unknown record-shaped value, or null when it is missing/non-string. */
|
|
4
|
+
export declare function readRecordString(value: unknown, key: string): string | null;
|
|
5
|
+
/** Parse a JSON string that must decode to a plain object, throwing a typed invalid_json error otherwise. */
|
|
6
|
+
export declare function parseJsonObject(value: string): Record<string, unknown>;
|
|
7
|
+
/** Read a JSON-object CLI option (e.g. `--selection-json`, `--filter-tree-json`), or undefined when absent. */
|
|
8
|
+
export declare function readJsonObjectOption(value: string | undefined): Record<string, unknown> | undefined;
|
|
9
|
+
/** Reject the mutually-exclusive `--live` + `--dry-run` combination with a typed conflicting_flags error. */
|
|
10
|
+
export declare function assertModeFlagsExclusive(options: {
|
|
11
|
+
live?: boolean;
|
|
12
|
+
dryRun?: boolean;
|
|
13
|
+
}): void;
|
|
14
|
+
/** Resolve run mode from `--live`/`--dry-run` flags: live only when `--live` is set, otherwise dry_run. */
|
|
15
|
+
export declare function resolveLiveDryRunMode(options: {
|
|
16
|
+
live?: boolean;
|
|
17
|
+
dryRun?: boolean;
|
|
18
|
+
}): "live" | "dry_run";
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Shared CLI value/option parsers, extracted so the same reader has exactly one
|
|
2
|
+
// definition instead of byte-identical copies drifting apart across index.ts and
|
|
3
|
+
// run-wait.ts (OXY-3733). Every helper here is pure and depends only on the
|
|
4
|
+
// leaf `OxygenError` primitive plus `readOption` from `./util.js`, so this module
|
|
5
|
+
// is a dependency-free leaf that index.ts and run-wait.ts can both import without
|
|
6
|
+
// a cycle. All symbols throw the same typed OxygenError codes the copies did.
|
|
7
|
+
import { OxygenError } from "@oxygen/shared";
|
|
8
|
+
import { readOption } from "./util.js";
|
|
9
|
+
/** Parse a `--limit`/`--interval`-style flag into a positive integer, or undefined when absent. */
|
|
10
|
+
export function readPositiveInt(value) {
|
|
11
|
+
const trimmed = value?.trim();
|
|
12
|
+
if (!trimmed)
|
|
13
|
+
return undefined;
|
|
14
|
+
const parsed = Number(trimmed);
|
|
15
|
+
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
16
|
+
throw new OxygenError("invalid_number", "Expected a positive integer.", {
|
|
17
|
+
details: { value },
|
|
18
|
+
exitCode: 1,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
/** Read a string field from an unknown record-shaped value, or null when it is missing/non-string. */
|
|
24
|
+
export function readRecordString(value, key) {
|
|
25
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
26
|
+
return null;
|
|
27
|
+
const entry = value[key];
|
|
28
|
+
return typeof entry === "string" ? entry : null;
|
|
29
|
+
}
|
|
30
|
+
/** Parse a JSON string that must decode to a plain object, throwing a typed invalid_json error otherwise. */
|
|
31
|
+
export function parseJsonObject(value) {
|
|
32
|
+
let parsed;
|
|
33
|
+
try {
|
|
34
|
+
parsed = JSON.parse(value);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
throw new OxygenError("invalid_json", "Input must be valid JSON.", {
|
|
38
|
+
details: { reason: error instanceof Error ? error.message : "unknown" },
|
|
39
|
+
exitCode: 1,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
43
|
+
throw new OxygenError("invalid_json", "Input JSON must be an object.", {
|
|
44
|
+
exitCode: 1,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
return parsed;
|
|
48
|
+
}
|
|
49
|
+
/** Read a JSON-object CLI option (e.g. `--selection-json`, `--filter-tree-json`), or undefined when absent. */
|
|
50
|
+
export function readJsonObjectOption(value) {
|
|
51
|
+
const raw = readOption(value);
|
|
52
|
+
if (!raw)
|
|
53
|
+
return undefined;
|
|
54
|
+
return parseJsonObject(raw);
|
|
55
|
+
}
|
|
56
|
+
/** Reject the mutually-exclusive `--live` + `--dry-run` combination with a typed conflicting_flags error. */
|
|
57
|
+
export function assertModeFlagsExclusive(options) {
|
|
58
|
+
if (options.live === true && options.dryRun === true) {
|
|
59
|
+
throw new OxygenError("conflicting_flags", "Pass either --live or --dry-run, not both.", { exitCode: 1 });
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Resolve run mode from `--live`/`--dry-run` flags: live only when `--live` is set, otherwise dry_run. */
|
|
63
|
+
export function resolveLiveDryRunMode(options) {
|
|
64
|
+
assertModeFlagsExclusive(options);
|
|
65
|
+
return options.live === true ? "live" : "dry_run";
|
|
66
|
+
}
|
package/dist/credentials.d.ts
CHANGED
|
@@ -26,10 +26,30 @@ type CredentialProfilesState = {
|
|
|
26
26
|
profiles: CredentialProfile[];
|
|
27
27
|
};
|
|
28
28
|
export declare function defaultApiUrl(env?: NodeJS.ProcessEnv): string;
|
|
29
|
+
/**
|
|
30
|
+
* Which host this command will talk to, and what put it there — resolved from the
|
|
31
|
+
* same read that loads the credentials, so naming it costs nothing.
|
|
32
|
+
*
|
|
33
|
+
* A gate rejection that omits this is unreadable: dev's numbers and prod's numbers
|
|
34
|
+
* look identical, and a drifted profile turns a prod-labelled binary into a report
|
|
35
|
+
* about dev that reads as a production outage (OXY-4091).
|
|
36
|
+
*/
|
|
37
|
+
export type CliEndpointContext = {
|
|
38
|
+
apiUrl: string;
|
|
39
|
+
/** Active profile name, or null when no named profile supplied the credentials. */
|
|
40
|
+
profile: string | null;
|
|
41
|
+
apiUrlSource: "env" | "profile" | "default";
|
|
42
|
+
};
|
|
43
|
+
export type LoadedCredentials = {
|
|
44
|
+
credentials: StoredCredentials | null;
|
|
45
|
+
endpoint: CliEndpointContext;
|
|
46
|
+
};
|
|
47
|
+
export declare function loadCredentialsWithEndpoint(env?: NodeJS.ProcessEnv): Promise<LoadedCredentials>;
|
|
29
48
|
export declare function loadCredentials(env?: NodeJS.ProcessEnv): Promise<StoredCredentials | null>;
|
|
30
49
|
export declare function saveCredentials(credentials: StoredCredentials, env?: NodeJS.ProcessEnv, options?: {
|
|
31
50
|
profile?: string;
|
|
32
51
|
activate?: boolean;
|
|
52
|
+
allowHostChange?: boolean;
|
|
33
53
|
}): Promise<string>;
|
|
34
54
|
export declare function clearCredentials(env?: NodeJS.ProcessEnv, options?: {
|
|
35
55
|
profile?: string;
|
|
@@ -46,11 +66,11 @@ export type ProfileResolution = {
|
|
|
46
66
|
exists: boolean;
|
|
47
67
|
};
|
|
48
68
|
export declare function resolveActiveProfile(env?: NodeJS.ProcessEnv): Promise<ProfileResolution>;
|
|
49
|
-
export declare function pickProfileNameForIdentity(organizationId: string, candidateSeed: string, env?: NodeJS.ProcessEnv): Promise<{
|
|
69
|
+
export declare function pickProfileNameForIdentity(organizationId: string, candidateSeed: string, apiUrl: string, env?: NodeJS.ProcessEnv): Promise<{
|
|
50
70
|
name: string;
|
|
51
71
|
renamed: boolean;
|
|
52
72
|
}>;
|
|
53
|
-
export declare function pickProfileNameForUserSession(userId: string, candidateSeed: string, env?: NodeJS.ProcessEnv): Promise<{
|
|
73
|
+
export declare function pickProfileNameForUserSession(userId: string, candidateSeed: string, apiUrl: string, env?: NodeJS.ProcessEnv): Promise<{
|
|
54
74
|
name: string;
|
|
55
75
|
renamed: boolean;
|
|
56
76
|
}>;
|
package/dist/credentials.js
CHANGED
|
@@ -4,6 +4,7 @@ import { homedir } from "node:os";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { promisify } from "node:util";
|
|
6
6
|
import { OxygenError } from "@oxygen/shared";
|
|
7
|
+
import { sameApiOrigin } from "./runtime.js";
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
const SERVICE_NAME = "oxygen-cli";
|
|
9
10
|
const ACCOUNT_NAME = "default";
|
|
@@ -27,7 +28,11 @@ function withApiUrlOverride(credentials, env) {
|
|
|
27
28
|
return credentials;
|
|
28
29
|
return { ...credentials, apiUrl: override };
|
|
29
30
|
}
|
|
30
|
-
export async function
|
|
31
|
+
export async function loadCredentialsWithEndpoint(env = process.env) {
|
|
32
|
+
// OXYGEN_API_URL outranks every stored apiUrl (see withApiUrlOverride), so when
|
|
33
|
+
// it is set it IS the reason we are calling this host — and unsetting it, not
|
|
34
|
+
// switching profiles, is the remedy.
|
|
35
|
+
const envApiUrl = readEnvApiUrl(env);
|
|
31
36
|
const envToken = readEnvToken(env);
|
|
32
37
|
if (envToken) {
|
|
33
38
|
const credentials = {
|
|
@@ -37,17 +42,41 @@ export async function loadCredentials(env = process.env) {
|
|
|
37
42
|
const authKind = inferAuthKind(envToken);
|
|
38
43
|
if (authKind)
|
|
39
44
|
credentials.authKind = authKind;
|
|
40
|
-
return
|
|
45
|
+
return {
|
|
46
|
+
credentials,
|
|
47
|
+
endpoint: {
|
|
48
|
+
apiUrl: credentials.apiUrl,
|
|
49
|
+
profile: null,
|
|
50
|
+
apiUrlSource: envApiUrl ? "env" : "default",
|
|
51
|
+
},
|
|
52
|
+
};
|
|
41
53
|
}
|
|
54
|
+
const unauthenticated = {
|
|
55
|
+
credentials: null,
|
|
56
|
+
endpoint: {
|
|
57
|
+
apiUrl: defaultApiUrl(env),
|
|
58
|
+
profile: null,
|
|
59
|
+
apiUrlSource: envApiUrl ? "env" : "default",
|
|
60
|
+
},
|
|
61
|
+
};
|
|
42
62
|
const document = await readSystemCredentialDocument(env);
|
|
43
63
|
if (!document)
|
|
44
|
-
return
|
|
64
|
+
return unauthenticated;
|
|
45
65
|
const profiles = readProfiles(document);
|
|
46
66
|
const requestedProfile = readEnvProfile(env);
|
|
47
67
|
const activeProfile = requestedProfile ?? readProfileName(document.activeProfile) ?? DEFAULT_PROFILE_NAME;
|
|
48
68
|
const profileCredentials = profiles[activeProfile];
|
|
49
|
-
if (profileCredentials)
|
|
50
|
-
|
|
69
|
+
if (profileCredentials) {
|
|
70
|
+
const credentials = withApiUrlOverride(profileCredentials, env);
|
|
71
|
+
return {
|
|
72
|
+
credentials,
|
|
73
|
+
endpoint: {
|
|
74
|
+
apiUrl: credentials.apiUrl,
|
|
75
|
+
profile: activeProfile,
|
|
76
|
+
apiUrlSource: envApiUrl ? "env" : "profile",
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
51
80
|
if (requestedProfile) {
|
|
52
81
|
throw new OxygenError("profile_not_found", `Oxygen CLI profile "${requestedProfile}" is not stored.`, {
|
|
53
82
|
details: { profile: requestedProfile },
|
|
@@ -55,18 +84,47 @@ export async function loadCredentials(env = process.env) {
|
|
|
55
84
|
});
|
|
56
85
|
}
|
|
57
86
|
const storedCredentials = parseStoredCredentialsObject(document);
|
|
58
|
-
|
|
87
|
+
if (!storedCredentials)
|
|
88
|
+
return unauthenticated;
|
|
89
|
+
const credentials = withApiUrlOverride(storedCredentials, env);
|
|
90
|
+
return {
|
|
91
|
+
credentials,
|
|
92
|
+
endpoint: {
|
|
93
|
+
apiUrl: credentials.apiUrl,
|
|
94
|
+
profile: null,
|
|
95
|
+
apiUrlSource: envApiUrl ? "env" : "profile",
|
|
96
|
+
},
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
export async function loadCredentials(env = process.env) {
|
|
100
|
+
return (await loadCredentialsWithEndpoint(env)).credentials;
|
|
59
101
|
}
|
|
60
102
|
export async function saveCredentials(credentials, env = process.env, options = {}) {
|
|
61
103
|
const normalized = normalizeStoredCredentialsRecord(credentials);
|
|
62
104
|
const existing = await readSystemCredentialDocument(env);
|
|
63
105
|
const profile = normalizeProfileName(options.profile ?? readEnvProfile(env) ?? existing?.activeProfile ?? DEFAULT_PROFILE_NAME);
|
|
64
106
|
const profiles = existing ? readProfiles(existing) : {};
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
107
|
+
// A stored profile is bound to the host it was logged into. Without this, a dev
|
|
108
|
+
// login that names no profile writes its `apiUrl` into whatever profile is
|
|
109
|
+
// active — which is how the profile literally named `default` came to point at
|
|
110
|
+
// dev, taking every prod runbook with it (OXY-4109). Repointing is still
|
|
111
|
+
// possible, but it has to be asked for.
|
|
112
|
+
const prior = profiles[profile];
|
|
113
|
+
if (prior && !options.allowHostChange && !sameApiOrigin(prior.apiUrl, normalized.apiUrl)) {
|
|
114
|
+
throw new OxygenError("profile_host_conflict", `CLI profile "${profile}" is bound to ${prior.apiUrl}, and this login targets ${normalized.apiUrl}. `
|
|
115
|
+
+ `Storing it here would silently repoint every command that uses "${profile}" at the other `
|
|
116
|
+
+ "environment. Re-run with `--profile <name>` to keep them apart, or `--force` to repoint "
|
|
117
|
+
+ `"${profile}" on purpose.`, {
|
|
118
|
+
details: {
|
|
119
|
+
profile,
|
|
120
|
+
stored_api_url: prior.apiUrl,
|
|
121
|
+
requested_api_url: normalized.apiUrl,
|
|
122
|
+
},
|
|
123
|
+
exitCode: 1,
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
if (!normalized.identity && prior?.identity && prior.token === normalized.token) {
|
|
127
|
+
normalized.identity = prior.identity;
|
|
70
128
|
}
|
|
71
129
|
profiles[profile] = normalized;
|
|
72
130
|
const activeProfile = options.activate === false
|
|
@@ -159,10 +217,15 @@ export async function resolveActiveProfile(env = process.env) {
|
|
|
159
217
|
: "default";
|
|
160
218
|
return { name: candidate, source, credentials, exists: Boolean(credentials) };
|
|
161
219
|
}
|
|
162
|
-
async function pickProfileName(matches, candidateSeed, env) {
|
|
220
|
+
async function pickProfileName(matches, candidateSeed, apiUrl, env) {
|
|
163
221
|
const document = await readSystemCredentialDocument(env);
|
|
164
222
|
const profiles = document ? readProfiles(document) : {};
|
|
165
|
-
|
|
223
|
+
// Identity alone is not enough to reuse a profile: the same person and the same
|
|
224
|
+
// org slug exist on dev and on prod, so matching on identity across hosts is
|
|
225
|
+
// what lets a dev login land on top of a production profile (OXY-4109). A
|
|
226
|
+
// profile is only reusable when it already points at the host we are logging in
|
|
227
|
+
// to; otherwise we name a new one.
|
|
228
|
+
const existing = Object.entries(profiles).find(([, credentials]) => sameApiOrigin(credentials.apiUrl, apiUrl) && matches(credentials));
|
|
166
229
|
if (existing)
|
|
167
230
|
return { name: existing[0], renamed: false };
|
|
168
231
|
const base = normalizeProfileName(candidateSeed);
|
|
@@ -178,13 +241,13 @@ async function pickProfileName(matches, candidateSeed, env) {
|
|
|
178
241
|
exitCode: 1,
|
|
179
242
|
});
|
|
180
243
|
}
|
|
181
|
-
export async function pickProfileNameForIdentity(organizationId, candidateSeed, env = process.env) {
|
|
244
|
+
export async function pickProfileNameForIdentity(organizationId, candidateSeed, apiUrl, env = process.env) {
|
|
182
245
|
return pickProfileName((credentials) => credentials.identity?.organization?.id === organizationId ||
|
|
183
|
-
credentials.activeOrganization?.id === organizationId, candidateSeed, env);
|
|
246
|
+
credentials.activeOrganization?.id === organizationId, candidateSeed, apiUrl, env);
|
|
184
247
|
}
|
|
185
|
-
export async function pickProfileNameForUserSession(userId, candidateSeed, env = process.env) {
|
|
248
|
+
export async function pickProfileNameForUserSession(userId, candidateSeed, apiUrl, env = process.env) {
|
|
186
249
|
return pickProfileName((credentials) => credentials.authKind === "user_session" &&
|
|
187
|
-
credentials.identity?.user.id === userId, candidateSeed, env);
|
|
250
|
+
credentials.identity?.user.id === userId, candidateSeed, apiUrl, env);
|
|
188
251
|
}
|
|
189
252
|
export async function switchCredentialProfile(profile, env = process.env) {
|
|
190
253
|
const document = await readSystemCredentialDocument(env);
|
package/dist/help.js
CHANGED
|
@@ -22,7 +22,7 @@ const HELP_GROUPS = [
|
|
|
22
22
|
heading: "Data (tables, CRM, dashboards):",
|
|
23
23
|
commands: [
|
|
24
24
|
"tables", "rows", "cells", "columns", "action-column", "enrich-column",
|
|
25
|
-
"enrichment", "table-runs", "table-ingestions", "runs", "crm",
|
|
25
|
+
"enrichment", "table-runs", "table-ingestions", "runs", "crm", "signals",
|
|
26
26
|
"dashboards", "projects", "signup-leads",
|
|
27
27
|
],
|
|
28
28
|
},
|
package/dist/http-client.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type StoredCredentials } from "./credentials.js";
|
|
2
|
+
import { type CliEndpoint } from "./runtime.js";
|
|
2
3
|
type RequestOptions = {
|
|
3
4
|
method?: "GET" | "POST" | "PATCH" | "DELETE";
|
|
4
5
|
body?: Record<string, unknown>;
|
|
@@ -16,5 +17,6 @@ export declare function requestOxygen<T>(// skipcq: JS-R1005
|
|
|
16
17
|
path: string, options?: RequestOptions): Promise<T>;
|
|
17
18
|
export declare function ensureFreshCliForApiUrl(apiUrl: string, options?: {
|
|
18
19
|
fetch?: typeof fetch;
|
|
20
|
+
endpoint?: CliEndpoint;
|
|
19
21
|
}): Promise<void>;
|
|
20
22
|
export {};
|
package/dist/http-client.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { OXYGEN_VERSION, OxygenError, isCliResult, isVersionGreater, readEnvelopeCompatibility, vercelProtectionBypassHeaders, withRetryAfterDetails, } from "@oxygen/shared";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
|
-
import { defaultApiUrl,
|
|
4
|
-
import { isProdApiUrl, resolveCliUpdateGuidance } from "./runtime.js";
|
|
3
|
+
import { defaultApiUrl, loadCredentialsWithEndpoint } from "./credentials.js";
|
|
4
|
+
import { assertResolvedApiUrl, cliEndpointDetails, describeCliEndpoint, isProdApiUrl, resolveCliUpdateGuidance, } from "./runtime.js";
|
|
5
5
|
const DEFAULT_REQUEST_TIMEOUT_MS = 120_000;
|
|
6
6
|
const CLI_COMPATIBILITY_CHECK_TIMEOUT_MS = 5_000;
|
|
7
7
|
const cliCompatibilityCheckKeys = new Set();
|
|
8
8
|
export async function requestOxygen(// skipcq: JS-R1005
|
|
9
9
|
path, options = {}) {
|
|
10
|
-
const
|
|
11
|
-
|
|
10
|
+
const loaded = options.credentials === undefined && options.requireAuth !== false
|
|
11
|
+
? await loadCredentialsWithEndpoint()
|
|
12
|
+
: null;
|
|
13
|
+
const credentials = options.credentials ?? loaded?.credentials ?? null;
|
|
12
14
|
if (!credentials && options.requireAuth !== false) {
|
|
13
15
|
throw new OxygenError("not_logged_in", "Run `oxygen login` before using CLI commands.", {
|
|
14
16
|
exitCode: 1,
|
|
15
17
|
});
|
|
16
18
|
}
|
|
17
19
|
const apiUrl = credentials?.apiUrl ?? defaultApiUrl();
|
|
20
|
+
// Which host, on whose behalf. Carried into every rejection below — a gate error
|
|
21
|
+
// that names only version numbers describes dev and prod identically (OXY-4091).
|
|
22
|
+
const endpoint = loaded?.endpoint ?? { apiUrl, profile: null };
|
|
23
|
+
// Before anything is dialed: if the caller pinned a host, this is where a
|
|
24
|
+
// command aimed at the wrong environment stops. It has to run ahead of the
|
|
25
|
+
// freshness probe, or the first thing a mispointed prod runbook gets is a
|
|
26
|
+
// version complaint from dev instead of the host mismatch that explains it.
|
|
27
|
+
assertResolvedApiUrl(endpoint);
|
|
18
28
|
const fetchImpl = options.fetch ?? fetch;
|
|
19
29
|
if (path !== "/api/health" && shouldCheckCliCompatibility(options)) {
|
|
20
|
-
await ensureFreshCliForApiUrl(apiUrl, { fetch: fetchImpl });
|
|
30
|
+
await ensureFreshCliForApiUrl(apiUrl, { fetch: fetchImpl, endpoint });
|
|
21
31
|
}
|
|
22
32
|
const traceId = options.traceId ?? randomUUID();
|
|
23
33
|
const headers = {
|
|
@@ -93,14 +103,14 @@ path, options = {}) {
|
|
|
93
103
|
}
|
|
94
104
|
const envelope = await readEnvelope(response);
|
|
95
105
|
const compatibility = readEnvelopeCompatibility(envelope);
|
|
96
|
-
assertCliMeetsMinimumApiVersion(compatibility, options,
|
|
97
|
-
warnIfCliIsOlderThanApi(compatibility.version,
|
|
106
|
+
assertCliMeetsMinimumApiVersion(compatibility, options, endpoint);
|
|
107
|
+
warnIfCliIsOlderThanApi(compatibility.version, endpoint);
|
|
98
108
|
if (!response.ok || !envelope.ok) {
|
|
99
109
|
const failure = envelope;
|
|
100
110
|
const responseTraceId = response.headers.get("x-oxygen-trace-id") ?? traceId;
|
|
101
111
|
const details = withRetryAfterDetails(failure.error.details, response);
|
|
102
|
-
throw new OxygenError(failure.error.code, failure.error.message, {
|
|
103
|
-
details: withTraceDetails(details, responseTraceId, compatibility,
|
|
112
|
+
throw new OxygenError(failure.error.code, describeFailureAgainstEndpoint(failure.error.code, failure.error.message, endpoint), {
|
|
113
|
+
details: withTraceDetails(details, responseTraceId, compatibility, endpoint),
|
|
104
114
|
exitCode: 1,
|
|
105
115
|
});
|
|
106
116
|
}
|
|
@@ -114,10 +124,31 @@ export async function ensureFreshCliForApiUrl(apiUrl, options = {}) {
|
|
|
114
124
|
if (!compatibility) {
|
|
115
125
|
return;
|
|
116
126
|
}
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
const endpoint = options.endpoint ?? { apiUrl, profile: null };
|
|
128
|
+
assertCliMeetsMinimumApiVersion(compatibility, {}, endpoint);
|
|
129
|
+
warnIfCliIsOlderThanApi(compatibility.version, endpoint);
|
|
119
130
|
cliCompatibilityCheckKeys.add(checkKey);
|
|
120
131
|
}
|
|
132
|
+
// The codes that mean "you may be talking to the wrong place": a floor rejection
|
|
133
|
+
// and an auth rejection both read as catastrophic when the reader assumes prod and
|
|
134
|
+
// the request went to dev. The server cannot know the caller's profile, so the
|
|
135
|
+
// client appends what only it knows. `cli_update_required` is rebuilt outright —
|
|
136
|
+
// the server's stock "run `oxygen update`" is actively wrong advice for a CLI that
|
|
137
|
+
// is merely pointed at the wrong host (OXY-4091).
|
|
138
|
+
const ENDPOINT_CONTEXT_CODES = new Set(["unauthorized", "forbidden"]);
|
|
139
|
+
function describeFailureAgainstEndpoint(code, message, endpoint) {
|
|
140
|
+
const target = describeCliEndpoint(endpoint);
|
|
141
|
+
if (!target)
|
|
142
|
+
return message;
|
|
143
|
+
if (code === "cli_update_required") {
|
|
144
|
+
return `This Oxygen API requires a newer CLI. You are calling ${target}. `
|
|
145
|
+
+ `${resolveCliUpdateGuidance(endpoint).failureInstruction}`;
|
|
146
|
+
}
|
|
147
|
+
if (ENDPOINT_CONTEXT_CODES.has(code)) {
|
|
148
|
+
return `${message} You are calling ${target}.`;
|
|
149
|
+
}
|
|
150
|
+
return message;
|
|
151
|
+
}
|
|
121
152
|
function resolveSelectedOrganization(credentials, explicit) {
|
|
122
153
|
const fromOption = explicit?.trim();
|
|
123
154
|
if (fromOption)
|
|
@@ -161,12 +192,12 @@ async function readHealthCompatibility(apiUrl, fetchImpl) {
|
|
|
161
192
|
}
|
|
162
193
|
}
|
|
163
194
|
const staleCliWarningVersions = new Set();
|
|
164
|
-
function warnIfCliIsOlderThanApi(serverVersion,
|
|
195
|
+
function warnIfCliIsOlderThanApi(serverVersion, endpoint) {
|
|
165
196
|
if (!serverVersion)
|
|
166
197
|
return;
|
|
167
198
|
if (!isVersionGreater(serverVersion, OXYGEN_VERSION))
|
|
168
199
|
return;
|
|
169
|
-
const guidance = resolveCliUpdateGuidance(
|
|
200
|
+
const guidance = resolveCliUpdateGuidance(endpoint);
|
|
170
201
|
const warningKey = `${serverVersion}|${guidance.channel}|${guidance.binaryName}`;
|
|
171
202
|
if (staleCliWarningVersions.has(warningKey))
|
|
172
203
|
return;
|
|
@@ -174,28 +205,35 @@ function warnIfCliIsOlderThanApi(serverVersion, apiUrl) {
|
|
|
174
205
|
process.stderr.write(`[${guidance.binaryName}] CLI version ${OXYGEN_VERSION} is older than Oxygen API version ${serverVersion}. `
|
|
175
206
|
+ `${guidance.warningInstruction}\n`);
|
|
176
207
|
}
|
|
177
|
-
function assertCliMeetsMinimumApiVersion(compatibility, options,
|
|
208
|
+
function assertCliMeetsMinimumApiVersion(compatibility, options, endpoint) {
|
|
178
209
|
if (options.enforceMinimumCliVersion === false)
|
|
179
210
|
return;
|
|
180
211
|
const minimumCliVersion = compatibility.minimumCliVersion;
|
|
181
212
|
if (!minimumCliVersion || !isVersionGreater(minimumCliVersion, OXYGEN_VERSION))
|
|
182
213
|
return;
|
|
183
|
-
const guidance = resolveCliUpdateGuidance(
|
|
184
|
-
|
|
214
|
+
const guidance = resolveCliUpdateGuidance(endpoint);
|
|
215
|
+
const target = describeCliEndpoint(endpoint);
|
|
216
|
+
throw new OxygenError("cli_update_required", `This Oxygen API requires a newer CLI.${target ? ` You are calling ${target}.` : ""} `
|
|
217
|
+
+ `${guidance.failureInstruction}`, {
|
|
185
218
|
details: {
|
|
186
219
|
client_version: OXYGEN_VERSION,
|
|
187
220
|
...(compatibility.version ? { server_version: compatibility.version } : {}),
|
|
188
221
|
minimum_cli_version: minimumCliVersion,
|
|
222
|
+
...cliEndpointDetails(endpoint),
|
|
189
223
|
...guidance.details,
|
|
190
224
|
},
|
|
191
225
|
exitCode: 1,
|
|
192
226
|
});
|
|
193
227
|
}
|
|
194
|
-
function withTraceDetails(details, traceId, compatibility,
|
|
228
|
+
function withTraceDetails(details, traceId, compatibility, endpoint) {
|
|
195
229
|
const serverVersion = compatibility.version;
|
|
230
|
+
const guidance = resolveCliUpdateGuidance(endpoint);
|
|
196
231
|
const fields = {
|
|
197
232
|
trace_id: traceId,
|
|
198
233
|
client_version: OXYGEN_VERSION,
|
|
234
|
+
// Every failure carries the host it was sent to. `unauthorized` on the wrong
|
|
235
|
+
// host looks exactly like a revoked token on the right one (OXY-4091).
|
|
236
|
+
...cliEndpointDetails(endpoint),
|
|
199
237
|
};
|
|
200
238
|
if (serverVersion)
|
|
201
239
|
fields.server_version = serverVersion;
|
|
@@ -204,20 +242,20 @@ function withTraceDetails(details, traceId, compatibility, apiUrl) {
|
|
|
204
242
|
if ((serverVersion && isVersionGreater(serverVersion, OXYGEN_VERSION))
|
|
205
243
|
|| (compatibility.minimumCliVersion
|
|
206
244
|
&& isVersionGreater(compatibility.minimumCliVersion, OXYGEN_VERSION))) {
|
|
207
|
-
Object.assign(fields,
|
|
208
|
-
}
|
|
209
|
-
if (details && typeof details === "object" && !Array.isArray(details)) {
|
|
210
|
-
return {
|
|
211
|
-
...details,
|
|
212
|
-
...fields,
|
|
213
|
-
};
|
|
245
|
+
Object.assign(fields, guidance.details);
|
|
214
246
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
247
|
+
const merged = details && typeof details === "object" && !Array.isArray(details)
|
|
248
|
+
? { ...details, ...fields }
|
|
249
|
+
: details === undefined
|
|
250
|
+
? { ...fields }
|
|
251
|
+
: { details, ...fields };
|
|
252
|
+
// The server tells every rejected CLI to run `oxygen update`, because it cannot
|
|
253
|
+
// see which binary or profile called it. On a non-production endpoint that is a
|
|
254
|
+
// closed loop — npm can only reinstall the version this floor is rejecting — so
|
|
255
|
+
// the client, which does know, removes the fix that cannot work (OXY-4091).
|
|
256
|
+
if (guidance.channel === "profile")
|
|
257
|
+
delete merged.cli_update_command;
|
|
258
|
+
return merged;
|
|
221
259
|
}
|
|
222
260
|
function resolveRequestTimeoutMs(value) {
|
|
223
261
|
if (value === undefined)
|