@apifuse/provider-sdk 2.1.0-beta.2 → 2.1.0-beta.21
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 +330 -8
- package/CHANGELOG.md +89 -1
- package/README.md +64 -17
- package/SUBMISSION.md +86 -0
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +58 -8
- package/bin/apifuse-pack-check.ts +32 -2
- package/bin/apifuse-pack-smoke.ts +133 -6
- package/bin/apifuse-perf.ts +142 -49
- package/bin/apifuse-record.ts +182 -104
- package/bin/apifuse-submit-check.ts +3243 -0
- package/bin/apifuse.ts +1 -1
- package/dist/auth.d.ts +76 -0
- package/dist/auth.js +436 -0
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +777 -0
- 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/config/loader.d.ts +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +156 -0
- package/dist/define.d.ts +100 -0
- package/dist/define.js +1383 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +38 -0
- package/dist/lint.d.ts +74 -0
- package/dist/lint.js +729 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +11 -0
- package/dist/provider.js +9 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +46 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1237 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +726 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +21 -0
- package/dist/runtime/stealth.js +980 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1118 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +307 -0
- package/dist/types.d.ts +1467 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +57 -29
- package/src/auth.ts +786 -0
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +165 -0
- package/src/cli/commands.ts +34 -11
- package/src/cli/create.ts +254 -128
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- 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 +87 -7
- package/src/cli/templates/provider/dev.ts.tpl +1 -1
- package/src/cli/templates/provider/domain/README.md.tpl +3 -0
- package/src/cli/templates/provider/index.ts.tpl +5 -47
- package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
- package/src/cli/templates/provider/meta.ts.tpl +7 -0
- package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
- 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/cli/templates/provider/start.ts.tpl +1 -1
- package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
- package/src/config/loader.ts +1224 -9
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +216 -0
- package/src/define.ts +1820 -70
- package/src/errors.ts +27 -0
- package/src/i18n/catalog.ts +277 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +189 -9
- package/src/lint.ts +580 -73
- package/src/observability.ts +41 -0
- package/src/provider.ts +131 -4
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +9 -0
- package/src/runtime/browser.ts +1054 -51
- package/src/runtime/cache.ts +528 -0
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +32 -3
- package/src/runtime/http.ts +980 -195
- package/src/runtime/insights.ts +11 -11
- package/src/runtime/instrumentation.ts +12 -4
- package/src/runtime/key-derivation.ts +1 -1
- package/src/runtime/keyring.ts +4 -3
- package/src/runtime/proxy-errors.ts +132 -0
- package/src/runtime/proxy-telemetry.ts +253 -0
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/request-options.ts +66 -0
- package/src/runtime/state.ts +563 -0
- package/src/runtime/stealth.ts +1336 -0
- package/src/runtime/stt.ts +629 -0
- package/src/runtime/trace.ts +1 -1
- package/src/schema.ts +363 -1
- package/src/server/serve.ts +1192 -75
- package/src/server/types.ts +37 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +40 -6
- package/src/types.ts +1283 -59
- package/src/runtime/tls.ts +0 -434
- package/src/types/playwright-stealth.d.ts +0 -9
package/src/lint.ts
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
import type { ZodType } from "zod";
|
|
2
2
|
|
|
3
|
+
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint";
|
|
4
|
+
import {
|
|
5
|
+
APIFUSE_DESCRIPTION_KEY_META_KEY,
|
|
6
|
+
APIFUSE_SENSITIVE_META_KEY,
|
|
7
|
+
} from "./schema";
|
|
8
|
+
|
|
3
9
|
type AuthModeLike =
|
|
4
10
|
| "none"
|
|
5
11
|
| "platform-managed"
|
|
@@ -14,13 +20,23 @@ type ProviderAuthLike = {
|
|
|
14
20
|
continue?: unknown;
|
|
15
21
|
poll?: unknown;
|
|
16
22
|
abort?: unknown;
|
|
23
|
+
refresh?: unknown;
|
|
17
24
|
};
|
|
25
|
+
exchange?: unknown;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const AUTH_OPERATION_ID_PATTERN =
|
|
29
|
+
/^(?:auth[-_])?(?:login|exchange|continue|refresh|callback)(?:[-_]|$)/i;
|
|
30
|
+
|
|
31
|
+
type ProviderContractMetaLike = {
|
|
32
|
+
publicSchemaFieldNames?: "normalized";
|
|
18
33
|
};
|
|
19
34
|
|
|
20
35
|
type SchemaLike = ZodType & {
|
|
21
36
|
description?: string;
|
|
22
37
|
def?: Record<string, unknown>;
|
|
23
38
|
_def?: Record<string, unknown>;
|
|
39
|
+
meta?: () => Record<string, unknown> | undefined;
|
|
24
40
|
shape?: Record<string, SchemaLike> | (() => Record<string, SchemaLike>);
|
|
25
41
|
element?: SchemaLike;
|
|
26
42
|
items?: SchemaLike[];
|
|
@@ -41,9 +57,21 @@ export interface LintDiagnostic {
|
|
|
41
57
|
field?: string;
|
|
42
58
|
}
|
|
43
59
|
|
|
60
|
+
export type ProviderLintMode = "official" | "standalone";
|
|
61
|
+
|
|
62
|
+
type ProviderLintOptions = {
|
|
63
|
+
mode?: ProviderLintMode;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
type ProviderSourceLike = {
|
|
67
|
+
authFlowSource?: string;
|
|
68
|
+
providerSourceFiles?: Record<string, string>;
|
|
69
|
+
operations?: Record<string, { handler?: unknown; source?: string }>;
|
|
70
|
+
};
|
|
71
|
+
|
|
44
72
|
function lintAllowedHosts(
|
|
45
73
|
providerId: string | undefined,
|
|
46
|
-
allowedHosts: string[] | undefined,
|
|
74
|
+
allowedHosts: readonly string[] | undefined,
|
|
47
75
|
): LintDiagnostic[] {
|
|
48
76
|
const prefix = providerId ? `Provider "${providerId}"` : "Provider";
|
|
49
77
|
|
|
@@ -103,7 +131,11 @@ function lintReviewed(
|
|
|
103
131
|
];
|
|
104
132
|
}
|
|
105
133
|
|
|
106
|
-
function
|
|
134
|
+
function isProviderAuthLike(value: unknown): value is ProviderAuthLike {
|
|
135
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function hasReusableSecretKeys(keys: readonly string[] | undefined): boolean {
|
|
107
139
|
if (!keys) {
|
|
108
140
|
return false;
|
|
109
141
|
}
|
|
@@ -115,6 +147,18 @@ function hasReusableSecretKeys(keys: string[] | undefined): boolean {
|
|
|
115
147
|
);
|
|
116
148
|
}
|
|
117
149
|
|
|
150
|
+
function hasReusableReloginSecretKeys(
|
|
151
|
+
keys: readonly string[] | undefined,
|
|
152
|
+
): boolean {
|
|
153
|
+
if (!keys) {
|
|
154
|
+
return false;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
return keys.some((key) =>
|
|
158
|
+
/(password|passcode|secret|cookie|session)/i.test(key),
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
|
|
118
162
|
function getAuthFlowSource(provider: {
|
|
119
163
|
auth?: ProviderAuthLike;
|
|
120
164
|
authFlowSource?: string;
|
|
@@ -128,6 +172,7 @@ function getAuthFlowSource(provider: {
|
|
|
128
172
|
provider.auth?.flow?.continue,
|
|
129
173
|
provider.auth?.flow?.poll,
|
|
130
174
|
provider.auth?.flow?.abort,
|
|
175
|
+
provider.auth?.flow?.refresh,
|
|
131
176
|
];
|
|
132
177
|
|
|
133
178
|
return parts
|
|
@@ -143,12 +188,12 @@ function lintAuthModel(provider: {
|
|
|
143
188
|
id?: string;
|
|
144
189
|
auth?: ProviderAuthLike;
|
|
145
190
|
credential?: {
|
|
146
|
-
keys?: string[];
|
|
191
|
+
keys?: readonly string[];
|
|
147
192
|
storesReusableSecret?: boolean;
|
|
148
193
|
justification?: string;
|
|
149
194
|
};
|
|
150
195
|
context?: {
|
|
151
|
-
keys?: string[];
|
|
196
|
+
keys?: readonly string[];
|
|
152
197
|
};
|
|
153
198
|
authFlowSource?: string;
|
|
154
199
|
}): LintDiagnostic[] {
|
|
@@ -178,6 +223,15 @@ function lintAuthModel(provider: {
|
|
|
178
223
|
});
|
|
179
224
|
}
|
|
180
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
|
+
|
|
181
235
|
if (authMode === "credentials" && credentialKeys.length === 0) {
|
|
182
236
|
diagnostics.push({
|
|
183
237
|
rule: "credential-keys-required-when-credentials-mode",
|
|
@@ -200,6 +254,20 @@ function lintAuthModel(provider: {
|
|
|
200
254
|
});
|
|
201
255
|
}
|
|
202
256
|
|
|
257
|
+
if (
|
|
258
|
+
typeof provider.auth?.flow?.refresh === "function" &&
|
|
259
|
+
hasReusableReloginSecretKeys(credentialKeys) &&
|
|
260
|
+
(!provider.credential?.storesReusableSecret ||
|
|
261
|
+
!provider.credential.justification)
|
|
262
|
+
) {
|
|
263
|
+
diagnostics.push({
|
|
264
|
+
rule: "auth-refresh-reusable-secret",
|
|
265
|
+
level: "error",
|
|
266
|
+
field: "credential",
|
|
267
|
+
message: `${providerLabel} must set storesReusableSecret and justification when auth.flow.refresh may silently re-login with reusable credential secrets.`,
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
203
271
|
if (authMode === "platform-managed" && credentialKeys.length > 0) {
|
|
204
272
|
diagnostics.push({
|
|
205
273
|
rule: "platform-managed-no-credential-keys",
|
|
@@ -359,7 +427,60 @@ function getChildSchemas(
|
|
|
359
427
|
}));
|
|
360
428
|
}
|
|
361
429
|
|
|
362
|
-
function
|
|
430
|
+
function uniqueFields(fields: string[]): string[] {
|
|
431
|
+
return Array.from(new Set(fields));
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function isSensitiveSchema(schema: unknown): boolean {
|
|
435
|
+
if (!schema || typeof schema !== "object" || !("meta" in schema)) {
|
|
436
|
+
return false;
|
|
437
|
+
}
|
|
438
|
+
const meta = schema.meta;
|
|
439
|
+
if (typeof meta !== "function") return false;
|
|
440
|
+
const metadata = meta.call(schema);
|
|
441
|
+
return (
|
|
442
|
+
!!metadata &&
|
|
443
|
+
typeof metadata === "object" &&
|
|
444
|
+
Reflect.get(metadata, APIFUSE_SENSITIVE_META_KEY) === true
|
|
445
|
+
);
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function getSchemaMetadata(schema: SchemaLike): Record<string, unknown> {
|
|
449
|
+
return schema.meta?.() ?? {};
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
function getSchemaDescriptionKey(schema: SchemaLike): string | undefined {
|
|
453
|
+
const value = Reflect.get(
|
|
454
|
+
getSchemaMetadata(schema),
|
|
455
|
+
APIFUSE_DESCRIPTION_KEY_META_KEY,
|
|
456
|
+
);
|
|
457
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
const SENSITIVE_FIELD_NAMES = new Set([
|
|
461
|
+
"apikey",
|
|
462
|
+
"authorization",
|
|
463
|
+
"cookie",
|
|
464
|
+
"secret",
|
|
465
|
+
"secrets",
|
|
466
|
+
"token",
|
|
467
|
+
"accesstoken",
|
|
468
|
+
"refreshtoken",
|
|
469
|
+
"password",
|
|
470
|
+
"passwd",
|
|
471
|
+
"otp",
|
|
472
|
+
"otpcode",
|
|
473
|
+
"phone",
|
|
474
|
+
"phonenumber",
|
|
475
|
+
"paymenturl",
|
|
476
|
+
]);
|
|
477
|
+
|
|
478
|
+
function isSensitiveFieldName(name: string): boolean {
|
|
479
|
+
const normalized = name.toLowerCase().replace(/[-_\s]/g, "");
|
|
480
|
+
return SENSITIVE_FIELD_NAMES.has(normalized);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
function collectUnmarkedSensitiveFields(
|
|
363
484
|
schema: unknown,
|
|
364
485
|
basePath: string,
|
|
365
486
|
seen = new Set<SchemaLike>(),
|
|
@@ -367,13 +488,80 @@ function collectMissingDescriptions(
|
|
|
367
488
|
if (!isSchema(schema) || seen.has(schema)) {
|
|
368
489
|
return [];
|
|
369
490
|
}
|
|
491
|
+
seen.add(schema);
|
|
492
|
+
const out: string[] = [];
|
|
493
|
+
for (const [key, child] of Object.entries(getObjectShape(schema))) {
|
|
494
|
+
const childPath = basePath ? `${basePath}.${key}` : key;
|
|
495
|
+
if (isSensitiveFieldName(key) && !isSensitiveSchema(child)) {
|
|
496
|
+
out.push(childPath);
|
|
497
|
+
}
|
|
498
|
+
out.push(...collectUnmarkedSensitiveFields(child, childPath, seen));
|
|
499
|
+
}
|
|
500
|
+
for (const child of getChildSchemas(schema)) {
|
|
501
|
+
if (Object.hasOwn(getObjectShape(schema), child.key)) continue;
|
|
502
|
+
const isWrapperNode = [
|
|
503
|
+
"unwrap",
|
|
504
|
+
"innerType",
|
|
505
|
+
"sourceType",
|
|
506
|
+
"schema",
|
|
507
|
+
"type",
|
|
508
|
+
"in",
|
|
509
|
+
"out",
|
|
510
|
+
"option",
|
|
511
|
+
"pipe",
|
|
512
|
+
"payload",
|
|
513
|
+
"item",
|
|
514
|
+
"rest",
|
|
515
|
+
"catchall",
|
|
516
|
+
"keyType",
|
|
517
|
+
"valueType",
|
|
518
|
+
].includes(child.key);
|
|
519
|
+
const childPath =
|
|
520
|
+
child.key === "element" || child.key.startsWith("element.")
|
|
521
|
+
? `${basePath}[]`
|
|
522
|
+
: isWrapperNode || child.key.startsWith("pipe.")
|
|
523
|
+
? basePath
|
|
524
|
+
: basePath
|
|
525
|
+
? `${basePath}.${child.key}`
|
|
526
|
+
: child.key;
|
|
527
|
+
out.push(...collectUnmarkedSensitiveFields(child.schema, childPath, seen));
|
|
528
|
+
}
|
|
529
|
+
return out;
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
function collectSchemaDescriptionKeyDiagnostics(
|
|
533
|
+
schema: unknown,
|
|
534
|
+
basePath: string,
|
|
535
|
+
seen = new Set<SchemaLike>(),
|
|
536
|
+
requireCurrentDescription = true,
|
|
537
|
+
): LintDiagnostic[] {
|
|
538
|
+
if (!isSchema(schema) || seen.has(schema)) {
|
|
539
|
+
return [];
|
|
540
|
+
}
|
|
370
541
|
|
|
371
542
|
seen.add(schema);
|
|
372
|
-
const
|
|
543
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
373
544
|
const currentPath = basePath || "schema";
|
|
545
|
+
const hasDescriptionKey = getSchemaDescriptionKey(schema) !== undefined;
|
|
546
|
+
|
|
547
|
+
if (schema.description && !hasDescriptionKey) {
|
|
548
|
+
diagnostics.push({
|
|
549
|
+
rule: "schema-description-raw-prose",
|
|
550
|
+
level: "error",
|
|
551
|
+
field: currentPath,
|
|
552
|
+
message: `Schema field "${currentPath}" must use .describeKey() or describeKey() instead of raw static prose.`,
|
|
553
|
+
});
|
|
554
|
+
}
|
|
374
555
|
|
|
375
|
-
if (!
|
|
376
|
-
|
|
556
|
+
if (requireCurrentDescription && !hasDescriptionKey) {
|
|
557
|
+
diagnostics.push({
|
|
558
|
+
rule: "schema-description-key-required",
|
|
559
|
+
level: "error",
|
|
560
|
+
field: currentPath,
|
|
561
|
+
message: schema.description
|
|
562
|
+
? `Schema field "${currentPath}" has a raw description but is missing .describeKey() or describeKey() metadata.`
|
|
563
|
+
: `Schema field "${currentPath}" is missing .describeKey() or describeKey() metadata.`,
|
|
564
|
+
});
|
|
377
565
|
}
|
|
378
566
|
|
|
379
567
|
for (const child of getChildSchemas(schema)) {
|
|
@@ -383,30 +571,42 @@ function collectMissingDescriptions(
|
|
|
383
571
|
"sourceType",
|
|
384
572
|
"schema",
|
|
385
573
|
"type",
|
|
574
|
+
"in",
|
|
575
|
+
"out",
|
|
386
576
|
"option",
|
|
387
577
|
"pipe",
|
|
388
578
|
"payload",
|
|
389
579
|
"item",
|
|
390
580
|
"rest",
|
|
391
581
|
"catchall",
|
|
582
|
+
"keyType",
|
|
583
|
+
"valueType",
|
|
392
584
|
].includes(child.key);
|
|
585
|
+
const isStructuralNode =
|
|
586
|
+
isWrapperNode ||
|
|
587
|
+
child.key.startsWith("pipe.") ||
|
|
588
|
+
child.key === "element" ||
|
|
589
|
+
child.key.startsWith("element.");
|
|
393
590
|
const childPath = isWrapperNode
|
|
394
591
|
? currentPath
|
|
395
592
|
: currentPath === "schema"
|
|
396
593
|
? child.key
|
|
397
594
|
: /^\d+$/.test(child.key)
|
|
398
595
|
? `${currentPath}[${child.key}]`
|
|
399
|
-
: child.key.startsWith("element")
|
|
596
|
+
: child.key === "element" || child.key.startsWith("element.")
|
|
400
597
|
? `${currentPath}[]`
|
|
401
598
|
: `${currentPath}.${child.key}`;
|
|
402
|
-
|
|
599
|
+
diagnostics.push(
|
|
600
|
+
...collectSchemaDescriptionKeyDiagnostics(
|
|
601
|
+
child.schema,
|
|
602
|
+
childPath,
|
|
603
|
+
seen,
|
|
604
|
+
!isStructuralNode,
|
|
605
|
+
),
|
|
606
|
+
);
|
|
403
607
|
}
|
|
404
608
|
|
|
405
|
-
return
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function uniqueFields(fields: string[]): string[] {
|
|
409
|
-
return Array.from(new Set(fields));
|
|
609
|
+
return diagnostics;
|
|
410
610
|
}
|
|
411
611
|
|
|
412
612
|
function isComplexSchema(
|
|
@@ -438,60 +638,296 @@ function hasBidirectionalFixtures(fixtures: unknown): boolean {
|
|
|
438
638
|
return "request" in fixtures && "response" in fixtures;
|
|
439
639
|
}
|
|
440
640
|
|
|
641
|
+
function getOperationSource(operation: {
|
|
642
|
+
handler?: unknown;
|
|
643
|
+
source?: string;
|
|
644
|
+
}): string {
|
|
645
|
+
if (operation.source) {
|
|
646
|
+
return operation.source;
|
|
647
|
+
}
|
|
648
|
+
return typeof operation.handler === "function"
|
|
649
|
+
? operation.handler.toString()
|
|
650
|
+
: "";
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
function lintStealthTransportUsage(provider: {
|
|
654
|
+
id?: string;
|
|
655
|
+
stealth?: unknown;
|
|
656
|
+
operations?: Record<string, { handler?: unknown; source?: string }>;
|
|
657
|
+
}): LintDiagnostic[] {
|
|
658
|
+
if (provider.stealth || !provider.operations) {
|
|
659
|
+
return [];
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const providerLabel = provider.id ? `Provider "${provider.id}"` : "Provider";
|
|
663
|
+
return Object.entries(provider.operations).flatMap(
|
|
664
|
+
([operationKey, operation]) => {
|
|
665
|
+
const source = getOperationSource(operation);
|
|
666
|
+
if (!/\bctx\.stealth\b/.test(source)) {
|
|
667
|
+
return [];
|
|
668
|
+
}
|
|
669
|
+
return [
|
|
670
|
+
{
|
|
671
|
+
rule: "stealth-config-required",
|
|
672
|
+
level: "error" as const,
|
|
673
|
+
field: `operations.${operationKey}`,
|
|
674
|
+
message: `${providerLabel} operation "${operationKey}" uses ctx.stealth but provider.stealth is not declared.`,
|
|
675
|
+
},
|
|
676
|
+
];
|
|
677
|
+
},
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
function lintCredentialWriteUsage(provider: {
|
|
682
|
+
operations?: Record<string, { handler?: unknown; source?: string }>;
|
|
683
|
+
}): LintDiagnostic[] {
|
|
684
|
+
if (!provider.operations) {
|
|
685
|
+
return [];
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
return Object.entries(provider.operations).flatMap(
|
|
689
|
+
([operationKey, operation]) => {
|
|
690
|
+
const source = getOperationSource(operation);
|
|
691
|
+
if (!/\bctx\.credential\.(?:set|setMany)\s*\(/.test(source)) {
|
|
692
|
+
return [];
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
return [
|
|
696
|
+
{
|
|
697
|
+
rule: "ctx-credential-write-forbidden-in-handler",
|
|
698
|
+
level: "error" as const,
|
|
699
|
+
field: `operations.${operationKey}.handler`,
|
|
700
|
+
message:
|
|
701
|
+
"Operation handlers must not mutate credentials; return refreshed credentials from auth.flow.refresh instead.",
|
|
702
|
+
},
|
|
703
|
+
];
|
|
704
|
+
},
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function lintPlaywrightDirectImports(provider: {
|
|
709
|
+
authFlowSource?: string;
|
|
710
|
+
providerSourceFiles?: Record<string, string>;
|
|
711
|
+
operations?: Record<string, { handler?: unknown; source?: string }>;
|
|
712
|
+
}): LintDiagnostic[] {
|
|
713
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
714
|
+
const importPattern =
|
|
715
|
+
/(?:import\s+(?:type\s+)?[\s\S]*?\s+from\s+["'](?:playwright|playwright-core)["']|require\(\s*["'](?:playwright|playwright-core)["']\s*\)|import\(\s*["'](?:playwright|playwright-core)["']\s*\))/;
|
|
716
|
+
|
|
717
|
+
if (provider.authFlowSource && importPattern.test(provider.authFlowSource)) {
|
|
718
|
+
diagnostics.push({
|
|
719
|
+
rule: "playwright-direct-import",
|
|
720
|
+
level: "warn",
|
|
721
|
+
field: "auth.flow",
|
|
722
|
+
message:
|
|
723
|
+
"Provider auth flow imports playwright directly; use ctx.browser frame-aware methods so the SDK can enforce the CDP pool runtime.",
|
|
724
|
+
});
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
for (const [filePath, source] of Object.entries(
|
|
728
|
+
provider.providerSourceFiles ?? {},
|
|
729
|
+
)) {
|
|
730
|
+
if (!importPattern.test(source)) {
|
|
731
|
+
continue;
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
diagnostics.push({
|
|
735
|
+
rule: "playwright-direct-import",
|
|
736
|
+
level: "warn",
|
|
737
|
+
field: `sourceFiles.${filePath}`,
|
|
738
|
+
message:
|
|
739
|
+
"Provider source imports playwright directly; use ctx.browser frame-aware methods so the SDK can enforce the CDP pool runtime.",
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
if (!provider.operations) {
|
|
744
|
+
return diagnostics;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
for (const [operationKey, operation] of Object.entries(provider.operations)) {
|
|
748
|
+
const source = getOperationSource(operation);
|
|
749
|
+
if (!importPattern.test(source)) {
|
|
750
|
+
continue;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
diagnostics.push({
|
|
754
|
+
rule: "playwright-direct-import",
|
|
755
|
+
level: "warn",
|
|
756
|
+
field: `operations.${operationKey}.handler`,
|
|
757
|
+
message:
|
|
758
|
+
"Operation source imports playwright directly; use ctx.browser frame-aware methods so the SDK can enforce the CDP pool runtime.",
|
|
759
|
+
});
|
|
760
|
+
}
|
|
761
|
+
|
|
762
|
+
return diagnostics;
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
type SelfHostedBrowserPattern = {
|
|
766
|
+
rule: string;
|
|
767
|
+
pattern: RegExp;
|
|
768
|
+
message: string;
|
|
769
|
+
};
|
|
770
|
+
|
|
771
|
+
const SELF_HOSTED_BROWSER_MESSAGE =
|
|
772
|
+
"Official browser providers must use ctx.browser backed by the managed CDP Pool; do not launch or connect to provider-local Chrome/CDP runtimes.";
|
|
773
|
+
|
|
774
|
+
const SELF_HOSTED_BROWSER_PATTERNS: readonly SelfHostedBrowserPattern[] = [
|
|
775
|
+
{
|
|
776
|
+
rule: "browser-self-hosted-launch",
|
|
777
|
+
pattern: /\b(?:playwright|chromium|firefox|webkit|puppeteer)\.launch\s*\(/,
|
|
778
|
+
message: `${SELF_HOSTED_BROWSER_MESSAGE} Replace direct Playwright/Puppeteer launch calls with ctx.browser.newPage() or ctx.browser.withIsolatedContext().`,
|
|
779
|
+
},
|
|
780
|
+
{
|
|
781
|
+
rule: "browser-self-hosted-child-process",
|
|
782
|
+
pattern:
|
|
783
|
+
/(?:\b(?:spawn|spawnSync|exec|execSync|execFile|execFileSync)\s*\([\s\S]{0,240}\b(?:google-chrome|chrome|chromium|chromium-browser)\b|\b(?:Bun\.)?spawn(?:Sync)?\s*\([\s\S]{0,240}\b(?:google-chrome|chrome|chromium|chromium-browser)\b|\$`[\s\S]{0,240}\b(?:google-chrome|chrome|chromium|chromium-browser)\b)/,
|
|
784
|
+
message: `${SELF_HOSTED_BROWSER_MESSAGE} Provider pods must not start Chrome with child_process, Bun.spawn, or shell commands.`,
|
|
785
|
+
},
|
|
786
|
+
{
|
|
787
|
+
rule: "browser-self-hosted-remote-debugging-port",
|
|
788
|
+
pattern:
|
|
789
|
+
/(?:\b(?:google-chrome|chrome|chromium|chromium-browser)\b[\s\S]{0,240}--remote-debugging-port\b|--remote-debugging-port(?:=|\s+))/,
|
|
790
|
+
message: `${SELF_HOSTED_BROWSER_MESSAGE} Provider entrypoints, Dockerfiles, and scripts must not start Chrome with a remote debugging port; use the managed CDP Pool instead.`,
|
|
791
|
+
},
|
|
792
|
+
{
|
|
793
|
+
rule: "browser-direct-cdp-version-poll",
|
|
794
|
+
pattern: /\/json\/version\b/,
|
|
795
|
+
message: `${SELF_HOSTED_BROWSER_MESSAGE} Do not poll /json/version from provider code; the SDK manages CDP leases through APIFUSE__CDP_POOL__URL.`,
|
|
796
|
+
},
|
|
797
|
+
{
|
|
798
|
+
rule: "browser-provider-local-cdp-env",
|
|
799
|
+
pattern:
|
|
800
|
+
/\b(?!APIFUSE__CDP_POOL__URL\b)[A-Z][A-Z0-9_]*_CDP_URL\b|process\.env(?:\.(?!APIFUSE__CDP_POOL__URL\b)[A-Z0-9_]*_CDP_URL\b|\[\s*["'`](?!APIFUSE__CDP_POOL__URL\b)[A-Z0-9_]*_CDP_URL["'`]\s*\])/,
|
|
801
|
+
message: `${SELF_HOSTED_BROWSER_MESSAGE} Do not read provider-local CDP endpoint env vars including AMAZON_CDP_URL or custom *_CDP_URL names; production uses APIFUSE__CDP_POOL__URL through ctx.browser.`,
|
|
802
|
+
},
|
|
803
|
+
];
|
|
804
|
+
|
|
805
|
+
function lintSelfHostedBrowserPatterns(
|
|
806
|
+
provider: ProviderSourceLike,
|
|
807
|
+
options: ProviderLintOptions,
|
|
808
|
+
): LintDiagnostic[] {
|
|
809
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
810
|
+
const level = options.mode === "standalone" ? "warn" : "error";
|
|
811
|
+
const sources: Array<{ field: string; source: string }> = [];
|
|
812
|
+
|
|
813
|
+
if (provider.authFlowSource) {
|
|
814
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
for (const [filePath, source] of Object.entries(
|
|
818
|
+
provider.providerSourceFiles ?? {},
|
|
819
|
+
)) {
|
|
820
|
+
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
821
|
+
}
|
|
822
|
+
|
|
823
|
+
for (const [operationKey, operation] of Object.entries(
|
|
824
|
+
provider.operations ?? {},
|
|
825
|
+
)) {
|
|
826
|
+
const source = getOperationSource(operation);
|
|
827
|
+
if (source) {
|
|
828
|
+
sources.push({
|
|
829
|
+
field: `operations.${operationKey}.handler`,
|
|
830
|
+
source,
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
for (const { field, source } of sources) {
|
|
836
|
+
for (const item of SELF_HOSTED_BROWSER_PATTERNS) {
|
|
837
|
+
item.pattern.lastIndex = 0;
|
|
838
|
+
if (!item.pattern.test(source)) {
|
|
839
|
+
continue;
|
|
840
|
+
}
|
|
841
|
+
diagnostics.push({
|
|
842
|
+
rule: item.rule,
|
|
843
|
+
level,
|
|
844
|
+
field,
|
|
845
|
+
message: item.message,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
return diagnostics;
|
|
851
|
+
}
|
|
852
|
+
|
|
441
853
|
export function lintOperation(op: {
|
|
442
|
-
description
|
|
854
|
+
description?: string;
|
|
855
|
+
descriptionKey?: string;
|
|
856
|
+
whenToUse?: readonly string[];
|
|
857
|
+
whenToUseKeys?: readonly string[];
|
|
858
|
+
whenNotToUse?: readonly string[];
|
|
859
|
+
whenNotToUseKeys?: readonly string[];
|
|
443
860
|
input: unknown;
|
|
444
861
|
output: unknown;
|
|
445
862
|
fixtures?: unknown;
|
|
446
|
-
inputExamples?: unknown[];
|
|
863
|
+
inputExamples?: readonly unknown[];
|
|
447
864
|
derivations?: Record<string, string>;
|
|
448
865
|
}): LintDiagnostic[] {
|
|
449
866
|
const diagnostics: LintDiagnostic[] = [];
|
|
450
867
|
const description = op.description ?? "";
|
|
868
|
+
const hasDescriptionKey =
|
|
869
|
+
typeof op.descriptionKey === "string" && op.descriptionKey.length > 0;
|
|
451
870
|
|
|
452
|
-
if (description.length
|
|
871
|
+
if (description.trim().length > 0 && !hasDescriptionKey) {
|
|
453
872
|
diagnostics.push({
|
|
454
|
-
rule: "description-
|
|
873
|
+
rule: "operation-description-raw-prose",
|
|
455
874
|
level: "error",
|
|
456
875
|
field: "description",
|
|
457
|
-
message:
|
|
876
|
+
message:
|
|
877
|
+
"Operation description must use descriptionKey instead of raw static prose.",
|
|
458
878
|
});
|
|
459
879
|
}
|
|
460
880
|
|
|
461
|
-
|
|
462
|
-
if (
|
|
463
|
-
!(lowerDescription.includes("use") && lowerDescription.includes("when"))
|
|
464
|
-
) {
|
|
881
|
+
if (!hasDescriptionKey && description.length < 150) {
|
|
465
882
|
diagnostics.push({
|
|
466
|
-
rule: "description-
|
|
467
|
-
level: "
|
|
883
|
+
rule: "description-min-length",
|
|
884
|
+
level: "error",
|
|
468
885
|
field: "description",
|
|
469
|
-
message:
|
|
886
|
+
message: "Operation description must be at least 150 characters.",
|
|
470
887
|
});
|
|
471
888
|
}
|
|
472
889
|
|
|
473
|
-
|
|
474
|
-
collectMissingDescriptions(op.input, "input"),
|
|
475
|
-
)) {
|
|
890
|
+
if ((op.whenToUse?.length ?? 0) > 0 && !(op.whenToUseKeys?.length ?? 0)) {
|
|
476
891
|
diagnostics.push({
|
|
477
|
-
rule: "
|
|
892
|
+
rule: "operation-when-to-use-raw-prose",
|
|
478
893
|
level: "error",
|
|
479
|
-
field,
|
|
480
|
-
message:
|
|
894
|
+
field: "whenToUse",
|
|
895
|
+
message:
|
|
896
|
+
"Operation whenToUse must use whenToUseKeys instead of raw static prose.",
|
|
481
897
|
});
|
|
482
898
|
}
|
|
483
899
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
900
|
+
if (
|
|
901
|
+
(op.whenNotToUse?.length ?? 0) > 0 &&
|
|
902
|
+
!(op.whenNotToUseKeys?.length ?? 0)
|
|
903
|
+
) {
|
|
487
904
|
diagnostics.push({
|
|
488
|
-
rule: "
|
|
905
|
+
rule: "operation-when-not-to-use-raw-prose",
|
|
489
906
|
level: "error",
|
|
490
|
-
field,
|
|
491
|
-
message:
|
|
907
|
+
field: "whenNotToUse",
|
|
908
|
+
message:
|
|
909
|
+
"Operation whenNotToUse must use whenNotToUseKeys instead of raw static prose.",
|
|
910
|
+
});
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
const lowerDescription = description.toLowerCase();
|
|
914
|
+
if (
|
|
915
|
+
!hasDescriptionKey &&
|
|
916
|
+
!(lowerDescription.includes("use") && lowerDescription.includes("when"))
|
|
917
|
+
) {
|
|
918
|
+
diagnostics.push({
|
|
919
|
+
rule: "description-has-when-clause",
|
|
920
|
+
level: "warn",
|
|
921
|
+
field: "description",
|
|
922
|
+
message: 'Operation description should include both "use" and "when".',
|
|
492
923
|
});
|
|
493
924
|
}
|
|
494
925
|
|
|
926
|
+
diagnostics.push(
|
|
927
|
+
...collectSchemaDescriptionKeyDiagnostics(op.input, "input"),
|
|
928
|
+
...collectSchemaDescriptionKeyDiagnostics(op.output, "output"),
|
|
929
|
+
);
|
|
930
|
+
|
|
495
931
|
if (!hasBidirectionalFixtures(op.fixtures)) {
|
|
496
932
|
diagnostics.push({
|
|
497
933
|
rule: "fixtures-both-directions",
|
|
@@ -511,41 +947,98 @@ export function lintOperation(op: {
|
|
|
511
947
|
});
|
|
512
948
|
}
|
|
513
949
|
|
|
950
|
+
for (const field of uniqueFields(
|
|
951
|
+
collectUnmarkedSensitiveFields(op.input, "input"),
|
|
952
|
+
)) {
|
|
953
|
+
diagnostics.push({
|
|
954
|
+
rule: "sensitive-field-unmarked",
|
|
955
|
+
level: "warn",
|
|
956
|
+
field,
|
|
957
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
|
|
958
|
+
});
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
for (const field of uniqueFields(
|
|
962
|
+
collectUnmarkedSensitiveFields(op.output, "output"),
|
|
963
|
+
)) {
|
|
964
|
+
diagnostics.push({
|
|
965
|
+
rule: "sensitive-field-unmarked",
|
|
966
|
+
level: "warn",
|
|
967
|
+
field,
|
|
968
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
|
|
969
|
+
});
|
|
970
|
+
}
|
|
971
|
+
|
|
514
972
|
return diagnostics;
|
|
515
973
|
}
|
|
516
974
|
|
|
517
|
-
export function lintProvider(
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
string
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
975
|
+
export function lintProvider(
|
|
976
|
+
provider: {
|
|
977
|
+
id?: string;
|
|
978
|
+
allowedHosts?: readonly string[];
|
|
979
|
+
stealth?: unknown;
|
|
980
|
+
auth?: ProviderAuthLike;
|
|
981
|
+
credential?: {
|
|
982
|
+
keys?: readonly string[];
|
|
983
|
+
storesReusableSecret?: boolean;
|
|
984
|
+
justification?: string;
|
|
985
|
+
};
|
|
986
|
+
context?: {
|
|
987
|
+
keys?: readonly string[];
|
|
988
|
+
};
|
|
989
|
+
authFlowSource?: string;
|
|
990
|
+
providerSourceFiles?: Record<string, string>;
|
|
991
|
+
operations?: Record<
|
|
992
|
+
string,
|
|
993
|
+
{
|
|
994
|
+
description?: string;
|
|
995
|
+
descriptionKey?: string;
|
|
996
|
+
whenToUse?: readonly string[];
|
|
997
|
+
whenToUseKeys?: readonly string[];
|
|
998
|
+
whenNotToUse?: readonly string[];
|
|
999
|
+
whenNotToUseKeys?: readonly string[];
|
|
1000
|
+
input: unknown;
|
|
1001
|
+
output: unknown;
|
|
1002
|
+
fixtures?: unknown;
|
|
1003
|
+
inputExamples?: readonly unknown[];
|
|
1004
|
+
derivations?: Record<string, string>;
|
|
1005
|
+
handler?: unknown;
|
|
1006
|
+
source?: string;
|
|
1007
|
+
}
|
|
1008
|
+
>;
|
|
1009
|
+
meta?: {
|
|
1010
|
+
contract?: ProviderContractMetaLike;
|
|
1011
|
+
};
|
|
1012
|
+
reviewed?: string;
|
|
1013
|
+
},
|
|
1014
|
+
options: ProviderLintOptions = {},
|
|
1015
|
+
): LintDiagnostic[] {
|
|
543
1016
|
const diagnostics: LintDiagnostic[] = [
|
|
544
1017
|
...lintAllowedHosts(provider.id, provider.allowedHosts),
|
|
545
1018
|
...lintReviewed(provider.id, provider.reviewed),
|
|
546
1019
|
...lintAuthModel(provider),
|
|
1020
|
+
...lintStealthTransportUsage(provider),
|
|
1021
|
+
...lintCredentialWriteUsage(provider),
|
|
1022
|
+
...lintPlaywrightDirectImports(provider),
|
|
1023
|
+
...lintSelfHostedBrowserPatterns(provider, options),
|
|
547
1024
|
];
|
|
548
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
|
+
|
|
549
1042
|
if (!provider.operations) {
|
|
550
1043
|
return diagnostics;
|
|
551
1044
|
}
|
|
@@ -553,14 +1046,28 @@ export function lintProvider(provider: {
|
|
|
553
1046
|
diagnostics.push(
|
|
554
1047
|
...Object.entries(provider.operations).flatMap(
|
|
555
1048
|
([operationKey, operation]) =>
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
1049
|
+
[
|
|
1050
|
+
...lintOperation({
|
|
1051
|
+
description: operation.description ?? "",
|
|
1052
|
+
descriptionKey: operation.descriptionKey,
|
|
1053
|
+
whenToUse: operation.whenToUse,
|
|
1054
|
+
whenToUseKeys: operation.whenToUseKeys,
|
|
1055
|
+
whenNotToUse: operation.whenNotToUse,
|
|
1056
|
+
whenNotToUseKeys: operation.whenNotToUseKeys,
|
|
1057
|
+
input: operation.input,
|
|
1058
|
+
output: operation.output,
|
|
1059
|
+
fixtures: operation.fixtures,
|
|
1060
|
+
inputExamples: operation.inputExamples,
|
|
1061
|
+
derivations: operation.derivations,
|
|
1062
|
+
}),
|
|
1063
|
+
...lintPublicSchemaFieldNames(
|
|
1064
|
+
provider.id,
|
|
1065
|
+
operationKey,
|
|
1066
|
+
operation.input,
|
|
1067
|
+
operation.output,
|
|
1068
|
+
provider.meta?.contract?.publicSchemaFieldNames === "normalized",
|
|
1069
|
+
),
|
|
1070
|
+
].map((diagnostic) => ({
|
|
564
1071
|
...diagnostic,
|
|
565
1072
|
field: diagnostic.field
|
|
566
1073
|
? `operations.${operationKey}.${diagnostic.field}`
|