@apifuse/provider-sdk 2.1.0-beta.21 → 2.1.0-beta.22

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # @apifuse/provider-sdk Changelog
2
2
 
3
+ ## 2.1.0-beta.22
4
+
5
+ - Release candidate for main commit af84b1e91c408b69773468fdaef80a01a36707cf.
6
+
3
7
  ## 2.1.0-beta.21
4
8
 
5
9
  - Release candidate for main commit 8124b3eb150cc7a73a86a95266b3747763a494ae.
@@ -285,6 +285,9 @@ export async function buildSubmitCheckReport(
285
285
  checks.push(scoreLocaleCatalog(providerRoot, provider));
286
286
  checks.push(scoreOperationMetadata(provider));
287
287
  checks.push(scoreFixtureCoverage(provider));
288
+ checks.push(scoreFixtureProvenance(providerRoot, provider));
289
+ checks.push(scoreVendorKeyLeak(providerRoot));
290
+ checks.push(scoreVendorTimestampLeak(providerRoot));
288
291
  checks.push(scoreHealthCoverage(provider));
289
292
  checks.push(scoreAuthSafety(provider));
290
293
  checks.push(scoreSmoke(smokeResult, args.smokeNote));
@@ -728,10 +731,11 @@ function unwrapParens(expr: string): string {
728
731
  // closing bracket. This lets a property value be read across newlines, so a
729
732
  // multi-line `input: z.object({...})\n.passthrough()` is captured whole.
730
733
  function balancedValueExpression(source: string, valueStart: number): string {
734
+ const masked = maskCommentsAndStrings(source);
731
735
  let depth = 0;
732
736
  let index = valueStart;
733
737
  for (; index < source.length; index += 1) {
734
- const ch = source[index];
738
+ const ch = masked[index];
735
739
  if (ch === "(" || ch === "{" || ch === "[") {
736
740
  depth += 1;
737
741
  } else if (ch === ")" || ch === "}" || ch === "]") {
@@ -1973,6 +1977,656 @@ function scoreFixtureCoverage(provider: ProviderDefinition): SubmitCheck {
1973
1977
  );
1974
1978
  }
1975
1979
 
1980
+ const GENERATED_LOCAL_ONLY_SCAFFOLD_REASON = /generated local-only scaffold/i;
1981
+
1982
+ function scoreFixtureProvenance(
1983
+ providerRoot: string,
1984
+ provider: ProviderDefinition,
1985
+ ): SubmitCheck {
1986
+ const rawPath = resolve(providerRoot, "__fixtures__", "raw.json");
1987
+ let hasRecordedEvidence = false;
1988
+ if (existsSync(rawPath)) {
1989
+ try {
1990
+ hasRecordedEvidence = hasNonEmptyRecordedFixture(JSON.parse(readFileSync(rawPath, "utf8")));
1991
+ } catch {
1992
+ hasRecordedEvidence = false;
1993
+ }
1994
+ }
1995
+
1996
+ if (hasRecordedEvidence) {
1997
+ return pass(
1998
+ "fixture-provenance",
1999
+ "fixtures",
2000
+ "Recorded upstream fixture evidence is present.",
2001
+ 0,
2002
+ );
2003
+ }
2004
+
2005
+ if (allOperationsAreGeneratedLocalScaffold(provider)) {
2006
+ return {
2007
+ id: "fixture-provenance",
2008
+ category: "fixtures",
2009
+ level: "warn",
2010
+ status: "warn",
2011
+ points: 0,
2012
+ maxPoints: 0,
2013
+ message:
2014
+ "Generated local-only scaffold has no recorded upstream fixture evidence yet; run `bun run record` once real operations exist.",
2015
+ remediation:
2016
+ "Run `bun run record` (apifuse record) against the real upstream to capture raw payloads once real operations exist.",
2017
+ evidence: ["__fixtures__/raw.json"],
2018
+ };
2019
+ }
2020
+
2021
+ return blocker(
2022
+ "fixture-provenance",
2023
+ "fixtures",
2024
+ "No recorded upstream fixture evidence (__fixtures__/raw.json is empty or missing).",
2025
+ "Run `bun run record` (apifuse record) against the real upstream to capture actual recorded upstream payloads per operation in __fixtures__/raw.json; derive normalized expectations in tests from mapper(recorded raw). Hand-authored fixtures without recorded provenance are not reviewable.",
2026
+ 0,
2027
+ ["__fixtures__/raw.json"],
2028
+ );
2029
+ }
2030
+
2031
+ function hasNonEmptyRecordedFixture(value: unknown): boolean {
2032
+ return recordedFixtureStats(value, 0).hasNestedSubstance;
2033
+ }
2034
+
2035
+ function recordedFixtureStats(
2036
+ value: unknown,
2037
+ depth: number,
2038
+ ): { hasNestedSubstance: boolean; leafValues: number } {
2039
+ if (value === null || value === undefined) {
2040
+ return { hasNestedSubstance: false, leafValues: 0 };
2041
+ }
2042
+ if (Array.isArray(value)) {
2043
+ let leafValues = 0;
2044
+ let hasNestedSubstance = false;
2045
+ for (const item of value) {
2046
+ const child = recordedFixtureStats(item, depth + 1);
2047
+ leafValues += child.leafValues;
2048
+ hasNestedSubstance ||= child.hasNestedSubstance;
2049
+ }
2050
+ return {
2051
+ hasNestedSubstance: hasNestedSubstance || (depth >= 1 && value.length > 0 && leafValues >= 2),
2052
+ leafValues,
2053
+ };
2054
+ }
2055
+ if (typeof value === "object") {
2056
+ let leafValues = 0;
2057
+ let hasNestedSubstance = false;
2058
+ for (const item of Object.values(value)) {
2059
+ const child = recordedFixtureStats(item, depth + 1);
2060
+ leafValues += child.leafValues;
2061
+ hasNestedSubstance ||= child.hasNestedSubstance;
2062
+ }
2063
+ return {
2064
+ hasNestedSubstance:
2065
+ hasNestedSubstance || (depth >= 1 && Object.keys(value).length > 0 && leafValues >= 2),
2066
+ leafValues,
2067
+ };
2068
+ }
2069
+ if (typeof value === "string" && value.length === 0) {
2070
+ return { hasNestedSubstance: false, leafValues: 0 };
2071
+ }
2072
+ return { hasNestedSubstance: false, leafValues: 1 };
2073
+ }
2074
+
2075
+ function allOperationsAreGeneratedLocalScaffold(provider: ProviderDefinition): boolean {
2076
+ const operations = Object.values(provider.operations);
2077
+ return (
2078
+ operations.length > 0 &&
2079
+ operations.every((operation) =>
2080
+ GENERATED_LOCAL_ONLY_SCAFFOLD_REASON.test(operation.healthCheckUnsupported?.reason ?? ""),
2081
+ )
2082
+ );
2083
+ }
2084
+
2085
+ function scoreVendorKeyLeak(providerRoot: string): SubmitCheck {
2086
+ return escapeHatchResult(providerRoot, "vendor-key-leak", findVendorKeyLeakFindings(providerRoot), {
2087
+ blockerMessage: "Public schema keys leak raw vendor field names.",
2088
+ remediation:
2089
+ "Normalize public request/response fields to APIFuse-standard lowerCamelCase names (e.g. isOpen24h, latitude); keep raw vendor keys only in upstream-parsing schemas (const upstream... = z.object(...)). Add `// @apifuse-allow vendor-key-leak` only with a comment explaining why the vendor name is genuinely canonical.",
2090
+ passMessage: "No vendor field-name leaks detected in public schemas.",
2091
+ });
2092
+ }
2093
+
2094
+ function scoreVendorTimestampLeak(providerRoot: string): SubmitCheck {
2095
+ return escapeHatchResult(
2096
+ providerRoot,
2097
+ "vendor-timestamp-leak",
2098
+ findVendorTimestampLeakFindings(providerRoot),
2099
+ {
2100
+ blockerMessage: "Normalized fixtures carry raw vendor timestamp formats.",
2101
+ remediation:
2102
+ "Convert vendor compact timestamps (yyyymmdd, HHmm, yyyymmddHHmmss) to ISO 8601 (date, time with timezone) at the mapper boundary; fixtures.response must show the normalized form. Add `// @apifuse-allow vendor-timestamp-leak` only when the value is genuinely not a timestamp.",
2103
+ passMessage: "No vendor timestamp formats detected in normalized fixtures.",
2104
+ },
2105
+ );
2106
+ }
2107
+
2108
+ type ObjectRange = {
2109
+ start: number;
2110
+ end: number;
2111
+ };
2112
+
2113
+ type ZObjectLiteral = {
2114
+ objectStart: number;
2115
+ objectEnd: number;
2116
+ callStart: number;
2117
+ };
2118
+
2119
+ type NamedObjectRange = ObjectRange & {
2120
+ name: string;
2121
+ };
2122
+
2123
+ function findVendorKeyLeakFindings(providerRoot: string): SourceFinding[] {
2124
+ const findings: SourceFinding[] = [];
2125
+ const seen = new Set<string>();
2126
+
2127
+ for (const filePath of listNonTestTypeScriptFiles(providerRoot)) {
2128
+ const source = readFileSync(filePath, "utf8");
2129
+ const relPath = toRelativeProviderPath(providerRoot, filePath);
2130
+ const upstreamRanges = findUpstreamMarkedConstRanges(source);
2131
+ for (const zObject of findZObjectLiterals(source)) {
2132
+ if (rangeContainsOffset(upstreamRanges, zObject.callStart)) {
2133
+ continue;
2134
+ }
2135
+ if (!zObjectAppearsPublicOutput(source, zObject)) {
2136
+ continue;
2137
+ }
2138
+ for (const keyFinding of vendorKeyFindingsForObject(source, zObject)) {
2139
+ const key = `${relPath}:${keyFinding.line}:${keyFinding.key}`;
2140
+ if (!seen.has(key)) {
2141
+ seen.add(key);
2142
+ findings.push({ file: relPath, line: keyFinding.line });
2143
+ if (findings.length >= MAX_SOURCE_FINDING_EVIDENCE) {
2144
+ return findings;
2145
+ }
2146
+ }
2147
+ }
2148
+ }
2149
+ }
2150
+
2151
+ return findings;
2152
+ }
2153
+
2154
+ function findZObjectLiterals(source: string): ZObjectLiteral[] {
2155
+ const literals: ZObjectLiteral[] = [];
2156
+ const masked = maskCommentsAndStrings(source);
2157
+ const callPattern = /\bz\s*\.\s*object\s*\(/g;
2158
+ for (let match = callPattern.exec(masked); match !== null; match = callPattern.exec(masked)) {
2159
+ const parenIndex = masked.indexOf("(", match.index);
2160
+ const objectStart = findNextNonWhitespace(masked, parenIndex + 1);
2161
+ if (objectStart === -1 || masked[objectStart] !== "{") {
2162
+ continue;
2163
+ }
2164
+ const objectEnd = findMatchingBracket(masked, objectStart);
2165
+ if (objectEnd === -1) {
2166
+ continue;
2167
+ }
2168
+ literals.push({
2169
+ objectStart,
2170
+ objectEnd,
2171
+ callStart: match.index,
2172
+ });
2173
+ callPattern.lastIndex = objectEnd;
2174
+ }
2175
+ return literals;
2176
+ }
2177
+
2178
+ function zObjectAppearsPublicOutput(source: string, zObject: ZObjectLiteral): boolean {
2179
+ const enclosingConst = findConstValueRangeContaining(source, zObject.callStart);
2180
+ if (enclosingConst && /output|response|result/i.test(enclosingConst.name)) {
2181
+ return true;
2182
+ }
2183
+ const before = source.slice(Math.max(0, zObject.callStart - 160), zObject.callStart);
2184
+ return /(?:^|[\s,{])(?:output|response)\s*:\s*$/.test(before);
2185
+ }
2186
+
2187
+ function vendorKeyFindingsForObject(
2188
+ source: string,
2189
+ zObject: ZObjectLiteral,
2190
+ ): Array<{ key: string; line: number }> {
2191
+ const keys = collectTopLevelObjectKeys(source, zObject.objectStart, zObject.objectEnd);
2192
+ const digitFamilies = new Map<string, Set<string>>();
2193
+ for (const key of keys) {
2194
+ const digitMatch = /^([a-z][a-zA-Z]*)(\d+)[a-z]*$/i.exec(key.name);
2195
+ if (!digitMatch?.[1] || !digitMatch[2]) {
2196
+ continue;
2197
+ }
2198
+ const digits = digitFamilies.get(digitMatch[1]) ?? new Set<string>();
2199
+ digits.add(digitMatch[2]);
2200
+ digitFamilies.set(digitMatch[1], digits);
2201
+ }
2202
+
2203
+ return keys
2204
+ .filter((key) => {
2205
+ if (!/^[a-z][a-zA-Z0-9]*$/.test(key.name)) {
2206
+ return true;
2207
+ }
2208
+ const digitMatch = /^([a-z][a-zA-Z]*)(\d+)[a-z]*$/i.exec(key.name);
2209
+ return digitMatch?.[1] !== undefined && (digitFamilies.get(digitMatch[1])?.size ?? 0) >= 3;
2210
+ })
2211
+ .map((key) => ({ key: key.name, line: offsetToLine(source, key.offset) }));
2212
+ }
2213
+
2214
+ function collectTopLevelObjectKeys(
2215
+ source: string,
2216
+ objectStart: number,
2217
+ objectEnd: number,
2218
+ ): Array<{ name: string; offset: number }> {
2219
+ const keys: Array<{ name: string; offset: number }> = [];
2220
+ const masked = maskCommentsAndStrings(source);
2221
+ let index = objectStart + 1;
2222
+ while (index < objectEnd) {
2223
+ index = skipWhitespaceAndComments(masked, index, objectEnd);
2224
+ if (index >= objectEnd || masked[index] === "}") {
2225
+ break;
2226
+ }
2227
+ const keyStart = index;
2228
+ let key: string | undefined;
2229
+ const quote = source[index];
2230
+ if (quote === '"' || quote === "'") {
2231
+ const endQuote = findStringEnd(source, index);
2232
+ if (endQuote === -1) {
2233
+ break;
2234
+ }
2235
+ key = source.slice(index + 1, endQuote);
2236
+ index = endQuote + 1;
2237
+ } else if (masked[index] === "[") {
2238
+ const computedEnd = findMatchingBracket(masked, index);
2239
+ const literalStart = findNextNonWhitespace(masked, index + 1);
2240
+ if (computedEnd === -1 || literalStart === -1) {
2241
+ break;
2242
+ }
2243
+ const computedQuote = source[literalStart];
2244
+ if (computedQuote === '"' || computedQuote === "'") {
2245
+ const literalEnd = findStringEnd(source, literalStart);
2246
+ const afterLiteral =
2247
+ literalEnd === -1
2248
+ ? -1
2249
+ : skipWhitespaceAndComments(masked, literalEnd + 1, computedEnd);
2250
+ if (literalEnd !== -1 && afterLiteral === computedEnd) {
2251
+ key = source.slice(literalStart + 1, literalEnd);
2252
+ }
2253
+ }
2254
+ index = computedEnd + 1;
2255
+ } else {
2256
+ const idMatch = /^[A-Za-z_$][\w$]*/.exec(masked.slice(index));
2257
+ if (idMatch?.[0]) {
2258
+ key = idMatch[0];
2259
+ index += idMatch[0].length;
2260
+ }
2261
+ }
2262
+ index = skipWhitespaceAndComments(masked, index, objectEnd);
2263
+ if (key && masked[index] === ":") {
2264
+ keys.push({ name: key, offset: keyStart });
2265
+ index = skipObjectValue(masked, index + 1, objectEnd);
2266
+ } else {
2267
+ // Spread-based composition is intentionally not expanded here; this gate
2268
+ // only evaluates keys visible in the object literal.
2269
+ index = skipObjectValue(masked, index, objectEnd);
2270
+ }
2271
+ if (masked[index] === ",") {
2272
+ index += 1;
2273
+ }
2274
+ }
2275
+ return keys;
2276
+ }
2277
+
2278
+ function findVendorTimestampLeakFindings(providerRoot: string): SourceFinding[] {
2279
+ const findings: SourceFinding[] = [];
2280
+ const seen = new Set<string>();
2281
+
2282
+ for (const filePath of listNonTestTypeScriptFiles(providerRoot)) {
2283
+ const source = readFileSync(filePath, "utf8");
2284
+ const relPath = toRelativeProviderPath(providerRoot, filePath);
2285
+ const zObjectRanges = findZObjectLiterals(source).map((zObject) => ({
2286
+ start: zObject.callStart,
2287
+ end: zObject.objectEnd,
2288
+ }));
2289
+ const upstreamRanges = findUpstreamMarkedConstRanges(source);
2290
+ const fixtureRanges = findPropertyObjectRanges(source, "fixtures");
2291
+ const fixtureResponseRanges = [
2292
+ ...findPropertyObjectRanges(source, "response"),
2293
+ ...findPropertyObjectRanges(source, "output"),
2294
+ ].filter((range) => rangeContainedInRanges(fixtureRanges, range));
2295
+
2296
+ for (const range of fixtureResponseRanges) {
2297
+ for (const literal of findStringLiteralsInRange(source, range)) {
2298
+ if (
2299
+ rangeContainsOffset(zObjectRanges, literal.offset) ||
2300
+ rangeContainsOffset(upstreamRanges, literal.offset) ||
2301
+ !isVendorTimestampCandidate(
2302
+ literal.value,
2303
+ propertyKeyForStringLiteral(source, literal.offset),
2304
+ )
2305
+ ) {
2306
+ continue;
2307
+ }
2308
+ const line = offsetToLine(source, literal.offset);
2309
+ const key = `${relPath}:${line}:${literal.value}`;
2310
+ if (!seen.has(key)) {
2311
+ seen.add(key);
2312
+ findings.push({ file: relPath, line });
2313
+ if (findings.length >= MAX_SOURCE_FINDING_EVIDENCE) {
2314
+ return findings;
2315
+ }
2316
+ }
2317
+ }
2318
+ }
2319
+ }
2320
+
2321
+ return findings;
2322
+ }
2323
+
2324
+ function findPropertyObjectRanges(source: string, propertyName: string): ObjectRange[] {
2325
+ const ranges: ObjectRange[] = [];
2326
+ const masked = maskCommentsAndStrings(source);
2327
+ const escaped = propertyName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
2328
+ const pattern = new RegExp(`(?:^|[^\\w$])["']?${escaped}["']?\\s*:`, "g");
2329
+ for (let match = pattern.exec(masked); match !== null; match = pattern.exec(masked)) {
2330
+ const objectStart = findNextNonWhitespace(masked, match.index + match[0].length);
2331
+ if (objectStart === -1 || masked[objectStart] !== "{") {
2332
+ continue;
2333
+ }
2334
+ const objectEnd = findMatchingBracket(masked, objectStart);
2335
+ if (objectEnd === -1) {
2336
+ continue;
2337
+ }
2338
+ ranges.push({ start: objectStart, end: objectEnd });
2339
+ pattern.lastIndex = objectEnd;
2340
+ }
2341
+ return ranges;
2342
+ }
2343
+
2344
+ function findUpstreamMarkedConstRanges(source: string): ObjectRange[] {
2345
+ return findNamedConstValueRanges(source)
2346
+ .filter((range) => /upstream|raw|vendor/i.test(range.name))
2347
+ .map(({ start, end }) => ({ start, end }));
2348
+ }
2349
+
2350
+ function findNamedConstValueRanges(source: string): NamedObjectRange[] {
2351
+ const ranges: NamedObjectRange[] = [];
2352
+ const masked = maskCommentsAndStrings(source);
2353
+ const pattern =
2354
+ /(?:^|\n)[ \t]*(?:export\s+)?(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*(?::[^=\n]+)?\s*=/g;
2355
+ for (let match = pattern.exec(masked); match !== null; match = pattern.exec(masked)) {
2356
+ const name = match[1];
2357
+ if (!name) {
2358
+ continue;
2359
+ }
2360
+ const start = match.index + match[0].length;
2361
+ const expression = balancedValueExpression(masked, start);
2362
+ ranges.push({ name, start, end: start + expression.length });
2363
+ }
2364
+ return ranges;
2365
+ }
2366
+
2367
+ function findConstValueRangeContaining(source: string, offset: number): NamedObjectRange | undefined {
2368
+ return findNamedConstValueRanges(source).find((range) => offset >= range.start && offset <= range.end);
2369
+ }
2370
+
2371
+ function findStringLiteralsInRange(
2372
+ source: string,
2373
+ range: ObjectRange,
2374
+ ): Array<{ value: string; offset: number }> {
2375
+ const literals: Array<{ value: string; offset: number }> = [];
2376
+ let index = range.start;
2377
+ while (index <= range.end) {
2378
+ const quote = source[index];
2379
+ if (quote !== '"' && quote !== "'" && quote !== "`") {
2380
+ index += 1;
2381
+ continue;
2382
+ }
2383
+ const end = findStringEnd(source, index);
2384
+ if (end === -1) {
2385
+ break;
2386
+ }
2387
+ if (quote === "`" && source.slice(index + 1, end).includes("${")) {
2388
+ index = end + 1;
2389
+ continue;
2390
+ }
2391
+ literals.push({ value: source.slice(index + 1, end), offset: index });
2392
+ index = end + 1;
2393
+ }
2394
+ return literals;
2395
+ }
2396
+
2397
+ function isVendorTimestampCandidate(value: string, key: string | undefined): boolean {
2398
+ if (/^\d{8}$/.test(value)) {
2399
+ return isPlausibleCompactDate(value);
2400
+ }
2401
+ if (/^\d{12}$/.test(value)) {
2402
+ return isPlausibleCompactDate(value.slice(0, 8)) && isPlausibleHourMinute(value.slice(8, 12));
2403
+ }
2404
+ if (/^\d{14}$/.test(value)) {
2405
+ const seconds = Number(value.slice(12, 14));
2406
+ return (
2407
+ isPlausibleCompactDate(value.slice(0, 8)) &&
2408
+ isPlausibleHourMinute(value.slice(8, 12)) &&
2409
+ seconds >= 0 &&
2410
+ seconds <= 59
2411
+ );
2412
+ }
2413
+ if (
2414
+ /^\d{4}$/.test(value) &&
2415
+ key !== undefined &&
2416
+ /(?:^|_)at$|At$|time|date|open|close|updated|created/i.test(key)
2417
+ ) {
2418
+ return isPlausibleHourMinute(value);
2419
+ }
2420
+ return false;
2421
+ }
2422
+
2423
+ function isPlausibleCompactDate(value: string): boolean {
2424
+ const year = Number(value.slice(0, 4));
2425
+ const month = Number(value.slice(4, 6));
2426
+ const day = Number(value.slice(6, 8));
2427
+ return year >= 1900 && year <= 2099 && month >= 1 && month <= 12 && day >= 1 && day <= 31;
2428
+ }
2429
+
2430
+ function isPlausibleHourMinute(value: string): boolean {
2431
+ const hour = Number(value.slice(0, 2));
2432
+ const minute = Number(value.slice(2, 4));
2433
+ return hour >= 0 && hour <= 23 && minute >= 0 && minute <= 59;
2434
+ }
2435
+
2436
+ function findNextNonWhitespace(source: string, start: number): number {
2437
+ for (let index = start; index < source.length; index += 1) {
2438
+ if (!/\s/.test(source[index] ?? "")) {
2439
+ return index;
2440
+ }
2441
+ }
2442
+ return -1;
2443
+ }
2444
+
2445
+ function findMatchingBracket(source: string, openIndex: number): number {
2446
+ const open = source[openIndex];
2447
+ const close = open === "{" ? "}" : open === "(" ? ")" : open === "[" ? "]" : undefined;
2448
+ if (!close) {
2449
+ return -1;
2450
+ }
2451
+ let depth = 0;
2452
+ for (let index = openIndex; index < source.length; index += 1) {
2453
+ const char = source[index];
2454
+ if (char === '"' || char === "'" || char === "`") {
2455
+ const stringEnd = findStringEnd(source, index);
2456
+ if (stringEnd === -1) {
2457
+ return -1;
2458
+ }
2459
+ index = stringEnd;
2460
+ continue;
2461
+ }
2462
+ if (char === open) {
2463
+ depth += 1;
2464
+ } else if (char === close) {
2465
+ depth -= 1;
2466
+ if (depth === 0) {
2467
+ return index;
2468
+ }
2469
+ }
2470
+ }
2471
+ return -1;
2472
+ }
2473
+
2474
+ function findStringEnd(source: string, start: number): number {
2475
+ const quote = source[start];
2476
+ for (let index = start + 1; index < source.length; index += 1) {
2477
+ if (source[index] === "\\") {
2478
+ index += 1;
2479
+ continue;
2480
+ }
2481
+ if (source[index] === quote) {
2482
+ return index;
2483
+ }
2484
+ }
2485
+ return -1;
2486
+ }
2487
+
2488
+ function maskCommentsAndStrings(source: string): string {
2489
+ const chars = source.split("");
2490
+ for (let index = 0; index < source.length; index += 1) {
2491
+ if (source.startsWith("//", index)) {
2492
+ const bodyStart = index + 2;
2493
+ const newline = source.indexOf("\n", bodyStart);
2494
+ const end = newline === -1 ? source.length : newline;
2495
+ for (let bodyIndex = bodyStart; bodyIndex < end; bodyIndex += 1) {
2496
+ chars[bodyIndex] = " ";
2497
+ }
2498
+ index = end;
2499
+ continue;
2500
+ }
2501
+ if (source.startsWith("/*", index)) {
2502
+ const bodyStart = index + 2;
2503
+ const close = source.indexOf("*/", bodyStart);
2504
+ const end = close === -1 ? source.length : close;
2505
+ for (let bodyIndex = bodyStart; bodyIndex < end; bodyIndex += 1) {
2506
+ if (chars[bodyIndex] !== "\n") {
2507
+ chars[bodyIndex] = " ";
2508
+ }
2509
+ }
2510
+ index = close === -1 ? source.length : close + 1;
2511
+ continue;
2512
+ }
2513
+ const quote = source[index];
2514
+ if (quote !== '"' && quote !== "'" && quote !== "`") {
2515
+ continue;
2516
+ }
2517
+ const end = findStringEnd(source, index);
2518
+ if (end === -1) {
2519
+ break;
2520
+ }
2521
+ // Preserve quoted property keys ("response": ...) so range/key scanners
2522
+ // can still match them; only string VALUES are blanked.
2523
+ let probe = end + 1;
2524
+ while (probe < source.length && /\s/.test(source[probe] ?? "")) {
2525
+ probe += 1;
2526
+ }
2527
+ if (source[probe] !== ":") {
2528
+ for (let bodyIndex = index + 1; bodyIndex < end; bodyIndex += 1) {
2529
+ if (chars[bodyIndex] !== "\n") {
2530
+ chars[bodyIndex] = " ";
2531
+ }
2532
+ }
2533
+ }
2534
+ index = end;
2535
+ }
2536
+ return chars.join("");
2537
+ }
2538
+
2539
+ function skipWhitespaceAndComments(source: string, start: number, end: number): number {
2540
+ let index = start;
2541
+ while (index < end) {
2542
+ if (/\s/.test(source[index] ?? "")) {
2543
+ index += 1;
2544
+ continue;
2545
+ }
2546
+ if (source.startsWith("//", index)) {
2547
+ const newline = source.indexOf("\n", index + 2);
2548
+ index = newline === -1 ? end : newline + 1;
2549
+ continue;
2550
+ }
2551
+ if (source.startsWith("/*", index)) {
2552
+ const close = source.indexOf("*/", index + 2);
2553
+ index = close === -1 ? end : close + 2;
2554
+ continue;
2555
+ }
2556
+ break;
2557
+ }
2558
+ return index;
2559
+ }
2560
+
2561
+ function skipObjectValue(source: string, start: number, end: number): number {
2562
+ let index = start;
2563
+ while (index < end) {
2564
+ const char = source[index];
2565
+ if (char === '"' || char === "'" || char === "`") {
2566
+ const stringEnd = findStringEnd(source, index);
2567
+ if (stringEnd === -1) {
2568
+ return end;
2569
+ }
2570
+ index = stringEnd + 1;
2571
+ continue;
2572
+ }
2573
+ if (char === "{" || char === "(" || char === "[") {
2574
+ const close = findMatchingBracket(source, index);
2575
+ if (close === -1) {
2576
+ return end;
2577
+ }
2578
+ index = close + 1;
2579
+ continue;
2580
+ }
2581
+ if (char === "," || char === "}") {
2582
+ return index;
2583
+ }
2584
+ index += 1;
2585
+ }
2586
+ return index;
2587
+ }
2588
+
2589
+ function rangeContainsOffset(ranges: readonly ObjectRange[], offset: number): boolean {
2590
+ return ranges.some((range) => offset >= range.start && offset <= range.end);
2591
+ }
2592
+
2593
+ function rangeContainedInRanges(ranges: readonly ObjectRange[], candidate: ObjectRange): boolean {
2594
+ return ranges.some((range) => candidate.start >= range.start && candidate.end <= range.end);
2595
+ }
2596
+
2597
+ function propertyKeyForStringLiteral(source: string, literalOffset: number): string | undefined {
2598
+ const masked = maskCommentsAndStrings(source);
2599
+ let index = skipWhitespaceBackward(masked, literalOffset - 1);
2600
+ if (masked[index] !== ":") {
2601
+ return undefined;
2602
+ }
2603
+ index = skipWhitespaceBackward(masked, index - 1);
2604
+ if (index < 0) {
2605
+ return undefined;
2606
+ }
2607
+ if (source[index] === '"' || source[index] === "'") {
2608
+ const quote = source[index];
2609
+ let start = index - 1;
2610
+ while (start >= 0) {
2611
+ if (source[start] === quote && source[start - 1] !== "\\") {
2612
+ return source.slice(start + 1, index);
2613
+ }
2614
+ start -= 1;
2615
+ }
2616
+ return undefined;
2617
+ }
2618
+ const keyMatch = /[A-Za-z_$][\w$]*$/.exec(masked.slice(0, index + 1));
2619
+ return keyMatch?.[0];
2620
+ }
2621
+
2622
+ function skipWhitespaceBackward(source: string, start: number): number {
2623
+ let index = start;
2624
+ while (index >= 0 && /\s/.test(source[index] ?? "")) {
2625
+ index -= 1;
2626
+ }
2627
+ return index;
2628
+ }
2629
+
1976
2630
  function scoreHealthCoverage(provider: ProviderDefinition): SubmitCheck {
1977
2631
  const operations = Object.entries(provider.operations);
1978
2632
  const missing: string[] = [];
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.1.0-beta.21",
2
+ "version": "2.1.0-beta.22",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",