@apifuse/provider-sdk 2.2.0-beta.5 → 2.2.0-beta.8
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/AUTHORING.md +53 -0
- package/CHANGELOG.md +12 -0
- package/README.md +5 -1
- package/SUBMISSION.md +1 -1
- package/bin/apifuse-check.ts +26 -1
- package/bin/apifuse-pack-check.ts +14 -0
- package/bin/apifuse-submit-check.ts +193 -2
- package/bin/apifuse-sync-assets.ts +117 -0
- package/dist/auth-turn/index.d.ts +2 -2
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.d.ts +3 -0
- package/dist/cli/create.js +34 -35
- package/dist/cli/prompt-assets.d.ts +80 -0
- package/dist/cli/prompt-assets.js +743 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/dist/config/loader.d.ts +79 -6
- package/dist/config/loader.js +272 -48
- package/dist/define.js +27 -3
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/runtime/executor.js +7 -0
- package/dist/runtime/http.js +3 -0
- package/dist/runtime/proxy-errors.js +6 -2
- package/dist/runtime/proxy-nodemaven.d.ts +34 -0
- package/dist/runtime/proxy-nodemaven.js +128 -0
- package/dist/runtime/proxy-telemetry.d.ts +2 -1
- package/dist/runtime/proxy-telemetry.js +39 -4
- package/dist/runtime/secrets.d.ts +27 -0
- package/dist/runtime/secrets.js +51 -0
- package/dist/runtime/stealth.js +20 -9
- package/dist/server/serve.d.ts +5 -0
- package/dist/server/serve.js +39 -0
- package/dist/server/types.d.ts +9 -9
- package/dist/types.d.ts +30 -1
- package/package.json +4 -3
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +42 -35
- package/src/cli/prompt-assets.ts +865 -0
- package/src/cli/templates/provider/AGENTS.md.tpl +17 -8
- package/src/config/loader.ts +405 -61
- package/src/define.ts +35 -3
- package/src/index.ts +5 -0
- package/src/runtime/executor.ts +8 -0
- package/src/runtime/http.ts +3 -0
- package/src/runtime/proxy-errors.ts +12 -4
- package/src/runtime/proxy-nodemaven.ts +178 -0
- package/src/runtime/proxy-telemetry.ts +56 -5
- package/src/runtime/secrets.ts +64 -0
- package/src/runtime/stealth.ts +26 -10
- package/src/server/serve.ts +53 -0
- package/src/types.ts +30 -1
- package/dist/cli/templates/provider/CLAUDE.md.tpl +0 -1
- package/src/cli/templates/provider/CLAUDE.md.tpl +0 -1
- /package/dist/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/dist/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/fixtures-and-recording/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/health-checks-and-fail-closed/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/normalization-standards/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/pagination-and-counts/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-contract-verification/SKILL.md.tpl +0 -0
- /package/src/cli/templates/provider/{skills → .agents/skills}/upstream-notes/README.md.tpl +0 -0
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
2
|
+
export const NODEMAVEN_USERNAME_ENV = "APIFUSE__PROXY__NODEMAVEN_USERNAME";
|
|
3
|
+
export const NODEMAVEN_PASSWORD_ENV = "APIFUSE__PROXY__NODEMAVEN_PASSWORD";
|
|
4
|
+
export const NODEMAVEN_FILTER_ENV = "APIFUSE__PROXY__NODEMAVEN_FILTER";
|
|
5
|
+
export const NODEMAVEN_GATEWAY_HOST = "gate.nodemaven.com";
|
|
6
|
+
/**
|
|
7
|
+
* NodeMaven's fastest protocol: HTTP CONNECT. Benchmarks (KR, cold + warm)
|
|
8
|
+
* showed socks5 through the gateway adds ~500ms per request over http, so
|
|
9
|
+
* NodeMaven never defaults to socks5.
|
|
10
|
+
*/
|
|
11
|
+
export const NODEMAVEN_DEFAULT_PROTOCOL = "http";
|
|
12
|
+
/** NodeMaven gateway port ranges per protocol (docs: HTTP 8080-9080, SOCKS5 1080-2080). */
|
|
13
|
+
const NODEMAVEN_PORTS = {
|
|
14
|
+
http: { min: 8080, max: 9080 },
|
|
15
|
+
socks5: { min: 1080, max: 2080 },
|
|
16
|
+
};
|
|
17
|
+
const NODEMAVEN_FILTERS = new Set(["medium", "high"]);
|
|
18
|
+
const DEFAULT_NODEMAVEN_FILTER = "medium";
|
|
19
|
+
const DEFAULT_NODEMAVEN_POOL_SIZE = 20;
|
|
20
|
+
const NODEMAVEN_MAX_POOL_SIZE = 50;
|
|
21
|
+
/** NodeMaven sticky sessions persist up to 24h server-side, keyed by the sid. */
|
|
22
|
+
const NODEMAVEN_MAX_LIFETIME_MINUTES = 1440;
|
|
23
|
+
const SID_LENGTH = 10;
|
|
24
|
+
export function hasNodemavenCredentials() {
|
|
25
|
+
return Boolean(readNodemavenUsername() && readNodemavenPassword());
|
|
26
|
+
}
|
|
27
|
+
function readNodemavenUsername() {
|
|
28
|
+
return process.env[NODEMAVEN_USERNAME_ENV]?.trim() || undefined;
|
|
29
|
+
}
|
|
30
|
+
function readNodemavenPassword() {
|
|
31
|
+
return process.env[NODEMAVEN_PASSWORD_ENV]?.trim() || undefined;
|
|
32
|
+
}
|
|
33
|
+
function resolveNodemavenFilter() {
|
|
34
|
+
const raw = process.env[NODEMAVEN_FILTER_ENV]?.trim().toLowerCase();
|
|
35
|
+
if (!raw)
|
|
36
|
+
return DEFAULT_NODEMAVEN_FILTER;
|
|
37
|
+
if (!NODEMAVEN_FILTERS.has(raw)) {
|
|
38
|
+
throw new Error(`${NODEMAVEN_FILTER_ENV} must be "medium" or "high"`);
|
|
39
|
+
}
|
|
40
|
+
return raw;
|
|
41
|
+
}
|
|
42
|
+
export function nodemavenPoolSize(policy) {
|
|
43
|
+
return Math.min(NODEMAVEN_MAX_POOL_SIZE, Math.max(1, Math.floor(policy.session?.poolSize ?? DEFAULT_NODEMAVEN_POOL_SIZE)));
|
|
44
|
+
}
|
|
45
|
+
function nodemavenLifetimeMinutes(policy) {
|
|
46
|
+
const configured = policy.session?.lifetimeMinutes;
|
|
47
|
+
if (typeof configured !== "number" || !Number.isFinite(configured) || configured <= 0) {
|
|
48
|
+
return NODEMAVEN_MAX_LIFETIME_MINUTES;
|
|
49
|
+
}
|
|
50
|
+
return Math.min(NODEMAVEN_MAX_LIFETIME_MINUTES, Math.max(1, Math.floor(configured)));
|
|
51
|
+
}
|
|
52
|
+
/** NodeMaven username tokens accept `[a-z0-9]`; slugify geo values to that set. */
|
|
53
|
+
function slugifyGeo(value) {
|
|
54
|
+
if (!value)
|
|
55
|
+
return undefined;
|
|
56
|
+
const slug = value
|
|
57
|
+
.trim()
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
.replace(/[^a-z0-9]+/g, "");
|
|
60
|
+
return slug || undefined;
|
|
61
|
+
}
|
|
62
|
+
function isStickyAffinity(policy) {
|
|
63
|
+
return (policy.session?.affinity ?? "request") !== "request";
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A sticky sid is deterministic from the affinity key so every process serving
|
|
67
|
+
* the same connection derives the same egress IP without shared storage. A
|
|
68
|
+
* rotating sid is random per call (a fresh egress IP per request).
|
|
69
|
+
*/
|
|
70
|
+
function deriveSid(policy, affinityKey, poolIndex, refreshEpoch) {
|
|
71
|
+
if (!isStickyAffinity(policy) || !affinityKey) {
|
|
72
|
+
return randomBytes(SID_LENGTH).toString("hex").slice(0, SID_LENGTH);
|
|
73
|
+
}
|
|
74
|
+
const digest = createHash("sha256")
|
|
75
|
+
.update(`${affinityKey}:${refreshEpoch}:${poolIndex}`)
|
|
76
|
+
.digest("hex");
|
|
77
|
+
// hex digits are a subset of the allowed [a-z0-9] sid charset.
|
|
78
|
+
return digest.slice(0, SID_LENGTH);
|
|
79
|
+
}
|
|
80
|
+
function selectPort(protocol, sid, poolIndex) {
|
|
81
|
+
const { min, max } = NODEMAVEN_PORTS[protocol];
|
|
82
|
+
const span = max - min + 1;
|
|
83
|
+
const hashInt = Number.parseInt(createHash("sha256").update(`${sid}:${poolIndex}`).digest("hex").slice(0, 8), 16);
|
|
84
|
+
return min + (hashInt % span);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Synthesize a NodeMaven gateway proxy URL locally from static credentials.
|
|
88
|
+
* There is no allocation API — geo/session are encoded in the username.
|
|
89
|
+
*/
|
|
90
|
+
export function synthesizeNodemavenProxy(input) {
|
|
91
|
+
const username = readNodemavenUsername();
|
|
92
|
+
const password = readNodemavenPassword();
|
|
93
|
+
if (!username || !password) {
|
|
94
|
+
throw new Error(`NodeMaven credentials missing: set ${NODEMAVEN_USERNAME_ENV} and ${NODEMAVEN_PASSWORD_ENV}.`);
|
|
95
|
+
}
|
|
96
|
+
const filter = resolveNodemavenFilter();
|
|
97
|
+
const sid = deriveSid(input.policy, input.affinityKey, input.poolIndex, input.refreshEpoch);
|
|
98
|
+
const port = selectPort(input.protocol, sid, input.poolIndex);
|
|
99
|
+
const lifetimeMinutes = nodemavenLifetimeMinutes(input.policy);
|
|
100
|
+
const country = slugifyGeo(input.country ?? input.policy.geo?.country);
|
|
101
|
+
const region = slugifyGeo(input.policy.geo?.subdivision);
|
|
102
|
+
const city = slugifyGeo(input.policy.geo?.city);
|
|
103
|
+
const tokens = [username];
|
|
104
|
+
if (country)
|
|
105
|
+
tokens.push("country", country);
|
|
106
|
+
if (region)
|
|
107
|
+
tokens.push("region", region);
|
|
108
|
+
if (city)
|
|
109
|
+
tokens.push("city", city);
|
|
110
|
+
tokens.push("sid", sid);
|
|
111
|
+
tokens.push("filter", filter);
|
|
112
|
+
tokens.push("ipv4", "true");
|
|
113
|
+
const proxyUsername = tokens.join("-");
|
|
114
|
+
// Username tokens are [a-z0-9-] only, which survive URL encoding unchanged.
|
|
115
|
+
const url = `${input.protocol}://${proxyUsername}:${encodeURIComponent(password)}@${NODEMAVEN_GATEWAY_HOST}:${port}`;
|
|
116
|
+
return {
|
|
117
|
+
url,
|
|
118
|
+
protocol: input.protocol,
|
|
119
|
+
diagnostics: {
|
|
120
|
+
vendor: "nodemaven",
|
|
121
|
+
protocol: input.protocol,
|
|
122
|
+
sticky: isStickyAffinity(input.policy),
|
|
123
|
+
filter,
|
|
124
|
+
lifetimeMinutes,
|
|
125
|
+
...(country ? { country } : {}),
|
|
126
|
+
},
|
|
127
|
+
};
|
|
128
|
+
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ProxyAttemptTelemetryEvent, ProxyResolutionTelemetryEvent, ProxyTelemetrySink } from "../config/loader.js";
|
|
1
|
+
import type { ProxyAttemptTelemetryEvent, ProxyResolutionTelemetryEvent, ProxyTelemetrySink, ProxyVendorFailoverTelemetryEvent } from "../config/loader.js";
|
|
2
2
|
export declare const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
|
|
3
3
|
export declare class ProxyTelemetryCollector implements ProxyTelemetrySink {
|
|
4
4
|
#private;
|
|
5
5
|
recordProxyResolution(event: ProxyResolutionTelemetryEvent): void;
|
|
6
|
+
recordProxyVendorFailover(event: ProxyVendorFailoverTelemetryEvent): void;
|
|
6
7
|
recordProxyAttempt(event: ProxyAttemptTelemetryEvent): void;
|
|
7
8
|
toHeaderValue(): string | undefined;
|
|
8
9
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
export const PROVIDER_TELEMETRY_HEADER = "X-ApiFuse-Provider-Telemetry";
|
|
2
2
|
const MAX_HEADER_BYTES = 4_096;
|
|
3
3
|
const MAX_PROXY_ATTEMPT_SAMPLES = 24;
|
|
4
|
+
const MAX_PROXY_FAILOVER_SAMPLES = 12;
|
|
4
5
|
const CACHE_STATUS_SEVERITY = {
|
|
5
6
|
disabled: 0,
|
|
6
7
|
memory_hit: 1,
|
|
@@ -28,9 +29,11 @@ function encodeBase64Url(value) {
|
|
|
28
29
|
export class ProxyTelemetryCollector {
|
|
29
30
|
#events = [];
|
|
30
31
|
#attempts = [];
|
|
32
|
+
#failovers = [];
|
|
31
33
|
recordProxyResolution(event) {
|
|
32
34
|
this.#events.push({
|
|
33
|
-
provider:
|
|
35
|
+
provider: event.provider,
|
|
36
|
+
...(event.protocol ? { protocol: event.protocol } : {}),
|
|
34
37
|
cacheStatus: event.cacheStatus,
|
|
35
38
|
cacheHit: event.cacheHit,
|
|
36
39
|
resolutionMs: Math.max(0, Math.floor(event.resolutionMs)),
|
|
@@ -53,11 +56,22 @@ export class ProxyTelemetryCollector {
|
|
|
53
56
|
refreshes: event.refreshes === undefined ? undefined : Math.max(0, Math.floor(event.refreshes)),
|
|
54
57
|
});
|
|
55
58
|
}
|
|
59
|
+
recordProxyVendorFailover(event) {
|
|
60
|
+
if (this.#failovers.length >= MAX_PROXY_FAILOVER_SAMPLES)
|
|
61
|
+
return;
|
|
62
|
+
this.#failovers.push({
|
|
63
|
+
vendor: event.vendor,
|
|
64
|
+
...(event.nextVendor ? { nextVendor: event.nextVendor } : {}),
|
|
65
|
+
phase: event.phase,
|
|
66
|
+
reason: event.reason,
|
|
67
|
+
...(event.attempt === undefined ? {} : { attempt: Math.max(0, Math.floor(event.attempt)) }),
|
|
68
|
+
});
|
|
69
|
+
}
|
|
56
70
|
recordProxyAttempt(event) {
|
|
57
71
|
if (this.#attempts.length >= MAX_PROXY_ATTEMPT_SAMPLES)
|
|
58
72
|
return;
|
|
59
73
|
this.#attempts.push({
|
|
60
|
-
provider:
|
|
74
|
+
provider: event.provider,
|
|
61
75
|
attempt: Math.max(1, Math.floor(event.attempt || 1)),
|
|
62
76
|
...(event.poolIndex === undefined
|
|
63
77
|
? {}
|
|
@@ -75,8 +89,16 @@ export class ProxyTelemetryCollector {
|
|
|
75
89
|
const [first, ...rest] = this.#events;
|
|
76
90
|
if (!first)
|
|
77
91
|
return undefined;
|
|
92
|
+
// The serving vendor/protocol is the last recorded resolution (a failed
|
|
93
|
+
// vendor records first, the vendor that served records last).
|
|
94
|
+
const serving = this.#events[this.#events.length - 1] ?? first;
|
|
95
|
+
const vendors = [];
|
|
96
|
+
for (const event of this.#events) {
|
|
97
|
+
if (!vendors.includes(event.provider))
|
|
98
|
+
vendors.push(event.provider);
|
|
99
|
+
}
|
|
78
100
|
const aggregate = rest.reduce((acc, event) => ({
|
|
79
|
-
provider:
|
|
101
|
+
provider: event.provider,
|
|
80
102
|
cacheStatus: worseStatus(acc.cacheStatus, event.cacheStatus),
|
|
81
103
|
cacheHit: acc.cacheHit && event.cacheHit,
|
|
82
104
|
resolutionMs: acc.resolutionMs + event.resolutionMs,
|
|
@@ -95,7 +117,8 @@ export class ProxyTelemetryCollector {
|
|
|
95
117
|
const payload = {
|
|
96
118
|
v: 1,
|
|
97
119
|
proxy: {
|
|
98
|
-
provider:
|
|
120
|
+
provider: serving.provider,
|
|
121
|
+
...(serving.protocol ? { protocol: serving.protocol } : {}),
|
|
99
122
|
cacheStatus: aggregate.cacheStatus,
|
|
100
123
|
cacheHit: aggregate.cacheHit,
|
|
101
124
|
resolutionMs: aggregate.resolutionMs,
|
|
@@ -132,6 +155,18 @@ export class ProxyTelemetryCollector {
|
|
|
132
155
|
})),
|
|
133
156
|
}
|
|
134
157
|
: {}),
|
|
158
|
+
...(vendors.length > 1 ? { vendors } : {}),
|
|
159
|
+
...(this.#failovers.length > 0
|
|
160
|
+
? {
|
|
161
|
+
failovers: this.#failovers.map((failover) => ({
|
|
162
|
+
v: failover.vendor,
|
|
163
|
+
...(failover.nextVendor ? { nx: failover.nextVendor } : {}),
|
|
164
|
+
p: failover.phase,
|
|
165
|
+
r: failover.reason,
|
|
166
|
+
...(failover.attempt === undefined ? {} : { a: failover.attempt }),
|
|
167
|
+
})),
|
|
168
|
+
}
|
|
169
|
+
: {}),
|
|
135
170
|
},
|
|
136
171
|
};
|
|
137
172
|
const encoded = encodeBase64Url(JSON.stringify(payload));
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { EnvContext, ProviderDefinition } from "../types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical error code for a declared-but-unprovisioned provider secret.
|
|
4
|
+
*
|
|
5
|
+
* The SDK is the single source of truth for env/secret presence validation:
|
|
6
|
+
* providers declare secrets in `defineProvider({ secrets: [...] })` and the
|
|
7
|
+
* runtime enforces presence before any handler or auth-flow code runs.
|
|
8
|
+
* Provider-local presence guards (requireServiceKey/requireApiKey style) are a
|
|
9
|
+
* deprecated antipattern — see the `sdk-owned-secret-presence` submit-check
|
|
10
|
+
* rule.
|
|
11
|
+
*/
|
|
12
|
+
export declare const MISSING_SECRET_CODE = "MISSING_SECRET";
|
|
13
|
+
/**
|
|
14
|
+
* Names of declared `required: true` secrets whose env values are unset or
|
|
15
|
+
* whitespace-only. Whitespace-only values count as missing for parity with the
|
|
16
|
+
* `.trim()` guards well-built providers used before the SDK owned this check —
|
|
17
|
+
* a blank value provisioned by a broken secret pipeline must not pass the gate.
|
|
18
|
+
*/
|
|
19
|
+
export declare function listMissingRequiredSecrets(provider: ProviderDefinition, env: EnvContext): string[];
|
|
20
|
+
/**
|
|
21
|
+
* Throws the canonical structured missing-secret error when any declared
|
|
22
|
+
* `required: true` secret is absent. All missing names are reported in a
|
|
23
|
+
* single error so operators can provision the full set in one pass instead of
|
|
24
|
+
* discovering them one deploy at a time (the 2026-07-22 unprovisioned-secret
|
|
25
|
+
* incident failure mode).
|
|
26
|
+
*/
|
|
27
|
+
export declare function assertRequiredSecretsPresent(provider: ProviderDefinition, env: EnvContext): void;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { ProviderSecretError } from "../errors.js";
|
|
2
|
+
/**
|
|
3
|
+
* Canonical error code for a declared-but-unprovisioned provider secret.
|
|
4
|
+
*
|
|
5
|
+
* The SDK is the single source of truth for env/secret presence validation:
|
|
6
|
+
* providers declare secrets in `defineProvider({ secrets: [...] })` and the
|
|
7
|
+
* runtime enforces presence before any handler or auth-flow code runs.
|
|
8
|
+
* Provider-local presence guards (requireServiceKey/requireApiKey style) are a
|
|
9
|
+
* deprecated antipattern — see the `sdk-owned-secret-presence` submit-check
|
|
10
|
+
* rule.
|
|
11
|
+
*/
|
|
12
|
+
export const MISSING_SECRET_CODE = "MISSING_SECRET";
|
|
13
|
+
/**
|
|
14
|
+
* Names of declared `required: true` secrets whose env values are unset or
|
|
15
|
+
* whitespace-only. Whitespace-only values count as missing for parity with the
|
|
16
|
+
* `.trim()` guards well-built providers used before the SDK owned this check —
|
|
17
|
+
* a blank value provisioned by a broken secret pipeline must not pass the gate.
|
|
18
|
+
*/
|
|
19
|
+
export function listMissingRequiredSecrets(provider, env) {
|
|
20
|
+
const missing = [];
|
|
21
|
+
for (const secret of provider.secrets ?? []) {
|
|
22
|
+
if (secret.required !== true) {
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
const value = env.get(secret.name);
|
|
26
|
+
if (value === undefined || value.trim() === "") {
|
|
27
|
+
missing.push(secret.name);
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return missing;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Throws the canonical structured missing-secret error when any declared
|
|
34
|
+
* `required: true` secret is absent. All missing names are reported in a
|
|
35
|
+
* single error so operators can provision the full set in one pass instead of
|
|
36
|
+
* discovering them one deploy at a time (the 2026-07-22 unprovisioned-secret
|
|
37
|
+
* incident failure mode).
|
|
38
|
+
*/
|
|
39
|
+
export function assertRequiredSecretsPresent(provider, env) {
|
|
40
|
+
const missing = listMissingRequiredSecrets(provider, env);
|
|
41
|
+
if (missing.length === 0) {
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const names = missing.join(", ");
|
|
45
|
+
throw new ProviderSecretError(`Missing required provider secret${missing.length > 1 ? "s" : ""}: ${names}`, {
|
|
46
|
+
code: MISSING_SECRET_CODE,
|
|
47
|
+
category: "credential_unavailable",
|
|
48
|
+
retryable: false,
|
|
49
|
+
fix: `Provision ${names} in the provider environment (e.g. Doppler). Declared in defineProvider({ secrets: [...] }).`,
|
|
50
|
+
});
|
|
51
|
+
}
|
package/dist/runtime/stealth.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { Impit } from "impit";
|
|
3
|
-
import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, resolveProxyConfigAsync, SMARTPROXY_MAX_POOL_SIZE, } from "../config/loader.js";
|
|
3
|
+
import { DEFAULT_SMARTPROXY_POOL_SIZE, invalidateProxyResolutionCacheAsync, ProxyResolutionError, resolvePolicyProxyPoolSpan, resolveProxyConfigAsync, SMARTPROXY_MAX_POOL_SIZE, vendorFromResolvedSource, } from "../config/loader.js";
|
|
4
4
|
import { SDKError, TransportError } from "../errors.js";
|
|
5
5
|
import { getStealthProfile } from "../stealth/profiles.js";
|
|
6
6
|
import { createProxyAuthIpDeniedError, createProxyEdgeAuthRejectedError, createProxyEdgeTlsRejectedError, createProxyPoolExhaustedError, createProxyPoolStaleError, isProxyAuthIpDeniedMessage, isProxyEdgeAuthRejectedMessage, isProxyEdgeTlsRejectedResponse, isProxyPoolRefreshableError, isProxyPoolStaleMessage, isProxyPoolStaleStatus, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_STALE_CODE, } from "./proxy-errors.js";
|
|
@@ -8,7 +8,11 @@ import { computeProxyAttemptIndex, computeProxyTransportRetryDelayMs, createDefa
|
|
|
8
8
|
import { appendQueryParams } from "./request-options.js";
|
|
9
9
|
const DEFAULT_PROFILE = "chrome-146";
|
|
10
10
|
const MISSING_PROXY_WARNING = "[provider-sdk] Provider requested proxy routing, but no proxy URL was configured. Continuing without proxy.";
|
|
11
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Upper bound on attempts across a multi-vendor chain, so a two-vendor chain can
|
|
13
|
+
* exhaust each vendor's pool before failing over and finally throwing.
|
|
14
|
+
*/
|
|
15
|
+
const MAX_POLICY_PROXY_TOTAL_ATTEMPTS = SMARTPROXY_MAX_POOL_SIZE * 2;
|
|
12
16
|
const MAX_POLICY_PROXY_POOL_REFRESHES = 1;
|
|
13
17
|
const PROXY_CONNECT_FAILURE_CODE = "proxy_connect_failed";
|
|
14
18
|
const PROXY_CONNECT_FAILURE_BODY_PATTERN = /\bproxy\b.*\b(non[\s-]?200|connect|tunnel)|\bconnect\b.*\bproxy\b|\btunnel\b/i;
|
|
@@ -463,7 +467,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
463
467
|
}
|
|
464
468
|
return client;
|
|
465
469
|
}
|
|
466
|
-
async function resolveRequestProxy(options, proxyAttempt) {
|
|
470
|
+
async function resolveRequestProxy(options, proxyAttempt, refreshEpoch) {
|
|
467
471
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
468
472
|
proxy: options?.proxy ?? clientOptions.proxy,
|
|
469
473
|
upstream: clientOptions.upstream,
|
|
@@ -474,6 +478,10 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
474
478
|
proxyAttemptOffset: options?.proxyAttemptOffset,
|
|
475
479
|
retryAttemptOffset: proxyAttempt,
|
|
476
480
|
}),
|
|
481
|
+
// The impit stealth transport tunnels both HTTP CONNECT and SOCKS5,
|
|
482
|
+
// preserving the client TLS fingerprint end-to-end.
|
|
483
|
+
transportProtocols: ["http", "socks5"],
|
|
484
|
+
...(refreshEpoch === undefined ? {} : { proxyRefreshEpoch: refreshEpoch }),
|
|
477
485
|
telemetry: clientOptions.telemetry,
|
|
478
486
|
});
|
|
479
487
|
if (resolvedProxy.shouldWarn && !hasWarnedMissingProxy) {
|
|
@@ -484,6 +492,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
484
492
|
url: resolvedProxy.url,
|
|
485
493
|
poolIndex: proxyPoolIndexFromDiagnostics(resolvedProxy.diagnostics),
|
|
486
494
|
proxyHash: proxyEndpointHash(resolvedProxy.url),
|
|
495
|
+
vendor: vendorFromResolvedSource(resolvedProxy.source),
|
|
487
496
|
};
|
|
488
497
|
}
|
|
489
498
|
const session = {
|
|
@@ -506,11 +515,13 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
506
515
|
const hasPolicyProxy = isPolicyManagedProxy(clientOptions);
|
|
507
516
|
const usesPolicyAllocator = hasPolicyProxy && !options.proxy && !clientOptions.proxy;
|
|
508
517
|
const retryAttemptCap = Math.max(1, stealthRetryOptions?.attempts ?? 1);
|
|
509
|
-
|
|
518
|
+
// Span the whole vendor chain: successive attempts rotate one vendor's
|
|
519
|
+
// pool, then fail over to the next vendor via the flat attempt index.
|
|
520
|
+
const policyProxy = clientOptions.proxyPolicy ??
|
|
510
521
|
(typeof clientOptions.upstream?.proxy === "object"
|
|
511
|
-
? clientOptions.upstream.proxy
|
|
512
|
-
: undefined)
|
|
513
|
-
|
|
522
|
+
? clientOptions.upstream.proxy
|
|
523
|
+
: undefined);
|
|
524
|
+
const policyProxyAttemptCap = Math.max(1, Math.min(MAX_POLICY_PROXY_TOTAL_ATTEMPTS, policyProxy ? resolvePolicyProxyPoolSpan(policyProxy) : DEFAULT_SMARTPROXY_POOL_SIZE));
|
|
514
525
|
const maxAttempts = usesPolicyAllocator ? policyProxyAttemptCap : retryAttemptCap;
|
|
515
526
|
let lastError;
|
|
516
527
|
for (let refreshAttempt = 0; refreshAttempt <= MAX_POLICY_PROXY_POOL_REFRESHES; refreshAttempt += 1) {
|
|
@@ -527,7 +538,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
527
538
|
return;
|
|
528
539
|
attemptRecorded = true;
|
|
529
540
|
clientOptions.telemetry?.recordProxyAttempt?.({
|
|
530
|
-
provider: "smartproxy",
|
|
541
|
+
provider: attemptProxy?.vendor ?? "smartproxy",
|
|
531
542
|
attempt: attempt + 1,
|
|
532
543
|
...(attemptProxy?.poolIndex === undefined
|
|
533
544
|
? {}
|
|
@@ -541,7 +552,7 @@ function createSessionFetcher(baseUrl, defaultProfile, clientOptions) {
|
|
|
541
552
|
};
|
|
542
553
|
try {
|
|
543
554
|
assertNoUnsupportedFingerprintOverrides(options);
|
|
544
|
-
attemptProxy = await resolveRequestProxy(options, attempt);
|
|
555
|
+
attemptProxy = await resolveRequestProxy(options, attempt, refreshAttempt);
|
|
545
556
|
proxy = attemptProxy.url;
|
|
546
557
|
if (proxy && usesPolicyAllocator) {
|
|
547
558
|
if (attemptedProxies.has(proxy)) {
|
package/dist/server/serve.d.ts
CHANGED
|
@@ -35,6 +35,11 @@ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
|
|
|
35
35
|
message: string;
|
|
36
36
|
}>;
|
|
37
37
|
}) | {
|
|
38
|
+
level: "warn";
|
|
39
|
+
event: "provider_secrets_missing";
|
|
40
|
+
providerId: string;
|
|
41
|
+
missingSecrets: string[];
|
|
42
|
+
} | {
|
|
38
43
|
level: "warn";
|
|
39
44
|
event: "provider_cleanup_failed";
|
|
40
45
|
providerId: string;
|
package/dist/server/serve.js
CHANGED
|
@@ -18,6 +18,7 @@ import { wrapWithInstrumentation } from "../runtime/instrumentation.js";
|
|
|
18
18
|
import { getProviderBaseUrl } from "../runtime/provider.js";
|
|
19
19
|
import { PROXY_AUTH_IP_DENIED_CODE, PROXY_EDGE_AUTH_REJECTED_CODE, PROXY_POOL_EXHAUSTED_CODE, } from "../runtime/proxy-errors.js";
|
|
20
20
|
import { PROVIDER_TELEMETRY_HEADER, ProxyTelemetryCollector } from "../runtime/proxy-telemetry.js";
|
|
21
|
+
import { assertRequiredSecretsPresent, listMissingRequiredSecrets, MISSING_SECRET_CODE, } from "../runtime/secrets.js";
|
|
21
22
|
import { createProviderRuntimeStateFromEnv, createUnsupportedProviderRuntimeState, } from "../runtime/state.js";
|
|
22
23
|
import { createStealthClient } from "../runtime/stealth.js";
|
|
23
24
|
import { createSttClientFromEnv } from "../runtime/stt.js";
|
|
@@ -357,6 +358,18 @@ function providerObservabilityDetails(error) {
|
|
|
357
358
|
retryable: error.options?.retryable ?? false,
|
|
358
359
|
};
|
|
359
360
|
}
|
|
361
|
+
// Missing-secret errors carry the canonical credential_unavailable category
|
|
362
|
+
// so Gateway/observability can attribute the failure to provisioning, not
|
|
363
|
+
// the upstream. Matched by code (not constructor) so both the SDK-owned
|
|
364
|
+
// runtime gate and any not-yet-migrated provider-thrown MISSING_SECRET
|
|
365
|
+
// serialize identically, including across duplicate SDK module instances.
|
|
366
|
+
if (isProviderError(error) && error.code === MISSING_SECRET_CODE) {
|
|
367
|
+
return {
|
|
368
|
+
category: error.options?.category ?? "credential_unavailable",
|
|
369
|
+
taxonomyVersion: PROVIDER_OBSERVABILITY_TAXONOMY_VERSION,
|
|
370
|
+
retryable: error.options?.retryable ?? false,
|
|
371
|
+
};
|
|
372
|
+
}
|
|
360
373
|
if (!isTransportError(error)) {
|
|
361
374
|
return undefined;
|
|
362
375
|
}
|
|
@@ -422,6 +435,10 @@ function toStatusCode(error) {
|
|
|
422
435
|
case "AUTH_REQUIRED":
|
|
423
436
|
case "reauth_required":
|
|
424
437
|
return 401;
|
|
438
|
+
// Unprovisioned declared secret: a deployment/config defect, never an
|
|
439
|
+
// upstream failure — explicit 400 (was only reached via fallthrough).
|
|
440
|
+
case MISSING_SECRET_CODE:
|
|
441
|
+
return 400;
|
|
425
442
|
case "NOT_FOUND":
|
|
426
443
|
case "not_found":
|
|
427
444
|
case "NO_DATA":
|
|
@@ -885,8 +902,16 @@ async function handleAuthFlow(provider, request, route, options = {}, signal) {
|
|
|
885
902
|
code: "AUTH_FLOW_NOT_CONFIGURED",
|
|
886
903
|
});
|
|
887
904
|
}
|
|
905
|
+
// Same SDK-owned gate as executeOperation: OAuth/credentials ceremonies
|
|
906
|
+
// depend on declared secrets (client ids/secrets), so fail structured before
|
|
907
|
+
// any flow code runs instead of at whatever point the ceremony first reads
|
|
908
|
+
// the env. `abort` stays exempt: a user must always be able to cancel a
|
|
909
|
+
// stranded flow even when provisioning is broken.
|
|
888
910
|
const { context, getPatch } = createAuthFlowContext(provider, request, options, signal);
|
|
889
911
|
try {
|
|
912
|
+
if (route !== "abort") {
|
|
913
|
+
assertRequiredSecretsPresent(provider, context.env);
|
|
914
|
+
}
|
|
890
915
|
const result = route === "start"
|
|
891
916
|
? await flow.start(context)
|
|
892
917
|
: route === "continue"
|
|
@@ -933,6 +958,20 @@ export function createServerApp(provider, options = {}) {
|
|
|
933
958
|
providerId: provider.id,
|
|
934
959
|
allowMemoryFallback: options.allowMemoryStateFallback === true,
|
|
935
960
|
});
|
|
961
|
+
// Boot-time visibility for unprovisioned declared secrets: emit a structured
|
|
962
|
+
// warn so deploy tooling/alerting sees the gap the moment the pod boots,
|
|
963
|
+
// instead of discovering it request-by-request. Deliberately log-only — a
|
|
964
|
+
// boot crash would trade a structured MISSING_SECRET signal for
|
|
965
|
+
// CrashLoopBackOff. Requests still fail closed via the executeOperation gate.
|
|
966
|
+
const missingSecretsAtBoot = listMissingRequiredSecrets(provider, createEnvContext(provider.secrets?.map((secret) => secret.name)));
|
|
967
|
+
if (missingSecretsAtBoot.length > 0) {
|
|
968
|
+
logger({
|
|
969
|
+
level: "warn",
|
|
970
|
+
event: "provider_secrets_missing",
|
|
971
|
+
providerId: provider.id,
|
|
972
|
+
missingSecrets: missingSecretsAtBoot,
|
|
973
|
+
});
|
|
974
|
+
}
|
|
936
975
|
app.notFound((c) => c.json({
|
|
937
976
|
error: {
|
|
938
977
|
code: "not_found",
|
package/dist/server/types.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const ConnectionModeSchema: z.ZodEnum<{
|
|
3
|
-
credentials: "credentials";
|
|
4
3
|
none: "none";
|
|
4
|
+
credentials: "credentials";
|
|
5
5
|
oauth2: "oauth2";
|
|
6
6
|
"platform-managed": "platform-managed";
|
|
7
7
|
}>;
|
|
8
8
|
export declare const OperationConnectionSchema: z.ZodObject<{
|
|
9
9
|
id: z.ZodString;
|
|
10
10
|
mode: z.ZodEnum<{
|
|
11
|
-
credentials: "credentials";
|
|
12
11
|
none: "none";
|
|
12
|
+
credentials: "credentials";
|
|
13
13
|
oauth2: "oauth2";
|
|
14
14
|
"platform-managed": "platform-managed";
|
|
15
15
|
}>;
|
|
@@ -25,8 +25,8 @@ export declare const OperationRequestSchema: z.ZodObject<{
|
|
|
25
25
|
connection: z.ZodOptional<z.ZodObject<{
|
|
26
26
|
id: z.ZodString;
|
|
27
27
|
mode: z.ZodEnum<{
|
|
28
|
-
credentials: "credentials";
|
|
29
28
|
none: "none";
|
|
29
|
+
credentials: "credentials";
|
|
30
30
|
oauth2: "oauth2";
|
|
31
31
|
"platform-managed": "platform-managed";
|
|
32
32
|
}>;
|
|
@@ -55,21 +55,21 @@ export declare const OperationSuccessResponseSchema: z.ZodObject<{
|
|
|
55
55
|
stale: z.ZodBoolean;
|
|
56
56
|
keys: z.ZodArray<z.ZodString>;
|
|
57
57
|
source: z.ZodOptional<z.ZodEnum<{
|
|
58
|
-
loader: "loader";
|
|
59
|
-
memory: "memory";
|
|
60
58
|
mixed: "mixed";
|
|
61
59
|
redis: "redis";
|
|
60
|
+
memory: "memory";
|
|
61
|
+
loader: "loader";
|
|
62
62
|
}>>;
|
|
63
63
|
}, z.core.$strip>>;
|
|
64
64
|
retry: z.ZodOptional<z.ZodObject<{
|
|
65
65
|
attempts: z.ZodNumber;
|
|
66
66
|
retries: z.ZodNumber;
|
|
67
67
|
preset: z.ZodOptional<z.ZodEnum<{
|
|
68
|
-
aggressive_read: "aggressive_read";
|
|
69
68
|
off: "off";
|
|
70
|
-
rate_limit_aware: "rate_limit_aware";
|
|
71
|
-
safe_read: "safe_read";
|
|
72
69
|
transport_transient: "transport_transient";
|
|
70
|
+
safe_read: "safe_read";
|
|
71
|
+
aggressive_read: "aggressive_read";
|
|
72
|
+
rate_limit_aware: "rate_limit_aware";
|
|
73
73
|
}>>;
|
|
74
74
|
transport: z.ZodEnum<{
|
|
75
75
|
native: "native";
|
|
@@ -101,8 +101,8 @@ export declare const AuthFlowRequestSchema: z.ZodObject<{
|
|
|
101
101
|
connection: z.ZodOptional<z.ZodObject<{
|
|
102
102
|
id: z.ZodString;
|
|
103
103
|
mode: z.ZodEnum<{
|
|
104
|
-
credentials: "credentials";
|
|
105
104
|
none: "none";
|
|
105
|
+
credentials: "credentials";
|
|
106
106
|
oauth2: "oauth2";
|
|
107
107
|
"platform-managed": "platform-managed";
|
|
108
108
|
}>;
|
package/dist/types.d.ts
CHANGED
|
@@ -650,7 +650,25 @@ export type ConnectionMode = AuthMode;
|
|
|
650
650
|
export type ProviderReviewed = "first-party" | "community" | "staging";
|
|
651
651
|
export type ProviderAccessVisibility = "public" | "early_access";
|
|
652
652
|
export type ProviderProxyMode = "disabled" | "optional" | "required";
|
|
653
|
-
|
|
653
|
+
/**
|
|
654
|
+
* Proxy egress vendors. These are FOUR DISTINCT services — do not conflate them
|
|
655
|
+
* (a common mistake because the names collide with a well-known rebrand):
|
|
656
|
+
*
|
|
657
|
+
* - `smartproxy` — **api.smartproxy.org**, a residential proxy with an IP
|
|
658
|
+
* *extraction/allocation* API (app_key → a pool of raw `ip:port` CONNECT
|
|
659
|
+
* endpoints). This is our own vendor. It is NOT the company formerly named
|
|
660
|
+
* "Smartproxy". Credentials: `APIFUSE__PROXY__SMARTPROXY_APP_KEY`.
|
|
661
|
+
* - `nodemaven` — **gate.nodemaven.com**, a *gateway* proxy with static
|
|
662
|
+
* credentials; geo/session encoded in the username, no allocation API.
|
|
663
|
+
* - `decodo` — **decodo.com**, the *gateway* proxy that was named "Smartproxy"
|
|
664
|
+
* (smartproxy.com) before its 2025 rebrand to Decodo. Sticky sessions via
|
|
665
|
+
* username params. A different company from `smartproxy` above.
|
|
666
|
+
* **@deprecated** — unused; no managed adapter. Use `smartproxy`/`nodemaven`,
|
|
667
|
+
* or the `APIFUSE__PROXY__URL` bring-your-own escape hatch.
|
|
668
|
+
* - `custom` — **@deprecated** bring-your-own static proxy URL marker. The
|
|
669
|
+
* `APIFUSE__PROXY__URL` env still works without declaring this value.
|
|
670
|
+
*/
|
|
671
|
+
export type ProviderProxyProvider = "smartproxy" | "nodemaven" | "decodo" | "custom";
|
|
654
672
|
export type ProviderProxySessionAffinity = "request" | "operation" | "auth-flow" | "connection";
|
|
655
673
|
export interface ProviderProxyPolicy {
|
|
656
674
|
/**
|
|
@@ -658,7 +676,18 @@ export interface ProviderProxyPolicy {
|
|
|
658
676
|
* certificate verification, and vendor allocator endpoints are SDK-owned.
|
|
659
677
|
*/
|
|
660
678
|
mode: ProviderProxyMode;
|
|
679
|
+
/**
|
|
680
|
+
* @deprecated Use `providers: [...]` to declare an ordered vendor fallback
|
|
681
|
+
* chain. A single-element `providers` list is equivalent to this field.
|
|
682
|
+
*/
|
|
661
683
|
provider?: ProviderProxyProvider;
|
|
684
|
+
/**
|
|
685
|
+
* Ordered proxy-vendor fallback chain. The SDK tries each vendor in order and
|
|
686
|
+
* fails over to the next when a vendor lacks credentials or its allocation /
|
|
687
|
+
* transport is exhausted. When omitted, `provider` (or the platform default)
|
|
688
|
+
* is used as a single-vendor chain.
|
|
689
|
+
*/
|
|
690
|
+
providers?: ProviderProxyProvider[];
|
|
662
691
|
geo?: {
|
|
663
692
|
/** ISO 3166-1 alpha-2 country code, for example KR or US. */
|
|
664
693
|
country?: Iso3166Alpha2CountryCode;
|
package/package.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "2.2.0-beta.
|
|
2
|
+
"version": "2.2.0-beta.8",
|
|
3
3
|
"name": "@apifuse/provider-sdk",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
@@ -75,10 +75,11 @@
|
|
|
75
75
|
"scripts": {
|
|
76
76
|
"lint": "biome lint .",
|
|
77
77
|
"lint:fix": "biome lint --write",
|
|
78
|
+
"lint:deprecated": "bun run scripts/lint-deprecated-usage.ts",
|
|
78
79
|
"format": "biome format --write",
|
|
79
80
|
"type-check": "tsc --noEmit",
|
|
80
81
|
"test": "bun test",
|
|
81
|
-
"check": "bun run lint && bun run type-check && bun run build",
|
|
82
|
+
"check": "bun run lint && bun run type-check && bun run lint:deprecated && bun run build",
|
|
82
83
|
"pack:check": "bun run build && bun bin/apifuse-pack-check.ts",
|
|
83
84
|
"pack:smoke": "bun run build && bun bin/apifuse-pack-smoke.ts",
|
|
84
85
|
"pack:types": "bun run build && bun bin/apifuse-pack-types.ts",
|
|
@@ -91,7 +92,7 @@
|
|
|
91
92
|
"@biomejs/biome": "^2.5.0",
|
|
92
93
|
"@types/bun": "latest",
|
|
93
94
|
"@types/node": "^25.9.3",
|
|
94
|
-
"typescript": "
|
|
95
|
+
"typescript": "6.0.3"
|
|
95
96
|
},
|
|
96
97
|
"dependencies": {
|
|
97
98
|
"@clack/prompts": "^1.5.1",
|
package/src/cli/commands.ts
CHANGED
|
@@ -2,6 +2,7 @@ export type ApifuseCommandName =
|
|
|
2
2
|
| "create"
|
|
3
3
|
| "dev"
|
|
4
4
|
| "check"
|
|
5
|
+
| "sync-assets"
|
|
5
6
|
| "submit-check"
|
|
6
7
|
| "bounty-check"
|
|
7
8
|
| "record"
|
|
@@ -46,6 +47,14 @@ export const COMMAND_MANIFEST: Record<
|
|
|
46
47
|
examples: ["apifuse check .", "apifuse check providers/korea-air-quality"],
|
|
47
48
|
modulePath: "./apifuse-check",
|
|
48
49
|
},
|
|
50
|
+
"sync-assets": {
|
|
51
|
+
name: "sync-assets",
|
|
52
|
+
summary:
|
|
53
|
+
"Regenerate SDK-managed agent prompt assets (AGENTS.md, .agents/skills, symlinks, manifest) for the installed SDK version.",
|
|
54
|
+
usage: "apifuse sync-assets [path] [--check]",
|
|
55
|
+
examples: ["apifuse sync-assets .", "apifuse sync-assets . --check"],
|
|
56
|
+
modulePath: "./apifuse-sync-assets",
|
|
57
|
+
},
|
|
49
58
|
"submit-check": {
|
|
50
59
|
name: "submit-check",
|
|
51
60
|
summary:
|
|
@@ -103,6 +112,7 @@ export const COMMAND_ORDER: ApifuseCommandName[] = [
|
|
|
103
112
|
"create",
|
|
104
113
|
"dev",
|
|
105
114
|
"check",
|
|
115
|
+
"sync-assets",
|
|
106
116
|
"submit-check",
|
|
107
117
|
"record",
|
|
108
118
|
"test",
|