@apifuse/provider-sdk 2.1.0-beta.1 → 2.1.0-beta.10
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 +208 -2
- package/CHANGELOG.md +47 -0
- package/README.md +114 -10
- package/SUBMISSION.md +87 -0
- package/bin/apifuse-check.ts +86 -4
- package/bin/apifuse-dev.ts +87 -13
- package/bin/apifuse-pack-check.ts +80 -0
- package/bin/apifuse-pack-smoke.ts +303 -2
- package/bin/apifuse-perf.ts +142 -49
- package/bin/apifuse-record.ts +182 -104
- package/bin/apifuse-submit-check.ts +2538 -0
- package/bin/apifuse.ts +1 -1
- 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 +762 -0
- package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
- package/dist/cli/templates/provider/.gitignore.tpl +22 -0
- package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
- package/dist/cli/templates/provider/README.md.tpl +160 -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/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 +155 -0
- package/dist/define.d.ts +97 -0
- package/dist/define.js +1320 -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 +41 -0
- package/dist/index.js +37 -0
- package/dist/lint.d.ts +73 -0
- package/dist/lint.js +702 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +8 -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 +44 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1034 -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 +706 -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 +18 -0
- package/dist/runtime/stealth.js +834 -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 +1110 -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 +303 -0
- package/dist/types.d.ts +1326 -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 -30
- 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 +214 -52
- package/src/cli/templates/provider/.dockerignore.tpl +22 -0
- package/src/cli/templates/provider/.gitignore.tpl +22 -0
- package/src/cli/templates/provider/README.md.tpl +120 -1
- 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 -48
- 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/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 +215 -0
- package/src/define.ts +1688 -48
- 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 +174 -9
- package/src/lint.ts +547 -73
- package/src/observability.ts +41 -0
- package/src/provider.ts +104 -4
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +762 -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 +939 -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 +1159 -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 +1157 -75
- package/src/server/types.ts +37 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +31 -5
- package/src/types.ts +1107 -59
- package/src/runtime/tls.ts +0 -434
- package/src/types/playwright-stealth.d.ts +0 -9
package/dist/lint.js
ADDED
|
@@ -0,0 +1,702 @@
|
|
|
1
|
+
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint";
|
|
2
|
+
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY, } from "./schema";
|
|
3
|
+
function lintAllowedHosts(providerId, allowedHosts) {
|
|
4
|
+
const prefix = providerId ? `Provider "${providerId}"` : "Provider";
|
|
5
|
+
if (!allowedHosts) {
|
|
6
|
+
return [
|
|
7
|
+
{
|
|
8
|
+
rule: "allowed-hosts-required",
|
|
9
|
+
level: "error",
|
|
10
|
+
field: "allowedHosts",
|
|
11
|
+
message: `${prefix} must declare allowedHosts.`,
|
|
12
|
+
},
|
|
13
|
+
];
|
|
14
|
+
}
|
|
15
|
+
if (allowedHosts.length === 0) {
|
|
16
|
+
return [
|
|
17
|
+
{
|
|
18
|
+
rule: "allowed-hosts-non-empty",
|
|
19
|
+
level: "error",
|
|
20
|
+
field: "allowedHosts",
|
|
21
|
+
message: `${prefix} must declare at least one allowed host.`,
|
|
22
|
+
},
|
|
23
|
+
];
|
|
24
|
+
}
|
|
25
|
+
const wildcardHost = allowedHosts.find((host) => host.trim().includes("*"));
|
|
26
|
+
if (wildcardHost) {
|
|
27
|
+
return [
|
|
28
|
+
{
|
|
29
|
+
rule: "allowed-hosts-no-wildcards",
|
|
30
|
+
level: "error",
|
|
31
|
+
field: "allowedHosts",
|
|
32
|
+
message: `${prefix} must not declare wildcard allowedHosts entries like "${wildcardHost}".`,
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
function lintReviewed(providerId, reviewed) {
|
|
39
|
+
if (reviewed === "first-party" || reviewed === "community") {
|
|
40
|
+
return [];
|
|
41
|
+
}
|
|
42
|
+
const prefix = providerId ? `Provider "${providerId}"` : "Provider";
|
|
43
|
+
return [
|
|
44
|
+
{
|
|
45
|
+
rule: "reviewed-required",
|
|
46
|
+
level: "error",
|
|
47
|
+
field: "reviewed",
|
|
48
|
+
message: `${prefix} must declare reviewed as "first-party" or "community".`,
|
|
49
|
+
},
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
function hasReusableSecretKeys(keys) {
|
|
53
|
+
if (!keys) {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
return keys.some((key) => /(access_token|refresh_token|password|secret|cookie|session|token|api[_-]?key)/i.test(key));
|
|
57
|
+
}
|
|
58
|
+
function hasReusableReloginSecretKeys(keys) {
|
|
59
|
+
if (!keys) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
return keys.some((key) => /(password|passcode|secret|cookie|session)/i.test(key));
|
|
63
|
+
}
|
|
64
|
+
function getAuthFlowSource(provider) {
|
|
65
|
+
if (provider.authFlowSource) {
|
|
66
|
+
return provider.authFlowSource;
|
|
67
|
+
}
|
|
68
|
+
const parts = [
|
|
69
|
+
provider.auth?.flow?.start,
|
|
70
|
+
provider.auth?.flow?.continue,
|
|
71
|
+
provider.auth?.flow?.poll,
|
|
72
|
+
provider.auth?.flow?.abort,
|
|
73
|
+
provider.auth?.flow?.refresh,
|
|
74
|
+
];
|
|
75
|
+
return parts
|
|
76
|
+
.filter((part) => typeof part === "function")
|
|
77
|
+
.map((part) => part.toString())
|
|
78
|
+
.join("\n");
|
|
79
|
+
}
|
|
80
|
+
function lintAuthModel(provider) {
|
|
81
|
+
const diagnostics = [];
|
|
82
|
+
const providerLabel = provider.id ? `Provider "${provider.id}"` : "Provider";
|
|
83
|
+
const authMode = provider.auth?.mode;
|
|
84
|
+
const credentialKeys = provider.credential?.keys ?? [];
|
|
85
|
+
if (authMode === "api-key") {
|
|
86
|
+
diagnostics.push({
|
|
87
|
+
rule: "auth-mode-api-key-removed",
|
|
88
|
+
level: "error",
|
|
89
|
+
field: "auth.mode",
|
|
90
|
+
message: `${providerLabel} must not use auth.mode "api-key".`,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
if ((authMode === "credentials" || authMode === "oauth2") &&
|
|
94
|
+
typeof provider.auth?.flow?.continue !== "function") {
|
|
95
|
+
diagnostics.push({
|
|
96
|
+
rule: "auth-flow-continue-required",
|
|
97
|
+
level: "error",
|
|
98
|
+
field: "auth.flow.continue",
|
|
99
|
+
message: `${providerLabel} must define auth.flow.continue for ${authMode} auth mode.`,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
if (authMode === "credentials" && credentialKeys.length === 0) {
|
|
103
|
+
diagnostics.push({
|
|
104
|
+
rule: "credential-keys-required-when-credentials-mode",
|
|
105
|
+
level: "error",
|
|
106
|
+
field: "credential.keys",
|
|
107
|
+
message: `${providerLabel} must declare credential.keys for credentials auth mode.`,
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
if (hasReusableSecretKeys(credentialKeys) &&
|
|
111
|
+
(!provider.credential?.storesReusableSecret ||
|
|
112
|
+
!provider.credential.justification)) {
|
|
113
|
+
diagnostics.push({
|
|
114
|
+
rule: "credential-reusable-secret",
|
|
115
|
+
level: "error",
|
|
116
|
+
field: "credential",
|
|
117
|
+
message: `${providerLabel} must set storesReusableSecret and justification when credential.keys includes reusable secrets.`,
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
if (typeof provider.auth?.flow?.refresh === "function" &&
|
|
121
|
+
hasReusableReloginSecretKeys(credentialKeys) &&
|
|
122
|
+
(!provider.credential?.storesReusableSecret ||
|
|
123
|
+
!provider.credential.justification)) {
|
|
124
|
+
diagnostics.push({
|
|
125
|
+
rule: "auth-refresh-reusable-secret",
|
|
126
|
+
level: "error",
|
|
127
|
+
field: "credential",
|
|
128
|
+
message: `${providerLabel} must set storesReusableSecret and justification when auth.flow.refresh may silently re-login with reusable credential secrets.`,
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
if (authMode === "platform-managed" && credentialKeys.length > 0) {
|
|
132
|
+
diagnostics.push({
|
|
133
|
+
rule: "platform-managed-no-credential-keys",
|
|
134
|
+
level: "error",
|
|
135
|
+
field: "credential.keys",
|
|
136
|
+
message: `${providerLabel} must not declare credential.keys for platform-managed auth mode.`,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
139
|
+
const authFlowSource = getAuthFlowSource(provider);
|
|
140
|
+
if (authFlowSource.includes("ctx.context") &&
|
|
141
|
+
(provider.context?.keys?.length ?? 0) === 0) {
|
|
142
|
+
diagnostics.push({
|
|
143
|
+
rule: "context-keys-required",
|
|
144
|
+
level: "warn",
|
|
145
|
+
field: "context.keys",
|
|
146
|
+
message: `${providerLabel} should declare context.keys when auth flow code accesses ctx.context.*.`,
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
return diagnostics;
|
|
150
|
+
}
|
|
151
|
+
function isSchema(value) {
|
|
152
|
+
return (!!value &&
|
|
153
|
+
typeof value === "object" &&
|
|
154
|
+
"safeParse" in value &&
|
|
155
|
+
typeof value.safeParse === "function");
|
|
156
|
+
}
|
|
157
|
+
function getSchemaDef(schema) {
|
|
158
|
+
const def = schema.def ?? schema._def;
|
|
159
|
+
if (def && typeof def === "object") {
|
|
160
|
+
return def;
|
|
161
|
+
}
|
|
162
|
+
return {};
|
|
163
|
+
}
|
|
164
|
+
function isSchemaRecord(value) {
|
|
165
|
+
if (!value || typeof value !== "object") {
|
|
166
|
+
return false;
|
|
167
|
+
}
|
|
168
|
+
for (const entry of Object.values(value)) {
|
|
169
|
+
if (!isSchema(entry)) {
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return true;
|
|
174
|
+
}
|
|
175
|
+
function getObjectShape(schema) {
|
|
176
|
+
const rawShape = typeof schema.shape === "function" ? schema.shape() : schema.shape;
|
|
177
|
+
if (isSchemaRecord(rawShape)) {
|
|
178
|
+
return rawShape;
|
|
179
|
+
}
|
|
180
|
+
const defShape = getSchemaDef(schema).shape;
|
|
181
|
+
if (typeof defShape === "function") {
|
|
182
|
+
const resolved = defShape();
|
|
183
|
+
if (isSchemaRecord(resolved)) {
|
|
184
|
+
return resolved;
|
|
185
|
+
}
|
|
186
|
+
return {};
|
|
187
|
+
}
|
|
188
|
+
if (isSchemaRecord(defShape)) {
|
|
189
|
+
return defShape;
|
|
190
|
+
}
|
|
191
|
+
return {};
|
|
192
|
+
}
|
|
193
|
+
function getChildSchemas(schema) {
|
|
194
|
+
const seen = new Map();
|
|
195
|
+
const def = getSchemaDef(schema);
|
|
196
|
+
const add = (key, value) => {
|
|
197
|
+
if (!isSchema(value)) {
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
seen.set(`${key}:${seen.size}`, value);
|
|
201
|
+
};
|
|
202
|
+
for (const [key, value] of Object.entries(getObjectShape(schema))) {
|
|
203
|
+
add(key, value);
|
|
204
|
+
}
|
|
205
|
+
add("element", schema.element);
|
|
206
|
+
add("innerType", schema.innerType);
|
|
207
|
+
add("unwrap", schema.unwrap?.());
|
|
208
|
+
add("sourceType", schema.sourceType?.());
|
|
209
|
+
add("in", schema.in);
|
|
210
|
+
add("out", schema.out);
|
|
211
|
+
add("left", schema.left);
|
|
212
|
+
add("right", schema.right);
|
|
213
|
+
if (Array.isArray(schema.items)) {
|
|
214
|
+
for (const [index, item] of schema.items.entries()) {
|
|
215
|
+
add(String(index), item);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (Array.isArray(def.items)) {
|
|
219
|
+
for (const [index, item] of def.items.entries()) {
|
|
220
|
+
add(String(index), item);
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
const options = schema.options ?? def.options;
|
|
224
|
+
if (Array.isArray(options)) {
|
|
225
|
+
for (const [index, option] of options.entries()) {
|
|
226
|
+
add(String(index), option);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
else if (options instanceof Set) {
|
|
230
|
+
for (const [index, option] of Array.from(options).entries()) {
|
|
231
|
+
add(String(index), option);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else if (options instanceof Map) {
|
|
235
|
+
for (const [key, option] of options.entries()) {
|
|
236
|
+
add(String(key), option);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
for (const key of [
|
|
240
|
+
"schema",
|
|
241
|
+
"innerType",
|
|
242
|
+
"type",
|
|
243
|
+
"valueType",
|
|
244
|
+
"keyType",
|
|
245
|
+
"item",
|
|
246
|
+
"rest",
|
|
247
|
+
"catchall",
|
|
248
|
+
"option",
|
|
249
|
+
"pipe",
|
|
250
|
+
"payload",
|
|
251
|
+
"shape",
|
|
252
|
+
]) {
|
|
253
|
+
const value = def[key];
|
|
254
|
+
if (Array.isArray(value)) {
|
|
255
|
+
for (const [index, item] of value.entries()) {
|
|
256
|
+
add(`${key}.${index}`, item);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
add(key, value);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
return Array.from(seen.entries()).map(([entryKey, child]) => ({
|
|
264
|
+
key: entryKey.split(":")[0] ?? entryKey,
|
|
265
|
+
schema: child,
|
|
266
|
+
}));
|
|
267
|
+
}
|
|
268
|
+
function uniqueFields(fields) {
|
|
269
|
+
return Array.from(new Set(fields));
|
|
270
|
+
}
|
|
271
|
+
function isSensitiveSchema(schema) {
|
|
272
|
+
if (!schema || typeof schema !== "object" || !("meta" in schema)) {
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
const meta = schema.meta;
|
|
276
|
+
if (typeof meta !== "function")
|
|
277
|
+
return false;
|
|
278
|
+
const metadata = meta.call(schema);
|
|
279
|
+
return (!!metadata &&
|
|
280
|
+
typeof metadata === "object" &&
|
|
281
|
+
Reflect.get(metadata, APIFUSE_SENSITIVE_META_KEY) === true);
|
|
282
|
+
}
|
|
283
|
+
function getSchemaMetadata(schema) {
|
|
284
|
+
return schema.meta?.() ?? {};
|
|
285
|
+
}
|
|
286
|
+
function getSchemaDescriptionKey(schema) {
|
|
287
|
+
const value = Reflect.get(getSchemaMetadata(schema), APIFUSE_DESCRIPTION_KEY_META_KEY);
|
|
288
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
289
|
+
}
|
|
290
|
+
const SENSITIVE_FIELD_NAMES = new Set([
|
|
291
|
+
"apikey",
|
|
292
|
+
"authorization",
|
|
293
|
+
"cookie",
|
|
294
|
+
"secret",
|
|
295
|
+
"secrets",
|
|
296
|
+
"token",
|
|
297
|
+
"accesstoken",
|
|
298
|
+
"refreshtoken",
|
|
299
|
+
"password",
|
|
300
|
+
"passwd",
|
|
301
|
+
"otp",
|
|
302
|
+
"otpcode",
|
|
303
|
+
"phone",
|
|
304
|
+
"phonenumber",
|
|
305
|
+
"paymenturl",
|
|
306
|
+
]);
|
|
307
|
+
function isSensitiveFieldName(name) {
|
|
308
|
+
const normalized = name.toLowerCase().replace(/[-_\s]/g, "");
|
|
309
|
+
return SENSITIVE_FIELD_NAMES.has(normalized);
|
|
310
|
+
}
|
|
311
|
+
function collectUnmarkedSensitiveFields(schema, basePath, seen = new Set()) {
|
|
312
|
+
if (!isSchema(schema) || seen.has(schema)) {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
seen.add(schema);
|
|
316
|
+
const out = [];
|
|
317
|
+
for (const [key, child] of Object.entries(getObjectShape(schema))) {
|
|
318
|
+
const childPath = basePath ? `${basePath}.${key}` : key;
|
|
319
|
+
if (isSensitiveFieldName(key) && !isSensitiveSchema(child)) {
|
|
320
|
+
out.push(childPath);
|
|
321
|
+
}
|
|
322
|
+
out.push(...collectUnmarkedSensitiveFields(child, childPath, seen));
|
|
323
|
+
}
|
|
324
|
+
for (const child of getChildSchemas(schema)) {
|
|
325
|
+
if (Object.hasOwn(getObjectShape(schema), child.key))
|
|
326
|
+
continue;
|
|
327
|
+
const isWrapperNode = [
|
|
328
|
+
"unwrap",
|
|
329
|
+
"innerType",
|
|
330
|
+
"sourceType",
|
|
331
|
+
"schema",
|
|
332
|
+
"type",
|
|
333
|
+
"in",
|
|
334
|
+
"out",
|
|
335
|
+
"option",
|
|
336
|
+
"pipe",
|
|
337
|
+
"payload",
|
|
338
|
+
"item",
|
|
339
|
+
"rest",
|
|
340
|
+
"catchall",
|
|
341
|
+
"keyType",
|
|
342
|
+
"valueType",
|
|
343
|
+
].includes(child.key);
|
|
344
|
+
const childPath = child.key === "element" || child.key.startsWith("element.")
|
|
345
|
+
? `${basePath}[]`
|
|
346
|
+
: isWrapperNode || child.key.startsWith("pipe.")
|
|
347
|
+
? basePath
|
|
348
|
+
: basePath
|
|
349
|
+
? `${basePath}.${child.key}`
|
|
350
|
+
: child.key;
|
|
351
|
+
out.push(...collectUnmarkedSensitiveFields(child.schema, childPath, seen));
|
|
352
|
+
}
|
|
353
|
+
return out;
|
|
354
|
+
}
|
|
355
|
+
function collectSchemaDescriptionKeyDiagnostics(schema, basePath, seen = new Set(), requireCurrentDescription = true) {
|
|
356
|
+
if (!isSchema(schema) || seen.has(schema)) {
|
|
357
|
+
return [];
|
|
358
|
+
}
|
|
359
|
+
seen.add(schema);
|
|
360
|
+
const diagnostics = [];
|
|
361
|
+
const currentPath = basePath || "schema";
|
|
362
|
+
const hasDescriptionKey = getSchemaDescriptionKey(schema) !== undefined;
|
|
363
|
+
if (schema.description && !hasDescriptionKey) {
|
|
364
|
+
diagnostics.push({
|
|
365
|
+
rule: "schema-description-raw-prose",
|
|
366
|
+
level: "error",
|
|
367
|
+
field: currentPath,
|
|
368
|
+
message: `Schema field "${currentPath}" must use .describeKey() or describeKey() instead of raw static prose.`,
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
if (requireCurrentDescription && !hasDescriptionKey) {
|
|
372
|
+
diagnostics.push({
|
|
373
|
+
rule: "schema-description-key-required",
|
|
374
|
+
level: "error",
|
|
375
|
+
field: currentPath,
|
|
376
|
+
message: schema.description
|
|
377
|
+
? `Schema field "${currentPath}" has a raw description but is missing .describeKey() or describeKey() metadata.`
|
|
378
|
+
: `Schema field "${currentPath}" is missing .describeKey() or describeKey() metadata.`,
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
for (const child of getChildSchemas(schema)) {
|
|
382
|
+
const isWrapperNode = [
|
|
383
|
+
"unwrap",
|
|
384
|
+
"innerType",
|
|
385
|
+
"sourceType",
|
|
386
|
+
"schema",
|
|
387
|
+
"type",
|
|
388
|
+
"in",
|
|
389
|
+
"out",
|
|
390
|
+
"option",
|
|
391
|
+
"pipe",
|
|
392
|
+
"payload",
|
|
393
|
+
"item",
|
|
394
|
+
"rest",
|
|
395
|
+
"catchall",
|
|
396
|
+
"keyType",
|
|
397
|
+
"valueType",
|
|
398
|
+
].includes(child.key);
|
|
399
|
+
const isStructuralNode = isWrapperNode ||
|
|
400
|
+
child.key.startsWith("pipe.") ||
|
|
401
|
+
child.key === "element" ||
|
|
402
|
+
child.key.startsWith("element.");
|
|
403
|
+
const childPath = isWrapperNode
|
|
404
|
+
? currentPath
|
|
405
|
+
: currentPath === "schema"
|
|
406
|
+
? child.key
|
|
407
|
+
: /^\d+$/.test(child.key)
|
|
408
|
+
? `${currentPath}[${child.key}]`
|
|
409
|
+
: child.key === "element" || child.key.startsWith("element.")
|
|
410
|
+
? `${currentPath}[]`
|
|
411
|
+
: `${currentPath}.${child.key}`;
|
|
412
|
+
diagnostics.push(...collectSchemaDescriptionKeyDiagnostics(child.schema, childPath, seen, !isStructuralNode));
|
|
413
|
+
}
|
|
414
|
+
return diagnostics;
|
|
415
|
+
}
|
|
416
|
+
function isComplexSchema(schema, seen = new Set()) {
|
|
417
|
+
if (!isSchema(schema) || seen.has(schema)) {
|
|
418
|
+
return false;
|
|
419
|
+
}
|
|
420
|
+
seen.add(schema);
|
|
421
|
+
const children = getChildSchemas(schema);
|
|
422
|
+
const hasNestedComposite = children.some(({ schema: child }) => {
|
|
423
|
+
const childChildren = getChildSchemas(child);
|
|
424
|
+
return childChildren.length > 0;
|
|
425
|
+
});
|
|
426
|
+
return (hasNestedComposite ||
|
|
427
|
+
children.some(({ schema: child }) => isComplexSchema(child, seen)));
|
|
428
|
+
}
|
|
429
|
+
function hasBidirectionalFixtures(fixtures) {
|
|
430
|
+
if (!fixtures || typeof fixtures !== "object") {
|
|
431
|
+
return true;
|
|
432
|
+
}
|
|
433
|
+
return "request" in fixtures && "response" in fixtures;
|
|
434
|
+
}
|
|
435
|
+
function getOperationSource(operation) {
|
|
436
|
+
if (operation.source) {
|
|
437
|
+
return operation.source;
|
|
438
|
+
}
|
|
439
|
+
return typeof operation.handler === "function"
|
|
440
|
+
? operation.handler.toString()
|
|
441
|
+
: "";
|
|
442
|
+
}
|
|
443
|
+
function lintStealthTransportUsage(provider) {
|
|
444
|
+
if (provider.stealth || !provider.operations) {
|
|
445
|
+
return [];
|
|
446
|
+
}
|
|
447
|
+
const providerLabel = provider.id ? `Provider "${provider.id}"` : "Provider";
|
|
448
|
+
return Object.entries(provider.operations).flatMap(([operationKey, operation]) => {
|
|
449
|
+
const source = getOperationSource(operation);
|
|
450
|
+
if (!/\bctx\.stealth\b/.test(source)) {
|
|
451
|
+
return [];
|
|
452
|
+
}
|
|
453
|
+
return [
|
|
454
|
+
{
|
|
455
|
+
rule: "stealth-config-required",
|
|
456
|
+
level: "error",
|
|
457
|
+
field: `operations.${operationKey}`,
|
|
458
|
+
message: `${providerLabel} operation "${operationKey}" uses ctx.stealth but provider.stealth is not declared.`,
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
function lintCredentialWriteUsage(provider) {
|
|
464
|
+
if (!provider.operations) {
|
|
465
|
+
return [];
|
|
466
|
+
}
|
|
467
|
+
return Object.entries(provider.operations).flatMap(([operationKey, operation]) => {
|
|
468
|
+
const source = getOperationSource(operation);
|
|
469
|
+
if (!/\bctx\.credential\.(?:set|setMany)\s*\(/.test(source)) {
|
|
470
|
+
return [];
|
|
471
|
+
}
|
|
472
|
+
return [
|
|
473
|
+
{
|
|
474
|
+
rule: "ctx-credential-write-forbidden-in-handler",
|
|
475
|
+
level: "error",
|
|
476
|
+
field: `operations.${operationKey}.handler`,
|
|
477
|
+
message: "Operation handlers must not mutate credentials; return refreshed credentials from auth.flow.refresh instead.",
|
|
478
|
+
},
|
|
479
|
+
];
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
function lintPlaywrightDirectImports(provider) {
|
|
483
|
+
const diagnostics = [];
|
|
484
|
+
const importPattern = /(?:import\s+(?:type\s+)?[\s\S]*?\s+from\s+["'](?:playwright|playwright-core)["']|require\(\s*["'](?:playwright|playwright-core)["']\s*\)|import\(\s*["'](?:playwright|playwright-core)["']\s*\))/;
|
|
485
|
+
if (provider.authFlowSource && importPattern.test(provider.authFlowSource)) {
|
|
486
|
+
diagnostics.push({
|
|
487
|
+
rule: "playwright-direct-import",
|
|
488
|
+
level: "warn",
|
|
489
|
+
field: "auth.flow",
|
|
490
|
+
message: "Provider auth flow imports playwright directly; use ctx.browser frame-aware methods so the SDK can enforce the CDP pool runtime.",
|
|
491
|
+
});
|
|
492
|
+
}
|
|
493
|
+
for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
|
|
494
|
+
if (!importPattern.test(source)) {
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
diagnostics.push({
|
|
498
|
+
rule: "playwright-direct-import",
|
|
499
|
+
level: "warn",
|
|
500
|
+
field: `sourceFiles.${filePath}`,
|
|
501
|
+
message: "Provider source imports playwright directly; use ctx.browser frame-aware methods so the SDK can enforce the CDP pool runtime.",
|
|
502
|
+
});
|
|
503
|
+
}
|
|
504
|
+
if (!provider.operations) {
|
|
505
|
+
return diagnostics;
|
|
506
|
+
}
|
|
507
|
+
for (const [operationKey, operation] of Object.entries(provider.operations)) {
|
|
508
|
+
const source = getOperationSource(operation);
|
|
509
|
+
if (!importPattern.test(source)) {
|
|
510
|
+
continue;
|
|
511
|
+
}
|
|
512
|
+
diagnostics.push({
|
|
513
|
+
rule: "playwright-direct-import",
|
|
514
|
+
level: "warn",
|
|
515
|
+
field: `operations.${operationKey}.handler`,
|
|
516
|
+
message: "Operation source imports playwright directly; use ctx.browser frame-aware methods so the SDK can enforce the CDP pool runtime.",
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
return diagnostics;
|
|
520
|
+
}
|
|
521
|
+
const SELF_HOSTED_BROWSER_MESSAGE = "Official browser providers must use ctx.browser backed by the managed CDP Pool; do not launch or connect to provider-local Chrome/CDP runtimes.";
|
|
522
|
+
const SELF_HOSTED_BROWSER_PATTERNS = [
|
|
523
|
+
{
|
|
524
|
+
rule: "browser-self-hosted-launch",
|
|
525
|
+
pattern: /\b(?:playwright|chromium|firefox|webkit|puppeteer)\.launch\s*\(/,
|
|
526
|
+
message: `${SELF_HOSTED_BROWSER_MESSAGE} Replace direct Playwright/Puppeteer launch calls with ctx.browser.newPage() or ctx.browser.withIsolatedContext().`,
|
|
527
|
+
},
|
|
528
|
+
{
|
|
529
|
+
rule: "browser-self-hosted-child-process",
|
|
530
|
+
pattern: /(?:\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)/,
|
|
531
|
+
message: `${SELF_HOSTED_BROWSER_MESSAGE} Provider pods must not start Chrome with child_process, Bun.spawn, or shell commands.`,
|
|
532
|
+
},
|
|
533
|
+
{
|
|
534
|
+
rule: "browser-self-hosted-remote-debugging-port",
|
|
535
|
+
pattern: /(?:\b(?:google-chrome|chrome|chromium|chromium-browser)\b[\s\S]{0,240}--remote-debugging-port\b|--remote-debugging-port(?:=|\s+))/,
|
|
536
|
+
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.`,
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
rule: "browser-direct-cdp-version-poll",
|
|
540
|
+
pattern: /\/json\/version\b/,
|
|
541
|
+
message: `${SELF_HOSTED_BROWSER_MESSAGE} Do not poll /json/version from provider code; the SDK manages CDP leases through APIFUSE__CDP_POOL__URL.`,
|
|
542
|
+
},
|
|
543
|
+
{
|
|
544
|
+
rule: "browser-provider-local-cdp-env",
|
|
545
|
+
pattern: /\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*\])/,
|
|
546
|
+
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.`,
|
|
547
|
+
},
|
|
548
|
+
];
|
|
549
|
+
function lintSelfHostedBrowserPatterns(provider, options) {
|
|
550
|
+
const diagnostics = [];
|
|
551
|
+
const level = options.mode === "standalone" ? "warn" : "error";
|
|
552
|
+
const sources = [];
|
|
553
|
+
if (provider.authFlowSource) {
|
|
554
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
555
|
+
}
|
|
556
|
+
for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
|
|
557
|
+
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
558
|
+
}
|
|
559
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
560
|
+
const source = getOperationSource(operation);
|
|
561
|
+
if (source) {
|
|
562
|
+
sources.push({
|
|
563
|
+
field: `operations.${operationKey}.handler`,
|
|
564
|
+
source,
|
|
565
|
+
});
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
for (const { field, source } of sources) {
|
|
569
|
+
for (const item of SELF_HOSTED_BROWSER_PATTERNS) {
|
|
570
|
+
item.pattern.lastIndex = 0;
|
|
571
|
+
if (!item.pattern.test(source)) {
|
|
572
|
+
continue;
|
|
573
|
+
}
|
|
574
|
+
diagnostics.push({
|
|
575
|
+
rule: item.rule,
|
|
576
|
+
level,
|
|
577
|
+
field,
|
|
578
|
+
message: item.message,
|
|
579
|
+
});
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
return diagnostics;
|
|
583
|
+
}
|
|
584
|
+
export function lintOperation(op) {
|
|
585
|
+
const diagnostics = [];
|
|
586
|
+
const description = op.description ?? "";
|
|
587
|
+
const hasDescriptionKey = typeof op.descriptionKey === "string" && op.descriptionKey.length > 0;
|
|
588
|
+
if (description.trim().length > 0 && !hasDescriptionKey) {
|
|
589
|
+
diagnostics.push({
|
|
590
|
+
rule: "operation-description-raw-prose",
|
|
591
|
+
level: "error",
|
|
592
|
+
field: "description",
|
|
593
|
+
message: "Operation description must use descriptionKey instead of raw static prose.",
|
|
594
|
+
});
|
|
595
|
+
}
|
|
596
|
+
if (!hasDescriptionKey && description.length < 150) {
|
|
597
|
+
diagnostics.push({
|
|
598
|
+
rule: "description-min-length",
|
|
599
|
+
level: "error",
|
|
600
|
+
field: "description",
|
|
601
|
+
message: "Operation description must be at least 150 characters.",
|
|
602
|
+
});
|
|
603
|
+
}
|
|
604
|
+
if ((op.whenToUse?.length ?? 0) > 0 && !(op.whenToUseKeys?.length ?? 0)) {
|
|
605
|
+
diagnostics.push({
|
|
606
|
+
rule: "operation-when-to-use-raw-prose",
|
|
607
|
+
level: "error",
|
|
608
|
+
field: "whenToUse",
|
|
609
|
+
message: "Operation whenToUse must use whenToUseKeys instead of raw static prose.",
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
if ((op.whenNotToUse?.length ?? 0) > 0 &&
|
|
613
|
+
!(op.whenNotToUseKeys?.length ?? 0)) {
|
|
614
|
+
diagnostics.push({
|
|
615
|
+
rule: "operation-when-not-to-use-raw-prose",
|
|
616
|
+
level: "error",
|
|
617
|
+
field: "whenNotToUse",
|
|
618
|
+
message: "Operation whenNotToUse must use whenNotToUseKeys instead of raw static prose.",
|
|
619
|
+
});
|
|
620
|
+
}
|
|
621
|
+
const lowerDescription = description.toLowerCase();
|
|
622
|
+
if (!hasDescriptionKey &&
|
|
623
|
+
!(lowerDescription.includes("use") && lowerDescription.includes("when"))) {
|
|
624
|
+
diagnostics.push({
|
|
625
|
+
rule: "description-has-when-clause",
|
|
626
|
+
level: "warn",
|
|
627
|
+
field: "description",
|
|
628
|
+
message: 'Operation description should include both "use" and "when".',
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
diagnostics.push(...collectSchemaDescriptionKeyDiagnostics(op.input, "input"), ...collectSchemaDescriptionKeyDiagnostics(op.output, "output"));
|
|
632
|
+
if (!hasBidirectionalFixtures(op.fixtures)) {
|
|
633
|
+
diagnostics.push({
|
|
634
|
+
rule: "fixtures-both-directions",
|
|
635
|
+
level: "error",
|
|
636
|
+
field: "fixtures",
|
|
637
|
+
message: "Fixtures must include both request and response.",
|
|
638
|
+
});
|
|
639
|
+
}
|
|
640
|
+
if (isComplexSchema(op.input) && (op.inputExamples?.length ?? 0) < 2) {
|
|
641
|
+
diagnostics.push({
|
|
642
|
+
rule: "complex-input-has-examples",
|
|
643
|
+
level: "warn",
|
|
644
|
+
field: "inputExamples",
|
|
645
|
+
message: "Complex input schemas should provide at least 2 input examples.",
|
|
646
|
+
});
|
|
647
|
+
}
|
|
648
|
+
for (const field of uniqueFields(collectUnmarkedSensitiveFields(op.input, "input"))) {
|
|
649
|
+
diagnostics.push({
|
|
650
|
+
rule: "sensitive-field-unmarked",
|
|
651
|
+
level: "warn",
|
|
652
|
+
field,
|
|
653
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
|
|
654
|
+
});
|
|
655
|
+
}
|
|
656
|
+
for (const field of uniqueFields(collectUnmarkedSensitiveFields(op.output, "output"))) {
|
|
657
|
+
diagnostics.push({
|
|
658
|
+
rule: "sensitive-field-unmarked",
|
|
659
|
+
level: "warn",
|
|
660
|
+
field,
|
|
661
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
|
|
662
|
+
});
|
|
663
|
+
}
|
|
664
|
+
return diagnostics;
|
|
665
|
+
}
|
|
666
|
+
export function lintProvider(provider, options = {}) {
|
|
667
|
+
const diagnostics = [
|
|
668
|
+
...lintAllowedHosts(provider.id, provider.allowedHosts),
|
|
669
|
+
...lintReviewed(provider.id, provider.reviewed),
|
|
670
|
+
...lintAuthModel(provider),
|
|
671
|
+
...lintStealthTransportUsage(provider),
|
|
672
|
+
...lintCredentialWriteUsage(provider),
|
|
673
|
+
...lintPlaywrightDirectImports(provider),
|
|
674
|
+
...lintSelfHostedBrowserPatterns(provider, options),
|
|
675
|
+
];
|
|
676
|
+
if (!provider.operations) {
|
|
677
|
+
return diagnostics;
|
|
678
|
+
}
|
|
679
|
+
diagnostics.push(...Object.entries(provider.operations).flatMap(([operationKey, operation]) => [
|
|
680
|
+
...lintOperation({
|
|
681
|
+
description: operation.description ?? "",
|
|
682
|
+
descriptionKey: operation.descriptionKey,
|
|
683
|
+
whenToUse: operation.whenToUse,
|
|
684
|
+
whenToUseKeys: operation.whenToUseKeys,
|
|
685
|
+
whenNotToUse: operation.whenNotToUse,
|
|
686
|
+
whenNotToUseKeys: operation.whenNotToUseKeys,
|
|
687
|
+
input: operation.input,
|
|
688
|
+
output: operation.output,
|
|
689
|
+
fixtures: operation.fixtures,
|
|
690
|
+
inputExamples: operation.inputExamples,
|
|
691
|
+
derivations: operation.derivations,
|
|
692
|
+
}),
|
|
693
|
+
...lintPublicSchemaFieldNames(provider.id, operationKey, operation.input, operation.output, provider.meta?.contract?.publicSchemaFieldNames === "normalized"),
|
|
694
|
+
].map((diagnostic) => ({
|
|
695
|
+
...diagnostic,
|
|
696
|
+
field: diagnostic.field
|
|
697
|
+
? `operations.${operationKey}.${diagnostic.field}`
|
|
698
|
+
: `operations.${operationKey}`,
|
|
699
|
+
message: `[${operationKey}] ${diagnostic.message}`,
|
|
700
|
+
}))));
|
|
701
|
+
return diagnostics;
|
|
702
|
+
}
|