@apifuse/provider-sdk 2.2.0-beta.40 → 2.2.0-beta.41
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/CHANGELOG.md +4 -0
- package/bin/apifuse-check.ts +61 -0
- package/bin/apifuse-migrate-shape.ts +84 -0
- package/bin/apifuse-submit-check.ts +1760 -222
- package/dist/cli/commands.d.ts +1 -1
- package/dist/cli/commands.js +8 -0
- package/dist/cli/create.js +6 -0
- package/dist/cli/migrate-provider-shape.d.ts +52 -0
- package/dist/cli/migrate-provider-shape.js +515 -0
- package/dist/cli/templates/provider/provider.json.tpl +6 -0
- package/dist/contract.js +1 -0
- package/dist/define.js +22 -1
- package/dist/error-observability.d.ts +7 -0
- package/dist/error-observability.js +61 -0
- package/dist/errors.d.ts +15 -0
- package/dist/fixture-sanitization.js +13 -3
- package/dist/index.d.ts +1 -1
- package/dist/provider.d.ts +1 -1
- package/dist/runtime/executor.js +11 -1
- package/dist/server/error-observability.d.ts +1 -0
- package/dist/server/error-observability.js +1 -0
- package/dist/server/index.d.ts +2 -1
- package/dist/server/self-test.js +3 -0
- package/dist/server/serve-implementation.d.ts +12 -0
- package/dist/server/serve-implementation.js +135 -66
- package/dist/types.d.ts +18 -10
- package/package.json +3 -3
- package/src/cli/commands.ts +10 -0
- package/src/cli/create.ts +6 -0
- package/src/cli/migrate-provider-shape.ts +701 -0
- package/src/cli/templates/provider/provider.json.tpl +6 -0
- package/src/contract.ts +1 -0
- package/src/define.ts +33 -1
- package/src/error-observability.ts +64 -0
- package/src/errors.ts +16 -0
- package/src/fixture-sanitization.ts +19 -3
- package/src/index.ts +1 -0
- package/src/provider.ts +1 -0
- package/src/runtime/executor.ts +13 -1
- package/src/server/error-observability.ts +1 -0
- package/src/server/index.ts +2 -0
- package/src/server/self-test.ts +5 -0
- package/src/server/serve-implementation.ts +172 -84
- package/src/types.ts +38 -27
package/src/define.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {
|
|
|
6
6
|
} from "./declaration-validation.js";
|
|
7
7
|
import { SDK_RUNTIME_OWNED_ERROR_CODES } from "./error-resolution.js";
|
|
8
8
|
import { ProviderError, ValidationError } from "./errors.js";
|
|
9
|
+
import { HealthScenarioSchema } from "./health-scenario.js";
|
|
9
10
|
import {
|
|
10
11
|
NativeEgressPolicyValidationError,
|
|
11
12
|
validateNativeProviderConfig,
|
|
@@ -1730,6 +1731,7 @@ const HEALTH_CHECK_CASE_FIELDS = new Set([
|
|
|
1730
1731
|
"input",
|
|
1731
1732
|
"prepareInput",
|
|
1732
1733
|
"assertions",
|
|
1734
|
+
"scenario",
|
|
1733
1735
|
"degradedThresholdMs",
|
|
1734
1736
|
"timeoutMs",
|
|
1735
1737
|
"expectedStatus",
|
|
@@ -1979,7 +1981,19 @@ function validateHealthCheckCase(
|
|
|
1979
1981
|
throw new ValidationError(
|
|
1980
1982
|
`Provider "${providerId}" ${fieldPath}.name must be a non-empty string.`,
|
|
1981
1983
|
);
|
|
1982
|
-
|
|
1984
|
+
const hasScenario = c.scenario !== undefined;
|
|
1985
|
+
const imperativeFields = [
|
|
1986
|
+
...(c.prepareInput === undefined ? [] : ["prepareInput"]),
|
|
1987
|
+
...(c.assertions === undefined ? [] : ["assertions"]),
|
|
1988
|
+
];
|
|
1989
|
+
if (hasScenario && imperativeFields.length > 0)
|
|
1990
|
+
throw new ValidationError(
|
|
1991
|
+
`Provider "${providerId}" operation "${operationName}" health-check case "${c.name}" cannot declare scenario with ${imperativeFields.join(" and ")}.`,
|
|
1992
|
+
{
|
|
1993
|
+
fix: `Remove ${imperativeFields.map((field) => `${fieldPath}.${field}`).join(" and ")} and keep ${fieldPath}.scenario, or remove ${fieldPath}.scenario to keep the imperative hooks.`,
|
|
1994
|
+
},
|
|
1995
|
+
);
|
|
1996
|
+
if (!hasScenario && typeof c.assertions !== "function")
|
|
1983
1997
|
throw new ValidationError(
|
|
1984
1998
|
`Provider "${providerId}" ${fieldPath}.assertions must be a function.`,
|
|
1985
1999
|
{
|
|
@@ -1990,6 +2004,24 @@ function validateHealthCheckCase(
|
|
|
1990
2004
|
throw new ValidationError(
|
|
1991
2005
|
`Provider "${providerId}" ${fieldPath}.prepareInput must be a function.`,
|
|
1992
2006
|
);
|
|
2007
|
+
if (hasScenario) {
|
|
2008
|
+
const parsedScenario = HealthScenarioSchema.safeParse(c.scenario);
|
|
2009
|
+
if (!parsedScenario.success)
|
|
2010
|
+
throw new ValidationError(
|
|
2011
|
+
`Provider "${providerId}" operation "${operationName}" health-check case "${c.name}" has an invalid scenario: it must conform to HealthScenario.`,
|
|
2012
|
+
{ fix: `Build ${fieldPath}.scenario with defineHealthScenario().` },
|
|
2013
|
+
);
|
|
2014
|
+
const unrelatedOperation = parsedScenario.data.coversOperations.find(
|
|
2015
|
+
(operationId) => operationId !== operationName,
|
|
2016
|
+
);
|
|
2017
|
+
if (unrelatedOperation !== undefined)
|
|
2018
|
+
throw new ValidationError(
|
|
2019
|
+
`Provider "${providerId}" operation "${operationName}" health-check case "${c.name}" scenario.coversOperations cannot claim unrelated operation "${unrelatedOperation}".`,
|
|
2020
|
+
{
|
|
2021
|
+
fix: `Set ${fieldPath}.scenario.coversOperations to ["${operationName}"].`,
|
|
2022
|
+
},
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
1993
2025
|
if (
|
|
1994
2026
|
c.degradedThresholdMs !== undefined &&
|
|
1995
2027
|
(typeof c.degradedThresholdMs !== "number" ||
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { isProviderError, type ProviderErrorObservability } from "./errors.js";
|
|
2
|
+
|
|
3
|
+
const PROVIDER_OBSERVABILITY_TOKEN_PATTERN = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
4
|
+
const PROVIDER_OBSERVABILITY_FINGERPRINT_PATTERN = /^[A-Fa-f0-9]{12}$/;
|
|
5
|
+
const MAX_PROVIDER_OBSERVABILITY_MESSAGE_LENGTH = 10_000_000;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Extracts only own data properties from branded provider errors. In
|
|
9
|
+
* particular, descriptor reads reject options/observability accessors without
|
|
10
|
+
* invoking provider-controlled getters.
|
|
11
|
+
*/
|
|
12
|
+
export function safeProviderErrorObservability(
|
|
13
|
+
error: unknown,
|
|
14
|
+
): ProviderErrorObservability | undefined {
|
|
15
|
+
if (!isProviderError(error)) return undefined;
|
|
16
|
+
let candidate: unknown;
|
|
17
|
+
let reason: unknown;
|
|
18
|
+
let fingerprint: unknown;
|
|
19
|
+
let messageLength: unknown;
|
|
20
|
+
try {
|
|
21
|
+
const optionsDescriptor = Object.getOwnPropertyDescriptor(error, "options");
|
|
22
|
+
if (optionsDescriptor === undefined || !Object.hasOwn(optionsDescriptor, "value")) {
|
|
23
|
+
return undefined;
|
|
24
|
+
}
|
|
25
|
+
const options = optionsDescriptor.value;
|
|
26
|
+
if (options === null || typeof options !== "object" || Array.isArray(options)) {
|
|
27
|
+
return undefined;
|
|
28
|
+
}
|
|
29
|
+
const observabilityDescriptor = Object.getOwnPropertyDescriptor(options, "observability");
|
|
30
|
+
if (observabilityDescriptor === undefined || !Object.hasOwn(observabilityDescriptor, "value")) {
|
|
31
|
+
return undefined;
|
|
32
|
+
}
|
|
33
|
+
candidate = observabilityDescriptor.value;
|
|
34
|
+
if (candidate === null || typeof candidate !== "object" || Array.isArray(candidate)) {
|
|
35
|
+
return undefined;
|
|
36
|
+
}
|
|
37
|
+
const ownValue = (key: keyof ProviderErrorObservability): unknown => {
|
|
38
|
+
const descriptor = Object.getOwnPropertyDescriptor(candidate as object, key);
|
|
39
|
+
return descriptor && Object.hasOwn(descriptor, "value") ? descriptor.value : undefined;
|
|
40
|
+
};
|
|
41
|
+
reason = ownValue("reason");
|
|
42
|
+
fingerprint = ownValue("fingerprint");
|
|
43
|
+
messageLength = ownValue("messageLength");
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
const safe: ProviderErrorObservability = {
|
|
48
|
+
...(typeof reason === "string" && PROVIDER_OBSERVABILITY_TOKEN_PATTERN.test(reason)
|
|
49
|
+
? { reason }
|
|
50
|
+
: {}),
|
|
51
|
+
...(typeof fingerprint === "string" &&
|
|
52
|
+
PROVIDER_OBSERVABILITY_FINGERPRINT_PATTERN.test(fingerprint)
|
|
53
|
+
? { fingerprint }
|
|
54
|
+
: {}),
|
|
55
|
+
...(typeof messageLength === "number" &&
|
|
56
|
+
Number.isInteger(messageLength) &&
|
|
57
|
+
messageLength >= 0 &&
|
|
58
|
+
messageLength <= MAX_PROVIDER_OBSERVABILITY_MESSAGE_LENGTH
|
|
59
|
+
? { messageLength }
|
|
60
|
+
: {}),
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
return Object.keys(safe).length > 0 ? safe : undefined;
|
|
64
|
+
}
|
package/src/errors.ts
CHANGED
|
@@ -46,6 +46,22 @@ export type ProviderErrorOptions = {
|
|
|
46
46
|
cause?: Error;
|
|
47
47
|
category?: ProviderErrorCategory;
|
|
48
48
|
retryable?: boolean;
|
|
49
|
+
/** Provider-authored, bounded metadata safe for operational logs and error headers. */
|
|
50
|
+
observability?: ProviderErrorObservability;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Provider-authored error diagnostics whose runtime values are validated before emission.
|
|
55
|
+
* Classification tokens such as `reason` are source literals, not runtime user input or
|
|
56
|
+
* credentials. The gateway removes the observability header from tenant responses.
|
|
57
|
+
*/
|
|
58
|
+
export type ProviderErrorObservability = {
|
|
59
|
+
/** A 1-64 character `[A-Za-z0-9_.-]` classification token, for example `LOGIN_COMPLETE_FAILED`. */
|
|
60
|
+
reason?: string;
|
|
61
|
+
/** A provider-computed 12-hex-character fingerprint of private diagnostic input. */
|
|
62
|
+
fingerprint?: string;
|
|
63
|
+
/** The non-negative length of the private diagnostic input, capped at 10,000,000. */
|
|
64
|
+
messageLength?: number;
|
|
49
65
|
};
|
|
50
66
|
|
|
51
67
|
export class ProviderError extends Error {
|
|
@@ -5,6 +5,12 @@ export const REDACTED_FIXTURE_VALUE = "[REDACTED]";
|
|
|
5
5
|
const OPAQUE_TOKEN = /^[A-Za-z0-9_+/=.:~-]+$/;
|
|
6
6
|
const OPAQUE_TOKEN_RUN = /[A-Za-z0-9_+/=.:~-]{24,}/g;
|
|
7
7
|
const URL_RUN = /https?:\/\/[^\s"'<>]+/gi;
|
|
8
|
+
const EMAIL_ADDRESS_RUN = /\b[A-Z0-9._%+-]+@[A-Z0-9.-]+\.[A-Z]{2,}\b/gi;
|
|
9
|
+
const DIAGNOSTIC_URL_SENTINEL_DELIMITER = String.fromCodePoint(0);
|
|
10
|
+
const DIAGNOSTIC_URL_SENTINEL_RUN = new RegExp(
|
|
11
|
+
`${DIAGNOSTIC_URL_SENTINEL_DELIMITER}APIFUSE_URL(\\d+)${DIAGNOSTIC_URL_SENTINEL_DELIMITER}`,
|
|
12
|
+
"g",
|
|
13
|
+
);
|
|
8
14
|
const PEM_PRIVATE_KEY =
|
|
9
15
|
/-----BEGIN (?:[A-Z0-9 ]+ )?PRIVATE KEY-----[\s\S]*?-----END (?:[A-Z0-9 ]+ )?PRIVATE KEY-----/g;
|
|
10
16
|
|
|
@@ -149,9 +155,15 @@ export function requestPathForFixture(value: string): string {
|
|
|
149
155
|
|
|
150
156
|
/** Scrubs secrets and terminal/log control characters before diagnostic text is emitted. */
|
|
151
157
|
export function sanitizeDiagnosticText(value: string): string {
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
158
|
+
const retainedUrls: string[] = [];
|
|
159
|
+
// Remove attacker-controlled NUL delimiters before introducing internal URL sentinels.
|
|
160
|
+
let sanitized = encodeDiagnosticControls(value)
|
|
161
|
+
.replace(URL_RUN, (url) => {
|
|
162
|
+
const index = retainedUrls.push(sanitizeUrlForLogs(url)) - 1;
|
|
163
|
+
return `${DIAGNOSTIC_URL_SENTINEL_DELIMITER}APIFUSE_URL${index}${DIAGNOSTIC_URL_SENTINEL_DELIMITER}`;
|
|
164
|
+
})
|
|
165
|
+
.replace(/\bBearer\s+[A-Za-z0-9._~+/=-]+/gi, `Bearer ${REDACTED_FIXTURE_VALUE}`)
|
|
166
|
+
.replace(EMAIL_ADDRESS_RUN, REDACTED_FIXTURE_VALUE);
|
|
155
167
|
sanitized = redactSensitiveAssignments(sanitized);
|
|
156
168
|
sanitized = sanitized.replace(OPAQUE_TOKEN_RUN, (candidate, offset: number, source: string) => {
|
|
157
169
|
if (/^(?:request|trace|correlation)[-_]?id[:=]/i.test(candidate)) return candidate;
|
|
@@ -159,6 +171,10 @@ export function sanitizeDiagnosticText(value: string): string {
|
|
|
159
171
|
if (/(?:request|trace|correlation)[-_]?id\s*[:=]\s*$/i.test(prefix)) return candidate;
|
|
160
172
|
return isSensitiveFixtureValue(candidate) ? REDACTED_FIXTURE_VALUE : candidate;
|
|
161
173
|
});
|
|
174
|
+
sanitized = sanitized.replace(
|
|
175
|
+
DIAGNOSTIC_URL_SENTINEL_RUN,
|
|
176
|
+
(_match, index: string) => retainedUrls[Number(index)] ?? REDACTED_FIXTURE_VALUE,
|
|
177
|
+
);
|
|
162
178
|
return encodeDiagnosticControls(sanitized);
|
|
163
179
|
}
|
|
164
180
|
|
package/src/index.ts
CHANGED
package/src/provider.ts
CHANGED
package/src/runtime/executor.ts
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
|
+
import { safeProviderErrorObservability } from "../error-observability.js";
|
|
1
2
|
import {
|
|
2
3
|
isSessionExpiredError,
|
|
3
4
|
isValidationError,
|
|
4
5
|
ProviderError,
|
|
6
|
+
type ProviderErrorOptions,
|
|
5
7
|
SessionExpiredError,
|
|
6
8
|
ValidationError,
|
|
7
9
|
} from "../errors.js";
|
|
@@ -15,6 +17,13 @@ export function isStreamingOperation(provider: ProviderDefinition, operationId:
|
|
|
15
17
|
return kind !== "json";
|
|
16
18
|
}
|
|
17
19
|
|
|
20
|
+
function preservedSessionExpiredOptions(error: unknown): ProviderErrorOptions {
|
|
21
|
+
const observability = safeProviderErrorObservability(error);
|
|
22
|
+
return observability
|
|
23
|
+
? { observability, retryable: true }
|
|
24
|
+
: { retryable: true };
|
|
25
|
+
}
|
|
26
|
+
|
|
18
27
|
/**
|
|
19
28
|
* Execute a provider operation by calling its handler.
|
|
20
29
|
*
|
|
@@ -84,7 +93,10 @@ export async function executeOperation<
|
|
|
84
93
|
// executor's, which `instanceof` would miss — dropping the retryable
|
|
85
94
|
// upgrade and stranding an operation that opted into auth refresh.
|
|
86
95
|
if (isSessionExpiredError(error) && operation.retryOnAuthRefresh) {
|
|
87
|
-
|
|
96
|
+
// Preserve provider-authored safe metadata while forcing the retry signal.
|
|
97
|
+
// `cause` intentionally remains dropped, matching the pre-existing
|
|
98
|
+
// reconstruction semantics.
|
|
99
|
+
throw new SessionExpiredError(error.message, preservedSessionExpiredOptions(error));
|
|
88
100
|
}
|
|
89
101
|
throw error;
|
|
90
102
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { safeProviderErrorObservability } from "../error-observability.js";
|
package/src/server/index.ts
CHANGED
|
@@ -17,6 +17,7 @@ export {
|
|
|
17
17
|
type ErrorObservabilityDetails,
|
|
18
18
|
type ProviderServerCloseOptions,
|
|
19
19
|
type ProviderServerHandle,
|
|
20
|
+
type ProviderErrorCauseFrame,
|
|
20
21
|
type ProviderServerLogEvent,
|
|
21
22
|
type ProviderServerLogger,
|
|
22
23
|
type ProviderServerOperationExecutor,
|
|
@@ -26,6 +27,7 @@ export {
|
|
|
26
27
|
type ServeOptions,
|
|
27
28
|
serve,
|
|
28
29
|
} from "./serve.js";
|
|
30
|
+
export type { ProviderErrorObservability } from "../errors.js";
|
|
29
31
|
export {
|
|
30
32
|
computeSelfTestPlanDigest,
|
|
31
33
|
createSelfTestApp,
|
package/src/server/self-test.ts
CHANGED
|
@@ -1106,6 +1106,11 @@ async function executeSelfTestCase(
|
|
|
1106
1106
|
const { startedAtMs, finish } = caseScope;
|
|
1107
1107
|
try {
|
|
1108
1108
|
return await runWithCaseTimeout(async () => {
|
|
1109
|
+
if (healthCase.assertions === undefined) {
|
|
1110
|
+
throw new Error(
|
|
1111
|
+
`Self-test cannot execute declarative health-check case "${healthCase.name}"; run its scenario through the health monitor.`,
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1109
1114
|
const resolvedInput = resolveHealthCheckInputDateTokens(healthCase.input);
|
|
1110
1115
|
const preparedInput = healthCase.prepareInput
|
|
1111
1116
|
? await healthCase.prepareInput({
|