@alphafox/cli 0.1.5 → 0.3.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +42 -4
- package/dist/auth/browser-login.d.ts +30 -0
- package/dist/auth/browser-login.js +193 -0
- package/dist/auth/loopback-callback.d.ts +32 -0
- package/dist/auth/loopback-callback.js +175 -0
- package/dist/auth/open-browser.d.ts +15 -0
- package/dist/auth/open-browser.js +54 -0
- package/dist/auth/refresh.d.ts +27 -2
- package/dist/auth/refresh.js +52 -15
- package/dist/catalog/allowlist.d.ts +20 -4
- package/dist/catalog/allowlist.js +126 -25
- package/dist/catalog/command-tree.d.ts +35 -0
- package/dist/catalog/command-tree.js +119 -0
- package/dist/catalog/compatibility.d.ts +23 -0
- package/dist/catalog/compatibility.js +58 -0
- package/dist/catalog/generated/registry.json +6137 -0
- package/dist/catalog/generated/schemas.json +31036 -0
- package/dist/catalog/operations.d.ts +76 -3
- package/dist/catalog/operations.js +87 -213
- package/dist/catalog/validate-body.d.ts +22 -0
- package/dist/catalog/validate-body.js +98 -0
- package/dist/commands/request-body.d.ts +21 -0
- package/dist/commands/request-body.js +92 -0
- package/dist/commands/run.js +283 -162
- package/dist/config/profiles.js +3 -3
- package/dist/engine-backtest/errors.d.ts +18 -0
- package/dist/engine-backtest/errors.js +26 -0
- package/dist/engine-backtest/fetch-runtime.d.ts +38 -0
- package/dist/engine-backtest/fetch-runtime.js +170 -0
- package/dist/engine-backtest/native-import.d.ts +1 -0
- package/dist/engine-backtest/native-import.js +10 -0
- package/dist/engine-backtest/parse-args.d.ts +6 -0
- package/dist/engine-backtest/parse-args.js +281 -0
- package/dist/engine-backtest/persist.d.ts +32 -0
- package/dist/engine-backtest/persist.js +94 -0
- package/dist/engine-backtest/replay-timeframe.d.ts +16 -0
- package/dist/engine-backtest/replay-timeframe.js +48 -0
- package/dist/engine-backtest/resolve-packages.d.ts +27 -0
- package/dist/engine-backtest/resolve-packages.js +288 -0
- package/dist/engine-backtest/run-command.d.ts +38 -0
- package/dist/engine-backtest/run-command.js +540 -0
- package/dist/engine-backtest/types.d.ts +249 -0
- package/dist/engine-backtest/types.js +2 -0
- package/dist/envelope.d.ts +3 -0
- package/dist/envelope.js +43 -3
- package/dist/http/client.js +18 -15
- package/dist/index.d.ts +16 -5
- package/dist/index.js +47 -1
- package/dist/install/exec.d.ts +13 -0
- package/dist/install/exec.js +73 -0
- package/dist/install/package-root.d.ts +4 -0
- package/dist/install/package-root.js +59 -0
- package/dist/install/types.d.ts +69 -0
- package/dist/install/types.js +26 -0
- package/dist/install/wizard.d.ts +21 -0
- package/dist/install/wizard.js +332 -0
- package/dist/keychain/linux-secret-service.d.ts +11 -0
- package/dist/keychain/linux-secret-service.js +93 -0
- package/dist/keychain/store.d.ts +20 -1
- package/dist/keychain/store.js +94 -6
- package/dist/keychain/windows-credential.d.ts +13 -0
- package/dist/keychain/windows-credential.js +176 -0
- package/dist/safety/confirmation.d.ts +10 -3
- package/dist/safety/confirmation.js +27 -4
- package/dist/version.d.ts +2 -2
- package/dist/version.js +3 -2
- package/docs/.nojekyll +0 -0
- package/docs/agents/domain.md +51 -0
- package/docs/agents/issue-tracker.md +156 -0
- package/docs/agents/triage-labels.md +18 -0
- package/docs/alphafox-cli-installation-guide.md +105 -0
- package/docs/e2e-staging.md +76 -6
- package/docs/favicon.svg +4 -0
- package/docs/index.html +748 -0
- package/docs/logo/alphafox-mark.svg +4 -0
- package/docs/logo/alphafox-wordmark-black-en.svg +17 -0
- package/docs/logo/alphafox-wordmark-white-en.svg +16 -0
- package/docs/release-supply-chain.md +107 -26
- package/package.json +10 -3
- package/skills/account/SKILL.md +8 -6
- package/skills/admin/SKILL.md +9 -4
- package/skills/alphafox-shared/SKILL.md +44 -14
- package/skills/auth/SKILL.md +21 -6
- package/skills/engine-backtest/SKILL.md +71 -0
- package/skills/exchange/SKILL.md +10 -3
- package/skills/market/SKILL.md +9 -4
- package/skills/notification/SKILL.md +8 -3
- package/skills/strategy/SKILL.md +23 -9
- package/skills/trading/SKILL.md +18 -5
- package/vendor/backtest-runner/NOTICE.md +1 -0
- package/vendor/backtest-runner/README.md +97 -0
- package/vendor/backtest-runner/index.d.ts +423 -0
- package/vendor/backtest-runner/index.mjs +59 -0
- package/vendor/backtest-runner/lib/abortable.mjs +23 -0
- package/vendor/backtest-runner/lib/cache.mjs +159 -0
- package/vendor/backtest-runner/lib/coverage.mjs +238 -0
- package/vendor/backtest-runner/lib/encode.mjs +28 -0
- package/vendor/backtest-runner/lib/exchanges.mjs +143 -0
- package/vendor/backtest-runner/lib/proxy.mjs +78 -0
- package/vendor/backtest-runner/lib/scenario.mjs +66 -0
- package/vendor/backtest-runner/lib/series.mjs +370 -0
- package/vendor/backtest-runner/lib/tape-loader.mjs +614 -0
- package/vendor/backtest-runner/lib/timeframes.mjs +55 -0
- package/vendor/backtest-runner/package.json +13 -0
package/dist/auth/refresh.js
CHANGED
|
@@ -1,13 +1,17 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Access-token renewal via refresh_token grant.
|
|
4
4
|
* Access tokens are short-lived (~10m); refresh tokens last ~30d (web ADR).
|
|
5
|
+
*
|
|
6
|
+
* Outcomes are explicit: callers must not treat a failed refresh as a healthy session.
|
|
5
7
|
*/
|
|
6
8
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
9
|
exports.ACCESS_TOKEN_REFRESH_SKEW_MS = void 0;
|
|
8
10
|
exports.accessTokenNeedsRefresh = accessTokenNeedsRefresh;
|
|
9
11
|
exports.refreshStoredTokens = refreshStoredTokens;
|
|
12
|
+
exports.refreshStoredTokensOrNull = refreshStoredTokensOrNull;
|
|
10
13
|
exports.clearRefreshInflightForTests = clearRefreshInflightForTests;
|
|
14
|
+
const version_1 = require("../version");
|
|
11
15
|
const store_1 = require("../keychain/store");
|
|
12
16
|
/** Refresh when access token expires within this window. */
|
|
13
17
|
exports.ACCESS_TOKEN_REFRESH_SKEW_MS = 60_000;
|
|
@@ -21,16 +25,20 @@ function accessTokenNeedsRefresh(tokens, now = Date.now()) {
|
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
27
|
* Exchange refresh_token for a new AT/RT pair and persist to keychain.
|
|
24
|
-
* Returns
|
|
28
|
+
* Returns a discriminated outcome — never a bare success token on failure.
|
|
25
29
|
*/
|
|
26
30
|
async function refreshStoredTokens(profile, env = process.env, fetchImpl = fetch, options = {}) {
|
|
27
31
|
const existing = (0, store_1.loadTokens)(profile.name, env);
|
|
28
32
|
if (!existing?.refreshToken?.trim()) {
|
|
29
|
-
return
|
|
33
|
+
return {
|
|
34
|
+
status: "no_session",
|
|
35
|
+
reason: "no_refresh_token",
|
|
36
|
+
tokens: null,
|
|
37
|
+
};
|
|
30
38
|
}
|
|
31
39
|
if (!options.force &&
|
|
32
40
|
!accessTokenNeedsRefresh(existing, options.now ?? Date.now())) {
|
|
33
|
-
return existing;
|
|
41
|
+
return { status: "unchanged", tokens: existing };
|
|
34
42
|
}
|
|
35
43
|
const key = profile.name;
|
|
36
44
|
const pending = inflightByProfile.get(key);
|
|
@@ -43,6 +51,17 @@ async function refreshStoredTokens(profile, env = process.env, fetchImpl = fetch
|
|
|
43
51
|
inflightByProfile.set(key, work);
|
|
44
52
|
return work;
|
|
45
53
|
}
|
|
54
|
+
/**
|
|
55
|
+
* Convenience for callers that only need tokens on successful refresh/unchanged.
|
|
56
|
+
* Returns null for no_session and failed — never pretends failure is success.
|
|
57
|
+
*/
|
|
58
|
+
async function refreshStoredTokensOrNull(profile, env = process.env, fetchImpl = fetch, options = {}) {
|
|
59
|
+
const outcome = await refreshStoredTokens(profile, env, fetchImpl, options);
|
|
60
|
+
if (outcome.status === "refreshed" || outcome.status === "unchanged") {
|
|
61
|
+
return outcome.tokens;
|
|
62
|
+
}
|
|
63
|
+
return null;
|
|
64
|
+
}
|
|
46
65
|
async function performRefresh(profile, existing, env, fetchImpl) {
|
|
47
66
|
const origin = profile.apiBaseUrl.replace(/\/$/, "").replace(/\/api\/v1$/, "");
|
|
48
67
|
const url = `${origin}/api/auth/oauth/token`;
|
|
@@ -59,27 +78,43 @@ async function performRefresh(profile, existing, env, fetchImpl) {
|
|
|
59
78
|
Accept: "application/json",
|
|
60
79
|
"Content-Type": "application/json",
|
|
61
80
|
"X-Alphafox-Client": "alphafox-cli",
|
|
62
|
-
"X-Alphafox-Client-Version": env.ALPHAFOX_CLI_VERSION ??
|
|
81
|
+
"X-Alphafox-Client-Version": env.ALPHAFOX_CLI_VERSION ?? version_1.CLI_VERSION,
|
|
63
82
|
},
|
|
64
83
|
body: JSON.stringify(body),
|
|
65
84
|
redirect: "manual",
|
|
66
85
|
});
|
|
67
86
|
}
|
|
68
|
-
catch {
|
|
69
|
-
return
|
|
87
|
+
catch (err) {
|
|
88
|
+
return {
|
|
89
|
+
status: "failed",
|
|
90
|
+
reason: err instanceof Error ? err.message : "network_error",
|
|
91
|
+
tokens: existing,
|
|
92
|
+
};
|
|
70
93
|
}
|
|
71
94
|
if (response.status >= 400) {
|
|
72
|
-
return
|
|
95
|
+
return {
|
|
96
|
+
status: "failed",
|
|
97
|
+
reason: `http_${response.status}`,
|
|
98
|
+
tokens: existing,
|
|
99
|
+
};
|
|
73
100
|
}
|
|
74
101
|
let json;
|
|
75
102
|
try {
|
|
76
103
|
json = await response.json();
|
|
77
104
|
}
|
|
78
105
|
catch {
|
|
79
|
-
return
|
|
106
|
+
return {
|
|
107
|
+
status: "failed",
|
|
108
|
+
reason: "invalid_json",
|
|
109
|
+
tokens: existing,
|
|
110
|
+
};
|
|
80
111
|
}
|
|
81
112
|
if (!json || typeof json !== "object") {
|
|
82
|
-
return
|
|
113
|
+
return {
|
|
114
|
+
status: "failed",
|
|
115
|
+
reason: "invalid_body",
|
|
116
|
+
tokens: existing,
|
|
117
|
+
};
|
|
83
118
|
}
|
|
84
119
|
const o = json;
|
|
85
120
|
const access = typeof o.access_token === "string"
|
|
@@ -93,16 +128,18 @@ async function performRefresh(profile, existing, env, fetchImpl) {
|
|
|
93
128
|
? o.refreshToken
|
|
94
129
|
: null;
|
|
95
130
|
if (!access || !refresh) {
|
|
96
|
-
return
|
|
131
|
+
return {
|
|
132
|
+
status: "failed",
|
|
133
|
+
reason: "missing_tokens",
|
|
134
|
+
tokens: existing,
|
|
135
|
+
};
|
|
97
136
|
}
|
|
98
137
|
const expiresIn = typeof o.expires_in === "number"
|
|
99
138
|
? o.expires_in
|
|
100
139
|
: typeof o.expiresIn === "number"
|
|
101
140
|
? o.expiresIn
|
|
102
141
|
: 600;
|
|
103
|
-
const scopeRaw = typeof o.scope === "string"
|
|
104
|
-
? o.scope
|
|
105
|
-
: existing.scopes.join(" ");
|
|
142
|
+
const scopeRaw = typeof o.scope === "string" ? o.scope : existing.scopes.join(" ");
|
|
106
143
|
const next = {
|
|
107
144
|
accessToken: access,
|
|
108
145
|
refreshToken: refresh,
|
|
@@ -114,7 +151,7 @@ async function performRefresh(profile, existing, env, fetchImpl) {
|
|
|
114
151
|
scopes: scopeRaw.split(/\s+/).filter(Boolean),
|
|
115
152
|
};
|
|
116
153
|
(0, store_1.saveTokens)(profile.name, next, env);
|
|
117
|
-
return next;
|
|
154
|
+
return { status: "refreshed", tokens: next };
|
|
118
155
|
}
|
|
119
156
|
const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
|
|
120
157
|
async function fetchFollowingSameSiteRedirects(fetchImpl, startUrl, init, maxHops = 5) {
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Raw API allowlist
|
|
3
|
-
*
|
|
2
|
+
* Raw API allowlist from the generated Operation Registry.
|
|
3
|
+
* Finite set: infra catalog paths + CLI-included operation facade paths.
|
|
4
|
+
* Unknown `/api/v1/*` paths are denied — raw API cannot bypass the catalog.
|
|
5
|
+
*/
|
|
6
|
+
/**
|
|
7
|
+
* Infra paths always allowed (exact, except `/api/v1/operations/*`).
|
|
8
|
+
* Product routes must match a registry facadePath — no prefix smuggling.
|
|
9
|
+
*/
|
|
10
|
+
export declare const FACILITY_ALWAYS_ALLOW: readonly ["/api/v1/meta", "/api/v1/openapi.json", "/api/v1/openapi", "/api/v1/operations"];
|
|
11
|
+
/**
|
|
12
|
+
* Normalize API paths for allowlist decisions:
|
|
13
|
+
* strip query/hash, collapse //, decode %2e-style segments, resolve `.`/`..`.
|
|
14
|
+
* Escaping above `/` via `..` is clamped at root (POSIX-style).
|
|
4
15
|
*/
|
|
5
|
-
/** MVP + core facade paths always allowed; full registry embedded for drift tests. */
|
|
6
|
-
export declare const FACILITY_ALWAYS_ALLOW: readonly ["/api/v1/meta", "/api/v1/me", "/api/v1/operations", "/api/v1/openapi.json", "/api/v1/trading/strategy-definitions", "/api/v1/exchange-connectors", "/api/v1/trading/traders", "/api/v1/chats", "/api/v1/backtests"];
|
|
7
16
|
export declare function normalizeApiPath(path: string): string;
|
|
8
17
|
export declare function isInternalDisallowedPath(path: string): boolean;
|
|
18
|
+
/** Match OpenAPI-style templates: /api/v1/backtests/{backtestId}/cancel */
|
|
19
|
+
export declare function pathTemplateMatches(template: string, actual: string, catchAll?: boolean): boolean;
|
|
20
|
+
/**
|
|
21
|
+
* Finite allow set: infra catalog paths + generated operation facade paths.
|
|
22
|
+
* Unknown `/api/v1/*` paths are denied.
|
|
23
|
+
* Matching always uses the segment-resolved path (no `..` prefix smuggling).
|
|
24
|
+
*/
|
|
9
25
|
export declare function isFacadeAllowlistedPath(path: string, extraAllow?: readonly string[]): boolean;
|
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* Raw API allowlist
|
|
4
|
-
*
|
|
3
|
+
* Raw API allowlist from the generated Operation Registry.
|
|
4
|
+
* Finite set: infra catalog paths + CLI-included operation facade paths.
|
|
5
|
+
* Unknown `/api/v1/*` paths are denied — raw API cannot bypass the catalog.
|
|
5
6
|
*/
|
|
6
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
8
|
exports.FACILITY_ALWAYS_ALLOW = void 0;
|
|
8
9
|
exports.normalizeApiPath = normalizeApiPath;
|
|
9
10
|
exports.isInternalDisallowedPath = isInternalDisallowedPath;
|
|
11
|
+
exports.pathTemplateMatches = pathTemplateMatches;
|
|
10
12
|
exports.isFacadeAllowlistedPath = isFacadeAllowlistedPath;
|
|
13
|
+
const operations_1 = require("./operations");
|
|
11
14
|
const INTERNAL_PREFIXES = [
|
|
12
15
|
"/backend",
|
|
13
16
|
"/control-plane",
|
|
@@ -16,53 +19,151 @@ const INTERNAL_PREFIXES = [
|
|
|
16
19
|
"/api/control-plane",
|
|
17
20
|
"/api/signal-center",
|
|
18
21
|
];
|
|
19
|
-
/**
|
|
22
|
+
/**
|
|
23
|
+
* Infra paths always allowed (exact, except `/api/v1/operations/*`).
|
|
24
|
+
* Product routes must match a registry facadePath — no prefix smuggling.
|
|
25
|
+
*/
|
|
20
26
|
exports.FACILITY_ALWAYS_ALLOW = [
|
|
21
27
|
"/api/v1/meta",
|
|
22
|
-
"/api/v1/me",
|
|
23
|
-
"/api/v1/operations",
|
|
24
28
|
"/api/v1/openapi.json",
|
|
25
|
-
"/api/v1/
|
|
26
|
-
"/api/v1/
|
|
27
|
-
"/api/v1/trading/traders",
|
|
28
|
-
"/api/v1/chats",
|
|
29
|
-
"/api/v1/backtests",
|
|
29
|
+
"/api/v1/openapi",
|
|
30
|
+
"/api/v1/operations",
|
|
30
31
|
];
|
|
32
|
+
/**
|
|
33
|
+
* Decode percent-encoded path segments (up to twice for double-encoding)
|
|
34
|
+
* without turning encoded slashes into separators mid-segment incorrectly:
|
|
35
|
+
* each segment is decoded independently so %2F stays inside a segment name.
|
|
36
|
+
*/
|
|
37
|
+
function decodePathSegments(path) {
|
|
38
|
+
let current = path;
|
|
39
|
+
for (let pass = 0; pass < 2; pass += 1) {
|
|
40
|
+
const next = current
|
|
41
|
+
.split("/")
|
|
42
|
+
.map((seg) => {
|
|
43
|
+
if (!seg.includes("%"))
|
|
44
|
+
return seg;
|
|
45
|
+
try {
|
|
46
|
+
return decodeURIComponent(seg);
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return seg;
|
|
50
|
+
}
|
|
51
|
+
})
|
|
52
|
+
.join("/");
|
|
53
|
+
if (next === current)
|
|
54
|
+
break;
|
|
55
|
+
current = next;
|
|
56
|
+
}
|
|
57
|
+
return current;
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Normalize API paths for allowlist decisions:
|
|
61
|
+
* strip query/hash, collapse //, decode %2e-style segments, resolve `.`/`..`.
|
|
62
|
+
* Escaping above `/` via `..` is clamped at root (POSIX-style).
|
|
63
|
+
*/
|
|
31
64
|
function normalizeApiPath(path) {
|
|
32
65
|
const trimmed = path.trim();
|
|
33
66
|
if (!trimmed)
|
|
34
67
|
return "/";
|
|
35
|
-
|
|
36
|
-
|
|
68
|
+
let raw = trimmed.startsWith("/") ? trimmed : `/${trimmed}`;
|
|
69
|
+
raw = (raw.split("?")[0] ?? raw).split("#")[0];
|
|
70
|
+
raw = decodePathSegments(raw);
|
|
71
|
+
raw = raw.replace(/\/{2,}/g, "/");
|
|
72
|
+
const resolved = [];
|
|
73
|
+
for (const seg of raw.split("/")) {
|
|
74
|
+
if (seg === "" || seg === ".") {
|
|
75
|
+
continue;
|
|
76
|
+
}
|
|
77
|
+
if (seg === "..") {
|
|
78
|
+
if (resolved.length > 0) {
|
|
79
|
+
resolved.pop();
|
|
80
|
+
}
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (seg.includes("\0") || seg.includes("\\")) {
|
|
84
|
+
return "/";
|
|
85
|
+
}
|
|
86
|
+
resolved.push(seg);
|
|
87
|
+
}
|
|
88
|
+
return resolved.length === 0 ? "/" : `/${resolved.join("/")}`;
|
|
37
89
|
}
|
|
38
90
|
function isInternalDisallowedPath(path) {
|
|
39
91
|
const n = normalizeApiPath(path);
|
|
40
92
|
return INTERNAL_PREFIXES.some((p) => n === p || n.startsWith(`${p}/`) || n.includes(`${p}/`));
|
|
41
93
|
}
|
|
94
|
+
/** Match OpenAPI-style templates: /api/v1/backtests/{backtestId}/cancel */
|
|
95
|
+
function pathTemplateMatches(template, actual, catchAll = false) {
|
|
96
|
+
const t = normalizeApiPath(template).split("/").filter(Boolean);
|
|
97
|
+
const a = normalizeApiPath(actual).split("/").filter(Boolean);
|
|
98
|
+
if (catchAll) {
|
|
99
|
+
if (a.length < t.length)
|
|
100
|
+
return false;
|
|
101
|
+
for (let i = 0; i < t.length; i += 1) {
|
|
102
|
+
const seg = t[i];
|
|
103
|
+
if (seg.startsWith("{") && seg.endsWith("}"))
|
|
104
|
+
continue;
|
|
105
|
+
if (seg !== a[i])
|
|
106
|
+
return false;
|
|
107
|
+
}
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
if (t.length !== a.length)
|
|
111
|
+
return false;
|
|
112
|
+
for (let i = 0; i < t.length; i += 1) {
|
|
113
|
+
const seg = t[i];
|
|
114
|
+
if (seg.startsWith("{") && seg.endsWith("}"))
|
|
115
|
+
continue;
|
|
116
|
+
if (seg !== a[i])
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
return true;
|
|
120
|
+
}
|
|
121
|
+
function isInfraAllowlisted(n) {
|
|
122
|
+
if (n === "/api/v1/meta" ||
|
|
123
|
+
n === "/api/v1/openapi.json" ||
|
|
124
|
+
n === "/api/v1/openapi" ||
|
|
125
|
+
n === "/api/v1/operations" ||
|
|
126
|
+
n.startsWith("/api/v1/operations/")) {
|
|
127
|
+
return true;
|
|
128
|
+
}
|
|
129
|
+
return false;
|
|
130
|
+
}
|
|
131
|
+
function isCatalogPathMatch(n) {
|
|
132
|
+
for (const op of operations_1.CATALOG_OPERATIONS) {
|
|
133
|
+
if (pathTemplateMatches(op.path, n, Boolean(op.catchAll))) {
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
return false;
|
|
138
|
+
}
|
|
139
|
+
function isExtraAllowMatch(n, extraAllow) {
|
|
140
|
+
for (const a of extraAllow) {
|
|
141
|
+
if (n === a || n.startsWith(`${a}/`) || pathTemplateMatches(a, n)) {
|
|
142
|
+
return true;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return false;
|
|
146
|
+
}
|
|
147
|
+
/**
|
|
148
|
+
* Finite allow set: infra catalog paths + generated operation facade paths.
|
|
149
|
+
* Unknown `/api/v1/*` paths are denied.
|
|
150
|
+
* Matching always uses the segment-resolved path (no `..` prefix smuggling).
|
|
151
|
+
*/
|
|
42
152
|
function isFacadeAllowlistedPath(path, extraAllow = []) {
|
|
43
153
|
const n = normalizeApiPath(path);
|
|
44
154
|
if (isInternalDisallowedPath(n)) {
|
|
45
155
|
return false;
|
|
46
156
|
}
|
|
47
|
-
if (!n.startsWith("/api/v1")) {
|
|
157
|
+
if (n !== "/api/v1" && !n.startsWith("/api/v1/")) {
|
|
48
158
|
return false;
|
|
49
159
|
}
|
|
50
|
-
|
|
51
|
-
for (const a of allow) {
|
|
52
|
-
if (n === a || n.startsWith(`${a}/`)) {
|
|
53
|
-
return true;
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
// templated backtests etc.
|
|
57
|
-
if (/^\/api\/v1\/backtests\/[^/]+(\/.*)?$/.test(n)) {
|
|
160
|
+
if (isInfraAllowlisted(n)) {
|
|
58
161
|
return true;
|
|
59
162
|
}
|
|
60
|
-
if (
|
|
163
|
+
if (isExtraAllowMatch(n, extraAllow)) {
|
|
61
164
|
return true;
|
|
62
165
|
}
|
|
63
|
-
if (
|
|
64
|
-
// Allow any /api/v1/* that is not internal — facade is the allowlist boundary.
|
|
65
|
-
// Internal services are never mounted under /api/v1.
|
|
166
|
+
if (isCatalogPathMatch(n)) {
|
|
66
167
|
return true;
|
|
67
168
|
}
|
|
68
169
|
return false;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Typed command tree derived from registry operationIds.
|
|
3
|
+
* Shortcuts may only compose operationIds — never a second schema catalog.
|
|
4
|
+
*/
|
|
5
|
+
import { type CatalogOperation } from "./operations";
|
|
6
|
+
export declare function splitCommandTokens(tokens: readonly string[]): {
|
|
7
|
+
readonly commandTokens: string[];
|
|
8
|
+
readonly flagArgs: string[];
|
|
9
|
+
readonly help: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function operationsWithPrefix(prefix: string): CatalogOperation[];
|
|
12
|
+
export type TypedResolution = {
|
|
13
|
+
readonly kind: "operation";
|
|
14
|
+
readonly operation: CatalogOperation;
|
|
15
|
+
readonly flagArgs: string[];
|
|
16
|
+
readonly help: boolean;
|
|
17
|
+
} | {
|
|
18
|
+
readonly kind: "help";
|
|
19
|
+
readonly prefix: string;
|
|
20
|
+
readonly operations: readonly CatalogOperation[];
|
|
21
|
+
readonly flagArgs: string[];
|
|
22
|
+
} | {
|
|
23
|
+
readonly kind: "ambiguous";
|
|
24
|
+
readonly prefix: string;
|
|
25
|
+
readonly candidates: readonly string[];
|
|
26
|
+
} | {
|
|
27
|
+
readonly kind: "missing";
|
|
28
|
+
readonly prefix: string;
|
|
29
|
+
};
|
|
30
|
+
export declare function resolveTypedCommand(tokens: readonly string[]): TypedResolution;
|
|
31
|
+
export declare function typedCommandExample(op: CatalogOperation): {
|
|
32
|
+
readonly typed: string;
|
|
33
|
+
readonly api: string;
|
|
34
|
+
readonly config?: string;
|
|
35
|
+
};
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Typed command tree derived from registry operationIds.
|
|
4
|
+
* Shortcuts may only compose operationIds — never a second schema catalog.
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.splitCommandTokens = splitCommandTokens;
|
|
8
|
+
exports.operationsWithPrefix = operationsWithPrefix;
|
|
9
|
+
exports.resolveTypedCommand = resolveTypedCommand;
|
|
10
|
+
exports.typedCommandExample = typedCommandExample;
|
|
11
|
+
const operations_1 = require("./operations");
|
|
12
|
+
function hyphenToUnderscore(token) {
|
|
13
|
+
return token.replace(/-/g, "_");
|
|
14
|
+
}
|
|
15
|
+
function splitCommandTokens(tokens) {
|
|
16
|
+
const commandTokens = [];
|
|
17
|
+
const flagArgs = [];
|
|
18
|
+
let help = false;
|
|
19
|
+
let inFlags = false;
|
|
20
|
+
for (const token of tokens) {
|
|
21
|
+
if (!inFlags && (token === "--help" || token === "-h")) {
|
|
22
|
+
help = true;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
if (!inFlags && token.startsWith("--")) {
|
|
26
|
+
inFlags = true;
|
|
27
|
+
}
|
|
28
|
+
if (inFlags) {
|
|
29
|
+
flagArgs.push(token);
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
commandTokens.push(token);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return { commandTokens, flagArgs, help };
|
|
36
|
+
}
|
|
37
|
+
function lookupId(id) {
|
|
38
|
+
return ((0, operations_1.findCatalogOperation)(id) ??
|
|
39
|
+
(0, operations_1.findCatalogOperation)(hyphenToUnderscore(id)));
|
|
40
|
+
}
|
|
41
|
+
function operationsWithPrefix(prefix) {
|
|
42
|
+
const norm = hyphenToUnderscore(prefix);
|
|
43
|
+
return operations_1.CATALOG_OPERATIONS.filter((op) => op.operationId === norm || op.operationId.startsWith(`${norm}.`));
|
|
44
|
+
}
|
|
45
|
+
function uniqueSuffixMatch(tokens) {
|
|
46
|
+
if (tokens.length < 2)
|
|
47
|
+
return [];
|
|
48
|
+
const domain = hyphenToUnderscore(tokens[0]);
|
|
49
|
+
const action = hyphenToUnderscore(tokens[tokens.length - 1]);
|
|
50
|
+
const middle = tokens.slice(1, -1).map(hyphenToUnderscore);
|
|
51
|
+
return operations_1.CATALOG_OPERATIONS.filter((op) => {
|
|
52
|
+
if (!op.operationId.startsWith(`${domain}.`))
|
|
53
|
+
return false;
|
|
54
|
+
if (!op.operationId.endsWith(`.${action}`) && op.operationId !== action) {
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
for (const part of middle) {
|
|
58
|
+
if (!op.operationId.includes(`.${part}.`) && !op.operationId.includes(`${part}.`)) {
|
|
59
|
+
return false;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
return true;
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
function resolveTypedCommand(tokens) {
|
|
66
|
+
const { commandTokens, flagArgs, help } = splitCommandTokens(tokens);
|
|
67
|
+
if (commandTokens.length === 0) {
|
|
68
|
+
return { kind: "missing", prefix: "" };
|
|
69
|
+
}
|
|
70
|
+
const joined = commandTokens.join(".");
|
|
71
|
+
const exact = lookupId(joined);
|
|
72
|
+
if (exact) {
|
|
73
|
+
return { kind: "operation", operation: exact, flagArgs, help };
|
|
74
|
+
}
|
|
75
|
+
const suffixHits = uniqueSuffixMatch(commandTokens);
|
|
76
|
+
if (suffixHits.length === 1) {
|
|
77
|
+
return {
|
|
78
|
+
kind: "operation",
|
|
79
|
+
operation: suffixHits[0],
|
|
80
|
+
flagArgs,
|
|
81
|
+
help,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
if (suffixHits.length > 1) {
|
|
85
|
+
return {
|
|
86
|
+
kind: "ambiguous",
|
|
87
|
+
prefix: joined,
|
|
88
|
+
candidates: suffixHits.map((op) => op.operationId),
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
const prefixOps = operationsWithPrefix(joined);
|
|
92
|
+
if (prefixOps.length > 0) {
|
|
93
|
+
return {
|
|
94
|
+
kind: "help",
|
|
95
|
+
prefix: hyphenToUnderscore(joined),
|
|
96
|
+
operations: prefixOps,
|
|
97
|
+
flagArgs,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
return { kind: "missing", prefix: joined };
|
|
101
|
+
}
|
|
102
|
+
function typedCommandExample(op) {
|
|
103
|
+
const segments = op.operationId.split(".");
|
|
104
|
+
const schemaNames = op.path.match(/\{([a-zA-Z0-9_]+)\}/g)?.map((s) => s.slice(1, -1)) ?? [];
|
|
105
|
+
const pathFlags = schemaNames.map((name) => `--${name} <${name}>`).join(" ");
|
|
106
|
+
const riskFlag = op.risk === "high-risk-write" || op.risk === "unknown" ? " --yes" : "";
|
|
107
|
+
const writesBody = op.method !== "GET" && op.method !== "HEAD" && op.method !== "DELETE";
|
|
108
|
+
const bodyFlag = writesBody ? " --body '{…}'" : "";
|
|
109
|
+
const typedFlags = [pathFlags, bodyFlag.trim(), riskFlag.trim()]
|
|
110
|
+
.filter(Boolean)
|
|
111
|
+
.join(" ");
|
|
112
|
+
return {
|
|
113
|
+
typed: `alphafox ${segments.join(" ")}${typedFlags ? ` ${typedFlags}` : ""}`,
|
|
114
|
+
api: `alphafox api ${op.method} ${op.path}${bodyFlag}${riskFlag}`,
|
|
115
|
+
config: writesBody
|
|
116
|
+
? `alphafox ${segments.join(" ")} --config @./${op.operationId}.json${riskFlag}`
|
|
117
|
+
: undefined,
|
|
118
|
+
};
|
|
119
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Equivalent of `@alphafoxai/contracts/public-api` `checkCliCompatibility`.
|
|
3
|
+
* Fail closed on CLI/contract version skew. Do not invent a second range:
|
|
4
|
+
* callers must pass the generated registry compatibility block.
|
|
5
|
+
*/
|
|
6
|
+
export interface CompatibilityRange {
|
|
7
|
+
readonly contractVersion: string;
|
|
8
|
+
readonly registryVersion: string;
|
|
9
|
+
readonly minCliVersion: string;
|
|
10
|
+
readonly maxCliVersion: string;
|
|
11
|
+
readonly openapi: string;
|
|
12
|
+
}
|
|
13
|
+
export type CompatibilityResult = {
|
|
14
|
+
readonly ok: true;
|
|
15
|
+
} | {
|
|
16
|
+
readonly ok: false;
|
|
17
|
+
readonly code: "cli_too_old" | "cli_too_new" | "contract_mismatch" | "invalid_version";
|
|
18
|
+
readonly message: string;
|
|
19
|
+
};
|
|
20
|
+
export declare function checkCliCompatibility(input: {
|
|
21
|
+
readonly cliVersion: string;
|
|
22
|
+
readonly contractVersion: string;
|
|
23
|
+
}, range: CompatibilityRange): CompatibilityResult;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Equivalent of `@alphafoxai/contracts/public-api` `checkCliCompatibility`.
|
|
4
|
+
* Fail closed on CLI/contract version skew. Do not invent a second range:
|
|
5
|
+
* callers must pass the generated registry compatibility block.
|
|
6
|
+
*/
|
|
7
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
|
+
exports.checkCliCompatibility = checkCliCompatibility;
|
|
9
|
+
function parseSemver(raw) {
|
|
10
|
+
const m = /^(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/.exec(raw.trim());
|
|
11
|
+
if (!m)
|
|
12
|
+
return null;
|
|
13
|
+
return [Number(m[1]), Number(m[2]), Number(m[3])];
|
|
14
|
+
}
|
|
15
|
+
function cmpSemver(a, b) {
|
|
16
|
+
const pa = parseSemver(a);
|
|
17
|
+
const pb = parseSemver(b);
|
|
18
|
+
if (!pa || !pb)
|
|
19
|
+
return null;
|
|
20
|
+
for (let i = 0; i < 3; i += 1) {
|
|
21
|
+
if (pa[i] !== pb[i])
|
|
22
|
+
return pa[i] - pb[i];
|
|
23
|
+
}
|
|
24
|
+
return 0;
|
|
25
|
+
}
|
|
26
|
+
function checkCliCompatibility(input, range) {
|
|
27
|
+
const cliVsMin = cmpSemver(input.cliVersion, range.minCliVersion);
|
|
28
|
+
const cliVsMax = cmpSemver(input.cliVersion, range.maxCliVersion);
|
|
29
|
+
if (cliVsMin === null || cliVsMax === null) {
|
|
30
|
+
return {
|
|
31
|
+
ok: false,
|
|
32
|
+
code: "invalid_version",
|
|
33
|
+
message: `CLI version ${input.cliVersion} is not valid semver (expected MAJOR.MINOR.PATCH).`,
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
if (cliVsMin < 0) {
|
|
37
|
+
return {
|
|
38
|
+
ok: false,
|
|
39
|
+
code: "cli_too_old",
|
|
40
|
+
message: `CLI ${input.cliVersion} is older than minCliVersion ${range.minCliVersion} for contract ${range.contractVersion}.`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (cliVsMax > 0) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
code: "cli_too_new",
|
|
47
|
+
message: `CLI ${input.cliVersion} is newer than maxCliVersion ${range.maxCliVersion} for contract ${range.contractVersion}.`,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (input.contractVersion !== range.contractVersion) {
|
|
51
|
+
return {
|
|
52
|
+
ok: false,
|
|
53
|
+
code: "contract_mismatch",
|
|
54
|
+
message: `Server contractVersion ${input.contractVersion} does not match CLI catalog ${range.contractVersion}.`,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { ok: true };
|
|
58
|
+
}
|