@apifuse/provider-sdk 2.1.0-beta.9 → 2.2.0-beta.1
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 +134 -0
- package/CHANGELOG.md +56 -0
- package/README.md +21 -9
- package/SUBMISSION.md +10 -11
- package/bin/apifuse-pack-check.ts +22 -0
- package/bin/apifuse-submit-check.ts +1747 -388
- package/dist/auth-turn/auth-turn.v1.schema.json +89 -0
- package/dist/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/dist/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/dist/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/dist/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/dist/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/dist/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/dist/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/dist/auth-turn/fixtures/valid/abort.json +8 -0
- package/dist/auth-turn/fixtures/valid/challenge.json +17 -0
- package/dist/auth-turn/fixtures/valid/complete.json +13 -0
- package/dist/auth-turn/fixtures/valid/form.json +14 -0
- package/dist/auth-turn/fixtures/valid/message.json +13 -0
- package/dist/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/dist/auth-turn/fixtures/valid/pending.json +5 -0
- package/dist/auth-turn/fixtures/valid/poll.json +9 -0
- package/dist/auth-turn/fixtures/valid/redirect.json +16 -0
- package/dist/auth-turn/fixtures/valid/retry.json +8 -0
- package/dist/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/dist/auth-turn/index.d.ts +195 -0
- package/dist/auth-turn/index.js +133 -0
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +436 -0
- package/dist/ceremonies/index.js +7 -31
- package/dist/cli/create.js +45 -30
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/dist/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +163 -0
- package/dist/cli/templates/provider/dev.ts.tpl +5 -0
- package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
- package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
- package/dist/cli/templates/provider/index.ts.tpl +15 -0
- package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/dist/cli/templates/provider/meta.ts.tpl +7 -0
- package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- package/dist/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/dist/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/dist/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/dist/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/dist/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/dist/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/dist/cli/templates/provider/start.ts.tpl +5 -0
- package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/dist/contract.js +1 -0
- package/dist/define.d.ts +6 -1
- package/dist/define.js +140 -70
- package/dist/index.d.ts +3 -2
- package/dist/index.js +2 -1
- package/dist/lint.d.ts +1 -0
- package/dist/lint.js +27 -0
- package/dist/provider.d.ts +4 -2
- package/dist/provider.js +2 -1
- package/dist/runtime/auth-flow.js +2 -0
- package/dist/runtime/browser.js +203 -0
- package/dist/runtime/http.js +28 -8
- package/dist/runtime/stealth.d.ts +5 -2
- package/dist/runtime/stealth.js +149 -3
- package/dist/server/index.d.ts +4 -0
- package/dist/server/index.js +4 -0
- package/dist/server/self-test-input-tokens.d.ts +1 -0
- package/dist/server/self-test-input-tokens.js +37 -0
- package/dist/server/self-test-redaction.d.ts +20 -0
- package/dist/server/self-test-redaction.js +70 -0
- package/dist/server/self-test-token.d.ts +30 -0
- package/dist/server/self-test-token.js +50 -0
- package/dist/server/self-test.d.ts +98 -0
- package/dist/server/self-test.js +555 -0
- package/dist/server/serve.d.ts +6 -0
- package/dist/server/serve.js +33 -8
- package/dist/testing/run.js +5 -1
- package/dist/types.d.ts +150 -0
- package/package.json +9 -2
- package/src/auth-turn/auth-turn.v1.schema.json +89 -0
- package/src/auth-turn/fixtures/invalid/empty-kind.json +4 -0
- package/src/auth-turn/fixtures/invalid/expires-at-not-string.json +5 -0
- package/src/auth-turn/fixtures/invalid/missing-kind.json +3 -0
- package/src/auth-turn/fixtures/invalid/missing-turn-id.json +3 -0
- package/src/auth-turn/fixtures/invalid/timing-unknown-field.json +7 -0
- package/src/auth-turn/fixtures/invalid/turn-id-snake-case.json +4 -0
- package/src/auth-turn/fixtures/invalid/unknown-top-level-field.json +5 -0
- package/src/auth-turn/fixtures/valid/abort.json +8 -0
- package/src/auth-turn/fixtures/valid/challenge.json +17 -0
- package/src/auth-turn/fixtures/valid/complete.json +13 -0
- package/src/auth-turn/fixtures/valid/form.json +14 -0
- package/src/auth-turn/fixtures/valid/message.json +13 -0
- package/src/auth-turn/fixtures/valid/multi_choice.json +15 -0
- package/src/auth-turn/fixtures/valid/pending.json +5 -0
- package/src/auth-turn/fixtures/valid/poll.json +9 -0
- package/src/auth-turn/fixtures/valid/redirect.json +16 -0
- package/src/auth-turn/fixtures/valid/retry.json +8 -0
- package/src/auth-turn/fixtures/valid/unknown-kind.json +7 -0
- package/src/auth-turn/index.ts +177 -0
- package/src/auth.ts +786 -0
- package/src/ceremonies/index.ts +9 -43
- package/src/cli/create.ts +60 -97
- package/src/cli/templates/provider/AGENTS.md.tpl +87 -0
- package/src/cli/templates/provider/CLAUDE.md.tpl +1 -0
- package/src/cli/templates/provider/README.md.tpl +7 -4
- package/src/cli/templates/provider/skills/fixtures-and-recording/SKILL.md.tpl +58 -0
- package/src/cli/templates/provider/skills/health-checks-and-fail-closed/SKILL.md.tpl +65 -0
- package/src/cli/templates/provider/skills/normalization-standards/SKILL.md.tpl +57 -0
- package/src/cli/templates/provider/skills/pagination-and-counts/SKILL.md.tpl +52 -0
- package/src/cli/templates/provider/skills/upstream-contract-verification/SKILL.md.tpl +45 -0
- package/src/cli/templates/provider/skills/upstream-notes/README.md.tpl +13 -0
- package/src/contract.ts +1 -0
- package/src/define.ts +198 -71
- package/src/index.ts +16 -0
- package/src/lint.ts +33 -0
- package/src/provider.ts +27 -0
- package/src/runtime/auth-flow.ts +2 -0
- package/src/runtime/browser.ts +293 -1
- package/src/runtime/http.ts +48 -7
- package/src/runtime/stealth.ts +182 -5
- package/src/server/index.ts +36 -0
- package/src/server/self-test-input-tokens.ts +46 -0
- package/src/server/self-test-redaction.ts +97 -0
- package/src/server/self-test-token.ts +70 -0
- package/src/server/self-test.ts +725 -0
- package/src/server/serve.ts +67 -4
- package/src/testing/run.ts +9 -1
- package/src/types.ts +186 -0
package/src/index.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// @apifuse/provider-sdk
|
|
2
2
|
|
|
3
|
+
export * from "./auth";
|
|
3
4
|
export * from "./ceremonies";
|
|
4
5
|
export * from "./choice-token";
|
|
5
6
|
export type {
|
|
@@ -20,6 +21,8 @@ export {
|
|
|
20
21
|
type ProviderContractSnapshot,
|
|
21
22
|
} from "./contract";
|
|
22
23
|
export {
|
|
24
|
+
centered,
|
|
25
|
+
delayed,
|
|
23
26
|
defineHealthJourney,
|
|
24
27
|
defineOperation,
|
|
25
28
|
defineProvider,
|
|
@@ -122,8 +125,15 @@ export type {
|
|
|
122
125
|
Bcp47Locale,
|
|
123
126
|
BrowserEngine,
|
|
124
127
|
BrowserOptions,
|
|
128
|
+
BrowserResourceBody,
|
|
129
|
+
BrowserResourceDecision,
|
|
130
|
+
BrowserResourceMethod,
|
|
131
|
+
BrowserResourcePolicy,
|
|
132
|
+
BrowserResourceRequest,
|
|
133
|
+
BrowserResourceRoute,
|
|
125
134
|
ConnectionMode,
|
|
126
135
|
ContextDeclaration,
|
|
136
|
+
CookieJar,
|
|
127
137
|
ContextScratchpad,
|
|
128
138
|
CredentialContext,
|
|
129
139
|
CredentialDeclaration,
|
|
@@ -144,6 +154,7 @@ export type {
|
|
|
144
154
|
HealthJourneyRunContext,
|
|
145
155
|
HealthJourneyRunResult,
|
|
146
156
|
HealthJourneySchedule,
|
|
157
|
+
HealthScheduleRandomization,
|
|
147
158
|
HealthJourneySmsContext,
|
|
148
159
|
HealthJourneyStep,
|
|
149
160
|
HttpClient,
|
|
@@ -191,6 +202,7 @@ export type {
|
|
|
191
202
|
ProviderContext,
|
|
192
203
|
ProviderDefinition,
|
|
193
204
|
ProviderHealthMonitorConfig,
|
|
205
|
+
ProviderHealthProbeConfig,
|
|
194
206
|
ProviderLocale,
|
|
195
207
|
ProviderLocaleKey,
|
|
196
208
|
ProviderLocaleKeyInput,
|
|
@@ -229,8 +241,12 @@ export type {
|
|
|
229
241
|
StealthFetchOptions,
|
|
230
242
|
StealthPlatform,
|
|
231
243
|
StealthProfile,
|
|
244
|
+
StealthRedirectHop,
|
|
245
|
+
StealthRedirectRunOptions,
|
|
246
|
+
StealthRedirectRunResult,
|
|
232
247
|
StealthResponse,
|
|
233
248
|
StealthSession,
|
|
249
|
+
StealthSessionCookies,
|
|
234
250
|
SttAudioInput,
|
|
235
251
|
SttContext,
|
|
236
252
|
SttPromptPolicy,
|
package/src/lint.ts
CHANGED
|
@@ -22,8 +22,12 @@ type ProviderAuthLike = {
|
|
|
22
22
|
abort?: unknown;
|
|
23
23
|
refresh?: unknown;
|
|
24
24
|
};
|
|
25
|
+
exchange?: unknown;
|
|
25
26
|
};
|
|
26
27
|
|
|
28
|
+
const AUTH_OPERATION_ID_PATTERN =
|
|
29
|
+
/^(?:auth[-_])?(?:login|exchange|continue|refresh|callback)(?:[-_]|$)/i;
|
|
30
|
+
|
|
27
31
|
type ProviderContractMetaLike = {
|
|
28
32
|
publicSchemaFieldNames?: "normalized";
|
|
29
33
|
};
|
|
@@ -127,6 +131,10 @@ function lintReviewed(
|
|
|
127
131
|
];
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
function isProviderAuthLike(value: unknown): value is ProviderAuthLike {
|
|
135
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
|
|
130
138
|
function hasReusableSecretKeys(keys: readonly string[] | undefined): boolean {
|
|
131
139
|
if (!keys) {
|
|
132
140
|
return false;
|
|
@@ -215,6 +223,15 @@ function lintAuthModel(provider: {
|
|
|
215
223
|
});
|
|
216
224
|
}
|
|
217
225
|
|
|
226
|
+
if (isProviderAuthLike(provider.auth) && "exchange" in provider.auth) {
|
|
227
|
+
diagnostics.push({
|
|
228
|
+
rule: "auth-exchange-unsupported",
|
|
229
|
+
level: "error",
|
|
230
|
+
field: "auth.exchange",
|
|
231
|
+
message: `${providerLabel} must not define auth.exchange. The Provider SDK has one auth interface: auth.flow. Gateway only calls auth.flow.start/continue/poll/abort/refresh and persists complete turn data.credential as-is; put login/token/session exchange inside auth.flow.continue.`,
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
|
|
218
235
|
if (authMode === "credentials" && credentialKeys.length === 0) {
|
|
219
236
|
diagnostics.push({
|
|
220
237
|
rule: "credential-keys-required-when-credentials-mode",
|
|
@@ -1006,6 +1023,22 @@ export function lintProvider(
|
|
|
1006
1023
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1007
1024
|
];
|
|
1008
1025
|
|
|
1026
|
+
if (provider.operations) {
|
|
1027
|
+
const authMode = provider.auth?.mode;
|
|
1028
|
+
if (authMode === "credentials" || authMode === "oauth2") {
|
|
1029
|
+
for (const operationKey of Object.keys(provider.operations)) {
|
|
1030
|
+
if (AUTH_OPERATION_ID_PATTERN.test(operationKey)) {
|
|
1031
|
+
diagnostics.push({
|
|
1032
|
+
rule: "auth-operation-unsupported",
|
|
1033
|
+
level: "error",
|
|
1034
|
+
field: `operations.${operationKey}`,
|
|
1035
|
+
message: `Provider "${provider.id ?? "unknown"}" operation "${operationKey}" looks like a login/token/session exchange endpoint. Authenticated providers must expose login through the single auth.flow interface because Gateway persists only auth.flow complete turn data.credential as the connection credential. Move this logic into auth.flow.continue instead of a provider operation.`,
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
1039
|
+
}
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1009
1042
|
if (!provider.operations) {
|
|
1010
1043
|
return diagnostics;
|
|
1011
1044
|
}
|
package/src/provider.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
export {
|
|
2
|
+
AuthAbortError,
|
|
3
|
+
credentialsAuthChallenge,
|
|
4
|
+
createAuthFlowHelpers,
|
|
5
|
+
defineCredentialsAuth,
|
|
6
|
+
} from "./auth";
|
|
7
|
+
export type {
|
|
8
|
+
CredentialsAuthChallengeDefinition,
|
|
9
|
+
CredentialsAuthChallengeRequest,
|
|
10
|
+
CredentialsAuthCompleteResult,
|
|
11
|
+
CredentialsAuthCredential,
|
|
12
|
+
CredentialsAuthField,
|
|
13
|
+
CredentialsAuthFields,
|
|
14
|
+
CredentialsAuthFieldType,
|
|
15
|
+
CredentialsAuthInput,
|
|
16
|
+
CredentialsAuthLoginResult,
|
|
17
|
+
DefineCredentialsAuthOptions,
|
|
18
|
+
DefinedCredentialsAuth,
|
|
19
|
+
} from "./auth";
|
|
1
20
|
export { createFormCeremony } from "./ceremonies";
|
|
2
21
|
export {
|
|
3
22
|
assertFreshProviderChoiceIssuedAt,
|
|
@@ -8,6 +27,8 @@ export {
|
|
|
8
27
|
parseProviderChoiceToken,
|
|
9
28
|
} from "./choice-token";
|
|
10
29
|
export {
|
|
30
|
+
centered,
|
|
31
|
+
delayed,
|
|
11
32
|
defineHealthJourney,
|
|
12
33
|
defineOperation,
|
|
13
34
|
defineProvider,
|
|
@@ -50,7 +71,12 @@ export {
|
|
|
50
71
|
z,
|
|
51
72
|
} from "./schema";
|
|
52
73
|
export type {
|
|
74
|
+
AuthAbortData,
|
|
75
|
+
AuthAbortRetry,
|
|
76
|
+
AuthFlowTerminalContext,
|
|
53
77
|
AuthMode,
|
|
78
|
+
AuthSafeData,
|
|
79
|
+
AuthSafeJson,
|
|
54
80
|
FlowContext,
|
|
55
81
|
HealthCheckAssertionContext,
|
|
56
82
|
HealthCheckCase,
|
|
@@ -61,6 +87,7 @@ export type {
|
|
|
61
87
|
HealthJourneyManualTriggerPolicy,
|
|
62
88
|
HealthJourneyRunContext,
|
|
63
89
|
HealthJourneyRunResult,
|
|
90
|
+
HealthScheduleRandomization,
|
|
64
91
|
HttpRetryOptions,
|
|
65
92
|
HttpRetrySummary,
|
|
66
93
|
InferSchemaOutput,
|
package/src/runtime/auth-flow.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { ContextAccessError } from "../errors";
|
|
2
|
+
import { createAuthFlowHelpers } from "../auth";
|
|
2
3
|
import type {
|
|
3
4
|
ContextScratchpad,
|
|
4
5
|
EnvContext,
|
|
@@ -70,5 +71,6 @@ export function createFlowContext(options: {
|
|
|
70
71
|
env: options.env,
|
|
71
72
|
context: createScratchpad(options.allowedKeys, options.initialContext),
|
|
72
73
|
stt: options.stt ?? createUnsupportedSttClient(),
|
|
74
|
+
auth: createAuthFlowHelpers(),
|
|
73
75
|
};
|
|
74
76
|
}
|
package/src/runtime/browser.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { createRequire } from "node:module";
|
|
2
|
-
import type { Frame, LaunchOptions, Locator, Page } from "playwright";
|
|
2
|
+
import type { Frame, LaunchOptions, Locator, Page, Request, Route } from "playwright";
|
|
3
3
|
|
|
4
4
|
import { ProviderError } from "../errors";
|
|
5
5
|
import type {
|
|
@@ -11,11 +11,18 @@ import type {
|
|
|
11
11
|
BrowserLocator,
|
|
12
12
|
BrowserOptions,
|
|
13
13
|
BrowserPage,
|
|
14
|
+
BrowserResourceBody,
|
|
15
|
+
BrowserResourceDecision,
|
|
16
|
+
BrowserResourceMethod,
|
|
17
|
+
BrowserResourcePolicy,
|
|
18
|
+
BrowserResourceRequest,
|
|
14
19
|
} from "../types";
|
|
15
20
|
|
|
16
21
|
const require = createRequire(import.meta.url);
|
|
17
22
|
const DEFAULT_WAIT_TIMEOUT_MS = 30_000;
|
|
18
23
|
const SELECTOR_POLL_INTERVAL_MS = 100;
|
|
24
|
+
const RESOURCE_POLICY_ROUTE_PATTERN = "**/*";
|
|
25
|
+
const DEFAULT_RESOURCE_METHODS = ["GET", "HEAD"] as const;
|
|
19
26
|
|
|
20
27
|
type PlaywrightModule = typeof import("playwright");
|
|
21
28
|
type PlaywrightExtraModule = {
|
|
@@ -62,8 +69,153 @@ type CdpFrameTreeNode = {
|
|
|
62
69
|
};
|
|
63
70
|
};
|
|
64
71
|
|
|
72
|
+
type CdpFetchFulfillParams = {
|
|
73
|
+
readonly requestId: string;
|
|
74
|
+
readonly responseCode: number;
|
|
75
|
+
readonly responseHeaders?: readonly {
|
|
76
|
+
readonly name: string;
|
|
77
|
+
readonly value: string;
|
|
78
|
+
}[];
|
|
79
|
+
readonly body?: string;
|
|
80
|
+
};
|
|
81
|
+
|
|
65
82
|
type BrowserPageContract = BrowserPage;
|
|
66
83
|
|
|
84
|
+
function toResourceBody(
|
|
85
|
+
body: BrowserResourceBody | undefined,
|
|
86
|
+
): Buffer | string | undefined {
|
|
87
|
+
if (body === undefined || typeof body === "string" || Buffer.isBuffer(body)) {
|
|
88
|
+
return body;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
if (body instanceof ArrayBuffer) {
|
|
92
|
+
return Buffer.from(new Uint8Array(body));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
return Buffer.from(body);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function isResourceMethod(method: string): method is BrowserResourceMethod {
|
|
99
|
+
return method === "GET" || method === "HEAD";
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function toResourceRequest(
|
|
103
|
+
request: Request,
|
|
104
|
+
): Promise<BrowserResourceRequest | null> {
|
|
105
|
+
const method = request.method().toUpperCase();
|
|
106
|
+
if (!isResourceMethod(method)) {
|
|
107
|
+
return null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return {
|
|
111
|
+
headers: await request.allHeaders(),
|
|
112
|
+
method,
|
|
113
|
+
resourceType: request.resourceType(),
|
|
114
|
+
url: request.url(),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function toCdpResourceRequest(
|
|
119
|
+
params: unknown,
|
|
120
|
+
): { requestId: string; request: BrowserResourceRequest } | null {
|
|
121
|
+
if (!isRecord(params)) {
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const requestId = params.requestId;
|
|
126
|
+
const rawRequest = params.request;
|
|
127
|
+
if (typeof requestId !== "string" || !isRecord(rawRequest)) {
|
|
128
|
+
return null;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const url = rawRequest.url;
|
|
132
|
+
const method = String(rawRequest.method ?? "").toUpperCase();
|
|
133
|
+
if (typeof url !== "string" || !isResourceMethod(method)) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
requestId,
|
|
139
|
+
request: {
|
|
140
|
+
headers: toCdpResourceHeaders(rawRequest.headers),
|
|
141
|
+
method,
|
|
142
|
+
resourceType:
|
|
143
|
+
typeof params.resourceType === "string" ? params.resourceType : undefined,
|
|
144
|
+
url,
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function getCdpPausedRequestId(params: unknown): string | null {
|
|
150
|
+
if (!isRecord(params) || typeof params.requestId !== "string") {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return params.requestId;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function toCdpResourceHeaders(value: unknown): Record<string, string> {
|
|
158
|
+
if (!isRecord(value)) {
|
|
159
|
+
return {};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const headers: Record<string, string> = {};
|
|
163
|
+
for (const [name, headerValue] of Object.entries(value)) {
|
|
164
|
+
if (typeof headerValue === "string") {
|
|
165
|
+
headers[name] = headerValue;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return headers;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function matchesResourceRoute(
|
|
173
|
+
match: BrowserResourcePolicy["routes"][number]["match"],
|
|
174
|
+
request: BrowserResourceRequest,
|
|
175
|
+
): boolean {
|
|
176
|
+
if (typeof match === "string") {
|
|
177
|
+
return request.url === match;
|
|
178
|
+
}
|
|
179
|
+
if (match instanceof RegExp) {
|
|
180
|
+
return match.test(request.url);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return match(request);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function toCdpFulfillParams(
|
|
187
|
+
requestId: string,
|
|
188
|
+
decision: Extract<BrowserResourceDecision, { readonly action: "fulfill" }>,
|
|
189
|
+
): CdpFetchFulfillParams {
|
|
190
|
+
const body = toResourceBody(decision.body);
|
|
191
|
+
return {
|
|
192
|
+
...(body === undefined
|
|
193
|
+
? {}
|
|
194
|
+
: { body: Buffer.from(body).toString("base64") }),
|
|
195
|
+
...(decision.headers === undefined
|
|
196
|
+
? {}
|
|
197
|
+
: {
|
|
198
|
+
responseHeaders: Object.entries(decision.headers).map(
|
|
199
|
+
([name, value]) => ({ name, value }),
|
|
200
|
+
),
|
|
201
|
+
}),
|
|
202
|
+
requestId,
|
|
203
|
+
responseCode: decision.status ?? 200,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
async function fulfillResourceRoute(
|
|
208
|
+
route: Route,
|
|
209
|
+
decision: Extract<BrowserResourceDecision, { readonly action: "fulfill" }>,
|
|
210
|
+
): Promise<void> {
|
|
211
|
+
const body = toResourceBody(decision.body);
|
|
212
|
+
await route.fulfill({
|
|
213
|
+
...(body === undefined ? {} : { body }),
|
|
214
|
+
...(decision.headers === undefined ? {} : { headers: decision.headers }),
|
|
215
|
+
status: decision.status ?? 200,
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
|
|
67
219
|
export type BrowserClientOptions = BrowserOptions & {
|
|
68
220
|
allowedHosts?: string[];
|
|
69
221
|
cdpUrl?: string;
|
|
@@ -398,6 +550,47 @@ class PlaywrightBrowserPage implements BrowserPageContract {
|
|
|
398
550
|
async close(): Promise<void> {
|
|
399
551
|
await this.page.close();
|
|
400
552
|
}
|
|
553
|
+
|
|
554
|
+
async withResourcePolicy<T>(
|
|
555
|
+
policy: BrowserResourcePolicy,
|
|
556
|
+
run: () => Promise<T>,
|
|
557
|
+
): Promise<T> {
|
|
558
|
+
const allowedMethods = new Set(
|
|
559
|
+
policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS,
|
|
560
|
+
);
|
|
561
|
+
const handler = async (route: Route): Promise<void> => {
|
|
562
|
+
const request = await toResourceRequest(route.request());
|
|
563
|
+
if (!request || !allowedMethods.has(request.method)) {
|
|
564
|
+
await route.abort("blockedbyclient");
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
for (const resourceRoute of policy.routes) {
|
|
569
|
+
if (!matchesResourceRoute(resourceRoute.match, request)) {
|
|
570
|
+
continue;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
const decision = await resourceRoute.handle(request);
|
|
574
|
+
switch (decision.action) {
|
|
575
|
+
case "fulfill":
|
|
576
|
+
await fulfillResourceRoute(route, decision);
|
|
577
|
+
return;
|
|
578
|
+
case "block":
|
|
579
|
+
await route.abort("blockedbyclient");
|
|
580
|
+
return;
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
await route.abort("blockedbyclient");
|
|
585
|
+
};
|
|
586
|
+
|
|
587
|
+
await this.page.route(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
588
|
+
try {
|
|
589
|
+
return await run();
|
|
590
|
+
} finally {
|
|
591
|
+
await this.page.unroute(RESOURCE_POLICY_ROUTE_PATTERN, handler);
|
|
592
|
+
}
|
|
593
|
+
}
|
|
401
594
|
}
|
|
402
595
|
|
|
403
596
|
class PlaywrightBrowserClient implements SupportedBrowserClient {
|
|
@@ -1055,6 +1248,105 @@ class CdpPoolBrowserPage implements BrowserPageContract {
|
|
|
1055
1248
|
}
|
|
1056
1249
|
}
|
|
1057
1250
|
|
|
1251
|
+
async withResourcePolicy<T>(
|
|
1252
|
+
policy: BrowserResourcePolicy,
|
|
1253
|
+
run: () => Promise<T>,
|
|
1254
|
+
): Promise<T> {
|
|
1255
|
+
const allowedMethods = new Set(
|
|
1256
|
+
policy.allowedMethods ?? DEFAULT_RESOURCE_METHODS,
|
|
1257
|
+
);
|
|
1258
|
+
const handlePausedRequest = (params: unknown): void => {
|
|
1259
|
+
void this.handleResourcePolicyPausedRequest(
|
|
1260
|
+
params,
|
|
1261
|
+
policy,
|
|
1262
|
+
allowedMethods,
|
|
1263
|
+
);
|
|
1264
|
+
};
|
|
1265
|
+
|
|
1266
|
+
const unsubscribe = this.pageClient.on(
|
|
1267
|
+
"Fetch.requestPaused",
|
|
1268
|
+
handlePausedRequest,
|
|
1269
|
+
);
|
|
1270
|
+
|
|
1271
|
+
try {
|
|
1272
|
+
await this.pageClient.send("Fetch.enable", {
|
|
1273
|
+
patterns: [{ requestStage: "Request", urlPattern: "*" }],
|
|
1274
|
+
});
|
|
1275
|
+
} catch (error) {
|
|
1276
|
+
unsubscribe();
|
|
1277
|
+
throw new ProviderError(
|
|
1278
|
+
"CDP browser target does not support BrowserPage.withResourcePolicy()",
|
|
1279
|
+
{
|
|
1280
|
+
cause: error instanceof Error ? error : undefined,
|
|
1281
|
+
code: "BROWSER_RUNTIME_UNSUPPORTED",
|
|
1282
|
+
fix: "Use a Chromium CDP target with the Fetch domain enabled, or use the local Playwright browser runtime.",
|
|
1283
|
+
},
|
|
1284
|
+
);
|
|
1285
|
+
}
|
|
1286
|
+
|
|
1287
|
+
try {
|
|
1288
|
+
return await run();
|
|
1289
|
+
} finally {
|
|
1290
|
+
unsubscribe();
|
|
1291
|
+
await this.pageClient.send("Fetch.disable");
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
private async handleResourcePolicyPausedRequest(
|
|
1296
|
+
params: unknown,
|
|
1297
|
+
policy: BrowserResourcePolicy,
|
|
1298
|
+
allowedMethods: ReadonlySet<BrowserResourceMethod>,
|
|
1299
|
+
): Promise<void> {
|
|
1300
|
+
const requestId = getCdpPausedRequestId(params);
|
|
1301
|
+
if (requestId === null) {
|
|
1302
|
+
return;
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
try {
|
|
1306
|
+
const parsed = toCdpResourceRequest(params);
|
|
1307
|
+
if (!parsed || !allowedMethods.has(parsed.request.method)) {
|
|
1308
|
+
await this.failCdpResourceRequest(requestId);
|
|
1309
|
+
return;
|
|
1310
|
+
}
|
|
1311
|
+
|
|
1312
|
+
for (const resourceRoute of policy.routes) {
|
|
1313
|
+
if (!matchesResourceRoute(resourceRoute.match, parsed.request)) {
|
|
1314
|
+
continue;
|
|
1315
|
+
}
|
|
1316
|
+
|
|
1317
|
+
const decision = await resourceRoute.handle(parsed.request);
|
|
1318
|
+
switch (decision.action) {
|
|
1319
|
+
case "fulfill":
|
|
1320
|
+
await this.pageClient.send(
|
|
1321
|
+
"Fetch.fulfillRequest",
|
|
1322
|
+
toCdpFulfillParams(parsed.requestId, decision),
|
|
1323
|
+
);
|
|
1324
|
+
return;
|
|
1325
|
+
case "block":
|
|
1326
|
+
await this.failCdpResourceRequest(parsed.requestId);
|
|
1327
|
+
return;
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
await this.failCdpResourceRequest(parsed.requestId);
|
|
1332
|
+
} catch {
|
|
1333
|
+
await this.failCdpResourceRequest(requestId);
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
private async failCdpResourceRequest(requestId: string): Promise<void> {
|
|
1338
|
+
try {
|
|
1339
|
+
await this.pageClient.send("Fetch.failRequest", {
|
|
1340
|
+
errorReason: "BlockedByClient",
|
|
1341
|
+
requestId,
|
|
1342
|
+
});
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (error instanceof Error) {
|
|
1345
|
+
return;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
}
|
|
1349
|
+
|
|
1058
1350
|
private async initialize(): Promise<void> {
|
|
1059
1351
|
if (this.initialized) {
|
|
1060
1352
|
return;
|
package/src/runtime/http.ts
CHANGED
|
@@ -58,8 +58,13 @@ type HttpStatusOutcome = {
|
|
|
58
58
|
status: number;
|
|
59
59
|
headers: Record<string, string>;
|
|
60
60
|
retryable: boolean;
|
|
61
|
+
proxyUsed: boolean;
|
|
61
62
|
};
|
|
62
63
|
|
|
64
|
+
type NativeHttpAttemptOutcome = HttpResponse | HttpStatusOutcome;
|
|
65
|
+
|
|
66
|
+
type NativeHttpAttemptError = TransportError & { proxyUsed?: boolean };
|
|
67
|
+
|
|
63
68
|
function isHttpStatusOutcome(
|
|
64
69
|
outcome: HttpResponse | HttpStatusOutcome,
|
|
65
70
|
): outcome is HttpStatusOutcome {
|
|
@@ -582,7 +587,8 @@ function toHttpTransportError(error: unknown): TransportError {
|
|
|
582
587
|
|
|
583
588
|
async function toNativeHttpResponse(response: Response): Promise<HttpResponse> {
|
|
584
589
|
const headers = Object.fromEntries(response.headers.entries());
|
|
585
|
-
const
|
|
590
|
+
const bodyBytes = new Uint8Array(await response.arrayBuffer());
|
|
591
|
+
const rawText = new TextDecoder().decode(bodyBytes);
|
|
586
592
|
const data = parseHttpData(rawText, headers);
|
|
587
593
|
|
|
588
594
|
return {
|
|
@@ -602,6 +608,12 @@ async function toNativeHttpResponse(response: Response): Promise<HttpResponse> {
|
|
|
602
608
|
ok: response.status >= 200 && response.status < 300,
|
|
603
609
|
status: response.status,
|
|
604
610
|
text: async () => rawText,
|
|
611
|
+
arrayBuffer: async () =>
|
|
612
|
+
bodyBytes.buffer.slice(
|
|
613
|
+
bodyBytes.byteOffset,
|
|
614
|
+
bodyBytes.byteOffset + bodyBytes.byteLength,
|
|
615
|
+
),
|
|
616
|
+
bytes: async () => bodyBytes.slice(0),
|
|
605
617
|
};
|
|
606
618
|
}
|
|
607
619
|
|
|
@@ -678,12 +690,20 @@ async function resolveNativeProxy(
|
|
|
678
690
|
options: RequestOptions,
|
|
679
691
|
clientOptions: HttpClientOptions,
|
|
680
692
|
warn: (message: string) => void,
|
|
693
|
+
proxyAttemptOffset = 0,
|
|
681
694
|
): Promise<string | undefined> {
|
|
695
|
+
const baseProxyAttempt =
|
|
696
|
+
clientOptions.proxyAttempt === undefined ||
|
|
697
|
+
!Number.isFinite(clientOptions.proxyAttempt)
|
|
698
|
+
? 0
|
|
699
|
+
: Math.max(0, Math.floor(clientOptions.proxyAttempt));
|
|
682
700
|
const resolvedProxy = await resolveProxyConfigAsync({
|
|
683
701
|
proxy: options.proxy ?? clientOptions.proxy,
|
|
684
702
|
upstream: clientOptions.upstream,
|
|
685
703
|
apifuseConfig: clientOptions.apifuseConfig,
|
|
704
|
+
proxyPolicy: clientOptions.proxyPolicy,
|
|
686
705
|
affinityKey: clientOptions.affinityKey,
|
|
706
|
+
proxyAttempt: baseProxyAttempt + proxyAttemptOffset,
|
|
687
707
|
telemetry: clientOptions.telemetry,
|
|
688
708
|
});
|
|
689
709
|
if (resolvedProxy.shouldWarn) {
|
|
@@ -723,7 +743,8 @@ async function fetchNativeHttp(
|
|
|
723
743
|
clientOptions: HttpClientOptions,
|
|
724
744
|
warn: (message: string) => void,
|
|
725
745
|
statusRetryCodes?: readonly number[],
|
|
726
|
-
|
|
746
|
+
proxyAttemptOffset = 0,
|
|
747
|
+
): Promise<NativeHttpAttemptOutcome> {
|
|
727
748
|
const requestUrl = appendQueryParams(
|
|
728
749
|
resolveHttpUrl(baseUrl, url),
|
|
729
750
|
options.params,
|
|
@@ -733,8 +754,14 @@ async function fetchNativeHttp(
|
|
|
733
754
|
? setTimeout(() => controller?.abort(), options.timeout)
|
|
734
755
|
: undefined;
|
|
735
756
|
|
|
757
|
+
let proxy: string | undefined;
|
|
736
758
|
try {
|
|
737
|
-
|
|
759
|
+
proxy = await resolveNativeProxy(
|
|
760
|
+
options,
|
|
761
|
+
clientOptions,
|
|
762
|
+
warn,
|
|
763
|
+
proxyAttemptOffset,
|
|
764
|
+
);
|
|
738
765
|
const requestInit: NativeFetchInit = {
|
|
739
766
|
headers: options.headers,
|
|
740
767
|
method,
|
|
@@ -756,6 +783,7 @@ async function fetchNativeHttp(
|
|
|
756
783
|
status: response.status,
|
|
757
784
|
headers,
|
|
758
785
|
retryable: statusRetryCodes.includes(response.status),
|
|
786
|
+
proxyUsed: Boolean(proxy),
|
|
759
787
|
};
|
|
760
788
|
}
|
|
761
789
|
|
|
@@ -775,7 +803,9 @@ async function fetchNativeHttp(
|
|
|
775
803
|
if (error instanceof SyntaxError) {
|
|
776
804
|
throw error;
|
|
777
805
|
}
|
|
778
|
-
|
|
806
|
+
const transportError = toHttpTransportError(error) as NativeHttpAttemptError;
|
|
807
|
+
transportError.proxyUsed = Boolean(proxy);
|
|
808
|
+
throw transportError;
|
|
779
809
|
} finally {
|
|
780
810
|
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
781
811
|
}
|
|
@@ -867,7 +897,12 @@ export function createHttpClient(
|
|
|
867
897
|
options.body,
|
|
868
898
|
);
|
|
869
899
|
const methodName = normalizeHttpMethod(method);
|
|
870
|
-
const
|
|
900
|
+
const explicitRetry = headersOptions.retry !== undefined;
|
|
901
|
+
const retryOptions =
|
|
902
|
+
normalizeRetryOptions(headersOptions.retry) ??
|
|
903
|
+
(explicitRetry
|
|
904
|
+
? undefined
|
|
905
|
+
: createRetryOptions(HttpRetryPreset.TransportTransient));
|
|
871
906
|
if (retryOptions) validateUnsafeRetryMethods(retryOptions);
|
|
872
907
|
const retryEnabled = Boolean(
|
|
873
908
|
retryOptions &&
|
|
@@ -876,6 +911,7 @@ export function createHttpClient(
|
|
|
876
911
|
);
|
|
877
912
|
const statusRetryEnabled = Boolean(
|
|
878
913
|
retryEnabled &&
|
|
914
|
+
explicitRetry &&
|
|
879
915
|
retryOptions &&
|
|
880
916
|
retryOptions.statusCodes.length > 0 &&
|
|
881
917
|
headersOptions.throwOnHttpError !== false,
|
|
@@ -885,7 +921,9 @@ export function createHttpClient(
|
|
|
885
921
|
? { ...headersOptions, throwOnHttpError: false }
|
|
886
922
|
: headersOptions;
|
|
887
923
|
|
|
888
|
-
const executeOnce = (
|
|
924
|
+
const executeOnce = (
|
|
925
|
+
proxyAttemptOffset = 0,
|
|
926
|
+
): Promise<NativeHttpAttemptOutcome> =>
|
|
889
927
|
fetchNativeHttp(
|
|
890
928
|
baseUrl,
|
|
891
929
|
url,
|
|
@@ -894,6 +932,7 @@ export function createHttpClient(
|
|
|
894
932
|
clientOptions,
|
|
895
933
|
warnOnce,
|
|
896
934
|
statusRetryEnabled ? retryOptions?.statusCodes : undefined,
|
|
935
|
+
proxyAttemptOffset,
|
|
897
936
|
);
|
|
898
937
|
|
|
899
938
|
if (!retryEnabled || !retryOptions) {
|
|
@@ -908,7 +947,7 @@ export function createHttpClient(
|
|
|
908
947
|
let lastStatus: number | undefined;
|
|
909
948
|
for (let attempt = 1; attempt <= retryOptions.attempts; attempt += 1) {
|
|
910
949
|
try {
|
|
911
|
-
const outcome = await executeOnce();
|
|
950
|
+
const outcome = await executeOnce(attempt - 1);
|
|
912
951
|
if (isHttpStatusOutcome(outcome)) {
|
|
913
952
|
lastStatus = outcome.status;
|
|
914
953
|
if (outcome.retryable && attempt < retryOptions.attempts) {
|
|
@@ -943,8 +982,10 @@ export function createHttpClient(
|
|
|
943
982
|
} catch (error) {
|
|
944
983
|
lastErrorCode = retryErrorCode(error);
|
|
945
984
|
lastStatus = retryErrorStatus(error);
|
|
985
|
+
const proxyUsed = Boolean((error as NativeHttpAttemptError).proxyUsed);
|
|
946
986
|
if (
|
|
947
987
|
attempt < retryOptions.attempts &&
|
|
988
|
+
(explicitRetry || proxyUsed) &&
|
|
948
989
|
shouldRetryTransportError(error, retryOptions)
|
|
949
990
|
) {
|
|
950
991
|
await sleep(computeRetryDelayMs(retryOptions, attempt));
|