@apifuse/provider-sdk 2.1.0-beta.3 → 2.1.0-beta.5
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 +187 -8
- package/CHANGELOG.md +13 -1
- package/README.md +40 -18
- package/SUBMISSION.md +4 -4
- package/bin/apifuse-dev.ts +12 -5
- package/bin/apifuse-pack-check.ts +9 -2
- package/bin/apifuse-pack-smoke.ts +127 -6
- package/bin/apifuse-perf.ts +76 -31
- package/bin/apifuse-record.ts +148 -94
- package/bin/apifuse-submit-check.ts +243 -7
- package/bin/apifuse.ts +1 -1
- package/package.json +17 -8
- package/src/choice-token.ts +164 -0
- package/src/cli/commands.ts +4 -7
- package/src/cli/create.ts +180 -51
- 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 +42 -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 +23 -0
- package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -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 +1206 -9
- package/src/define.ts +1620 -106
- package/src/errors.ts +12 -0
- package/src/i18n/catalog.ts +121 -0
- package/src/i18n/index.ts +2 -0
- package/src/i18n/keys.ts +64 -0
- package/src/index.ts +149 -8
- package/src/lint.ts +306 -51
- package/src/observability.ts +41 -0
- package/src/provider.ts +60 -3
- package/src/public-schema-field-lint.ts +237 -0
- package/src/runtime/auth-flow.ts +7 -0
- package/src/runtime/browser.ts +77 -21
- package/src/runtime/cache.ts +582 -0
- package/src/runtime/executor.ts +13 -1
- 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/request-options.ts +66 -0
- package/src/runtime/state.ts +76 -0
- package/src/runtime/stealth.ts +1145 -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 +816 -58
- package/src/server/types.ts +35 -0
- package/src/stream.ts +210 -0
- package/src/testing/run.ts +17 -4
- package/src/types.ts +876 -53
- 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"
|
|
@@ -17,10 +23,15 @@ type ProviderAuthLike = {
|
|
|
17
23
|
};
|
|
18
24
|
};
|
|
19
25
|
|
|
26
|
+
type ProviderContractMetaLike = {
|
|
27
|
+
publicSchemaFieldNames?: "normalized";
|
|
28
|
+
};
|
|
29
|
+
|
|
20
30
|
type SchemaLike = ZodType & {
|
|
21
31
|
description?: string;
|
|
22
32
|
def?: Record<string, unknown>;
|
|
23
33
|
_def?: Record<string, unknown>;
|
|
34
|
+
meta?: () => Record<string, unknown> | undefined;
|
|
24
35
|
shape?: Record<string, SchemaLike> | (() => Record<string, SchemaLike>);
|
|
25
36
|
element?: SchemaLike;
|
|
26
37
|
items?: SchemaLike[];
|
|
@@ -43,7 +54,7 @@ export interface LintDiagnostic {
|
|
|
43
54
|
|
|
44
55
|
function lintAllowedHosts(
|
|
45
56
|
providerId: string | undefined,
|
|
46
|
-
allowedHosts: string[] | undefined,
|
|
57
|
+
allowedHosts: readonly string[] | undefined,
|
|
47
58
|
): LintDiagnostic[] {
|
|
48
59
|
const prefix = providerId ? `Provider "${providerId}"` : "Provider";
|
|
49
60
|
|
|
@@ -103,7 +114,7 @@ function lintReviewed(
|
|
|
103
114
|
];
|
|
104
115
|
}
|
|
105
116
|
|
|
106
|
-
function hasReusableSecretKeys(keys: string[] | undefined): boolean {
|
|
117
|
+
function hasReusableSecretKeys(keys: readonly string[] | undefined): boolean {
|
|
107
118
|
if (!keys) {
|
|
108
119
|
return false;
|
|
109
120
|
}
|
|
@@ -143,12 +154,12 @@ function lintAuthModel(provider: {
|
|
|
143
154
|
id?: string;
|
|
144
155
|
auth?: ProviderAuthLike;
|
|
145
156
|
credential?: {
|
|
146
|
-
keys?: string[];
|
|
157
|
+
keys?: readonly string[];
|
|
147
158
|
storesReusableSecret?: boolean;
|
|
148
159
|
justification?: string;
|
|
149
160
|
};
|
|
150
161
|
context?: {
|
|
151
|
-
keys?: string[];
|
|
162
|
+
keys?: readonly string[];
|
|
152
163
|
};
|
|
153
164
|
authFlowSource?: string;
|
|
154
165
|
}): LintDiagnostic[] {
|
|
@@ -359,7 +370,60 @@ function getChildSchemas(
|
|
|
359
370
|
}));
|
|
360
371
|
}
|
|
361
372
|
|
|
362
|
-
function
|
|
373
|
+
function uniqueFields(fields: string[]): string[] {
|
|
374
|
+
return Array.from(new Set(fields));
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
function isSensitiveSchema(schema: unknown): boolean {
|
|
378
|
+
if (!schema || typeof schema !== "object" || !("meta" in schema)) {
|
|
379
|
+
return false;
|
|
380
|
+
}
|
|
381
|
+
const meta = schema.meta;
|
|
382
|
+
if (typeof meta !== "function") return false;
|
|
383
|
+
const metadata = meta.call(schema);
|
|
384
|
+
return (
|
|
385
|
+
!!metadata &&
|
|
386
|
+
typeof metadata === "object" &&
|
|
387
|
+
Reflect.get(metadata, APIFUSE_SENSITIVE_META_KEY) === true
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
function getSchemaMetadata(schema: SchemaLike): Record<string, unknown> {
|
|
392
|
+
return schema.meta?.() ?? {};
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
function getSchemaDescriptionKey(schema: SchemaLike): string | undefined {
|
|
396
|
+
const value = Reflect.get(
|
|
397
|
+
getSchemaMetadata(schema),
|
|
398
|
+
APIFUSE_DESCRIPTION_KEY_META_KEY,
|
|
399
|
+
);
|
|
400
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const SENSITIVE_FIELD_NAMES = new Set([
|
|
404
|
+
"apikey",
|
|
405
|
+
"authorization",
|
|
406
|
+
"cookie",
|
|
407
|
+
"secret",
|
|
408
|
+
"secrets",
|
|
409
|
+
"token",
|
|
410
|
+
"accesstoken",
|
|
411
|
+
"refreshtoken",
|
|
412
|
+
"password",
|
|
413
|
+
"passwd",
|
|
414
|
+
"otp",
|
|
415
|
+
"otpcode",
|
|
416
|
+
"phone",
|
|
417
|
+
"phonenumber",
|
|
418
|
+
"paymenturl",
|
|
419
|
+
]);
|
|
420
|
+
|
|
421
|
+
function isSensitiveFieldName(name: string): boolean {
|
|
422
|
+
const normalized = name.toLowerCase().replace(/[-_\s]/g, "");
|
|
423
|
+
return SENSITIVE_FIELD_NAMES.has(normalized);
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
function collectUnmarkedSensitiveFields(
|
|
363
427
|
schema: unknown,
|
|
364
428
|
basePath: string,
|
|
365
429
|
seen = new Set<SchemaLike>(),
|
|
@@ -367,13 +431,80 @@ function collectMissingDescriptions(
|
|
|
367
431
|
if (!isSchema(schema) || seen.has(schema)) {
|
|
368
432
|
return [];
|
|
369
433
|
}
|
|
434
|
+
seen.add(schema);
|
|
435
|
+
const out: string[] = [];
|
|
436
|
+
for (const [key, child] of Object.entries(getObjectShape(schema))) {
|
|
437
|
+
const childPath = basePath ? `${basePath}.${key}` : key;
|
|
438
|
+
if (isSensitiveFieldName(key) && !isSensitiveSchema(child)) {
|
|
439
|
+
out.push(childPath);
|
|
440
|
+
}
|
|
441
|
+
out.push(...collectUnmarkedSensitiveFields(child, childPath, seen));
|
|
442
|
+
}
|
|
443
|
+
for (const child of getChildSchemas(schema)) {
|
|
444
|
+
if (Object.hasOwn(getObjectShape(schema), child.key)) continue;
|
|
445
|
+
const isWrapperNode = [
|
|
446
|
+
"unwrap",
|
|
447
|
+
"innerType",
|
|
448
|
+
"sourceType",
|
|
449
|
+
"schema",
|
|
450
|
+
"type",
|
|
451
|
+
"in",
|
|
452
|
+
"out",
|
|
453
|
+
"option",
|
|
454
|
+
"pipe",
|
|
455
|
+
"payload",
|
|
456
|
+
"item",
|
|
457
|
+
"rest",
|
|
458
|
+
"catchall",
|
|
459
|
+
"keyType",
|
|
460
|
+
"valueType",
|
|
461
|
+
].includes(child.key);
|
|
462
|
+
const childPath =
|
|
463
|
+
child.key === "element" || child.key.startsWith("element.")
|
|
464
|
+
? `${basePath}[]`
|
|
465
|
+
: isWrapperNode || child.key.startsWith("pipe.")
|
|
466
|
+
? basePath
|
|
467
|
+
: basePath
|
|
468
|
+
? `${basePath}.${child.key}`
|
|
469
|
+
: child.key;
|
|
470
|
+
out.push(...collectUnmarkedSensitiveFields(child.schema, childPath, seen));
|
|
471
|
+
}
|
|
472
|
+
return out;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
function collectSchemaDescriptionKeyDiagnostics(
|
|
476
|
+
schema: unknown,
|
|
477
|
+
basePath: string,
|
|
478
|
+
seen = new Set<SchemaLike>(),
|
|
479
|
+
requireCurrentDescription = true,
|
|
480
|
+
): LintDiagnostic[] {
|
|
481
|
+
if (!isSchema(schema) || seen.has(schema)) {
|
|
482
|
+
return [];
|
|
483
|
+
}
|
|
370
484
|
|
|
371
485
|
seen.add(schema);
|
|
372
|
-
const
|
|
486
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
373
487
|
const currentPath = basePath || "schema";
|
|
488
|
+
const hasDescriptionKey = getSchemaDescriptionKey(schema) !== undefined;
|
|
374
489
|
|
|
375
|
-
if (
|
|
376
|
-
|
|
490
|
+
if (schema.description && !hasDescriptionKey) {
|
|
491
|
+
diagnostics.push({
|
|
492
|
+
rule: "schema-description-raw-prose",
|
|
493
|
+
level: "error",
|
|
494
|
+
field: currentPath,
|
|
495
|
+
message: `Schema field "${currentPath}" must use .describeKey() or describeKey() instead of raw static prose.`,
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
if (requireCurrentDescription && !hasDescriptionKey) {
|
|
500
|
+
diagnostics.push({
|
|
501
|
+
rule: "schema-description-key-required",
|
|
502
|
+
level: "error",
|
|
503
|
+
field: currentPath,
|
|
504
|
+
message: schema.description
|
|
505
|
+
? `Schema field "${currentPath}" has a raw description but is missing .describeKey() or describeKey() metadata.`
|
|
506
|
+
: `Schema field "${currentPath}" is missing .describeKey() or describeKey() metadata.`,
|
|
507
|
+
});
|
|
377
508
|
}
|
|
378
509
|
|
|
379
510
|
for (const child of getChildSchemas(schema)) {
|
|
@@ -383,30 +514,42 @@ function collectMissingDescriptions(
|
|
|
383
514
|
"sourceType",
|
|
384
515
|
"schema",
|
|
385
516
|
"type",
|
|
517
|
+
"in",
|
|
518
|
+
"out",
|
|
386
519
|
"option",
|
|
387
520
|
"pipe",
|
|
388
521
|
"payload",
|
|
389
522
|
"item",
|
|
390
523
|
"rest",
|
|
391
524
|
"catchall",
|
|
525
|
+
"keyType",
|
|
526
|
+
"valueType",
|
|
392
527
|
].includes(child.key);
|
|
528
|
+
const isStructuralNode =
|
|
529
|
+
isWrapperNode ||
|
|
530
|
+
child.key.startsWith("pipe.") ||
|
|
531
|
+
child.key === "element" ||
|
|
532
|
+
child.key.startsWith("element.");
|
|
393
533
|
const childPath = isWrapperNode
|
|
394
534
|
? currentPath
|
|
395
535
|
: currentPath === "schema"
|
|
396
536
|
? child.key
|
|
397
537
|
: /^\d+$/.test(child.key)
|
|
398
538
|
? `${currentPath}[${child.key}]`
|
|
399
|
-
: child.key.startsWith("element")
|
|
539
|
+
: child.key === "element" || child.key.startsWith("element.")
|
|
400
540
|
? `${currentPath}[]`
|
|
401
541
|
: `${currentPath}.${child.key}`;
|
|
402
|
-
|
|
542
|
+
diagnostics.push(
|
|
543
|
+
...collectSchemaDescriptionKeyDiagnostics(
|
|
544
|
+
child.schema,
|
|
545
|
+
childPath,
|
|
546
|
+
seen,
|
|
547
|
+
!isStructuralNode,
|
|
548
|
+
),
|
|
549
|
+
);
|
|
403
550
|
}
|
|
404
551
|
|
|
405
|
-
return
|
|
406
|
-
}
|
|
407
|
-
|
|
408
|
-
function uniqueFields(fields: string[]): string[] {
|
|
409
|
-
return Array.from(new Set(fields));
|
|
552
|
+
return diagnostics;
|
|
410
553
|
}
|
|
411
554
|
|
|
412
555
|
function isComplexSchema(
|
|
@@ -438,60 +581,124 @@ function hasBidirectionalFixtures(fixtures: unknown): boolean {
|
|
|
438
581
|
return "request" in fixtures && "response" in fixtures;
|
|
439
582
|
}
|
|
440
583
|
|
|
584
|
+
function getOperationSource(operation: {
|
|
585
|
+
handler?: unknown;
|
|
586
|
+
source?: string;
|
|
587
|
+
}): string {
|
|
588
|
+
if (operation.source) {
|
|
589
|
+
return operation.source;
|
|
590
|
+
}
|
|
591
|
+
return typeof operation.handler === "function"
|
|
592
|
+
? operation.handler.toString()
|
|
593
|
+
: "";
|
|
594
|
+
}
|
|
595
|
+
|
|
596
|
+
function lintStealthTransportUsage(provider: {
|
|
597
|
+
id?: string;
|
|
598
|
+
stealth?: unknown;
|
|
599
|
+
operations?: Record<string, { handler?: unknown; source?: string }>;
|
|
600
|
+
}): LintDiagnostic[] {
|
|
601
|
+
if (provider.stealth || !provider.operations) {
|
|
602
|
+
return [];
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
const providerLabel = provider.id ? `Provider "${provider.id}"` : "Provider";
|
|
606
|
+
return Object.entries(provider.operations).flatMap(
|
|
607
|
+
([operationKey, operation]) => {
|
|
608
|
+
const source = getOperationSource(operation);
|
|
609
|
+
if (!/\bctx\.stealth\b/.test(source)) {
|
|
610
|
+
return [];
|
|
611
|
+
}
|
|
612
|
+
return [
|
|
613
|
+
{
|
|
614
|
+
rule: "stealth-config-required",
|
|
615
|
+
level: "error" as const,
|
|
616
|
+
field: `operations.${operationKey}`,
|
|
617
|
+
message: `${providerLabel} operation "${operationKey}" uses ctx.stealth but provider.stealth is not declared.`,
|
|
618
|
+
},
|
|
619
|
+
];
|
|
620
|
+
},
|
|
621
|
+
);
|
|
622
|
+
}
|
|
623
|
+
|
|
441
624
|
export function lintOperation(op: {
|
|
442
|
-
description
|
|
625
|
+
description?: string;
|
|
626
|
+
descriptionKey?: string;
|
|
627
|
+
whenToUse?: readonly string[];
|
|
628
|
+
whenToUseKeys?: readonly string[];
|
|
629
|
+
whenNotToUse?: readonly string[];
|
|
630
|
+
whenNotToUseKeys?: readonly string[];
|
|
443
631
|
input: unknown;
|
|
444
632
|
output: unknown;
|
|
445
633
|
fixtures?: unknown;
|
|
446
|
-
inputExamples?: unknown[];
|
|
634
|
+
inputExamples?: readonly unknown[];
|
|
447
635
|
derivations?: Record<string, string>;
|
|
448
636
|
}): LintDiagnostic[] {
|
|
449
637
|
const diagnostics: LintDiagnostic[] = [];
|
|
450
638
|
const description = op.description ?? "";
|
|
639
|
+
const hasDescriptionKey =
|
|
640
|
+
typeof op.descriptionKey === "string" && op.descriptionKey.length > 0;
|
|
451
641
|
|
|
452
|
-
if (description.length
|
|
642
|
+
if (description.trim().length > 0 && !hasDescriptionKey) {
|
|
453
643
|
diagnostics.push({
|
|
454
|
-
rule: "description-
|
|
644
|
+
rule: "operation-description-raw-prose",
|
|
455
645
|
level: "error",
|
|
456
646
|
field: "description",
|
|
457
|
-
message:
|
|
647
|
+
message:
|
|
648
|
+
"Operation description must use descriptionKey instead of raw static prose.",
|
|
458
649
|
});
|
|
459
650
|
}
|
|
460
651
|
|
|
461
|
-
|
|
462
|
-
if (
|
|
463
|
-
!(lowerDescription.includes("use") && lowerDescription.includes("when"))
|
|
464
|
-
) {
|
|
652
|
+
if (!hasDescriptionKey && description.length < 150) {
|
|
465
653
|
diagnostics.push({
|
|
466
|
-
rule: "description-
|
|
467
|
-
level: "
|
|
654
|
+
rule: "description-min-length",
|
|
655
|
+
level: "error",
|
|
468
656
|
field: "description",
|
|
469
|
-
message:
|
|
657
|
+
message: "Operation description must be at least 150 characters.",
|
|
470
658
|
});
|
|
471
659
|
}
|
|
472
660
|
|
|
473
|
-
|
|
474
|
-
collectMissingDescriptions(op.input, "input"),
|
|
475
|
-
)) {
|
|
661
|
+
if ((op.whenToUse?.length ?? 0) > 0 && !(op.whenToUseKeys?.length ?? 0)) {
|
|
476
662
|
diagnostics.push({
|
|
477
|
-
rule: "
|
|
663
|
+
rule: "operation-when-to-use-raw-prose",
|
|
478
664
|
level: "error",
|
|
479
|
-
field,
|
|
480
|
-
message:
|
|
665
|
+
field: "whenToUse",
|
|
666
|
+
message:
|
|
667
|
+
"Operation whenToUse must use whenToUseKeys instead of raw static prose.",
|
|
481
668
|
});
|
|
482
669
|
}
|
|
483
670
|
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
671
|
+
if (
|
|
672
|
+
(op.whenNotToUse?.length ?? 0) > 0 &&
|
|
673
|
+
!(op.whenNotToUseKeys?.length ?? 0)
|
|
674
|
+
) {
|
|
487
675
|
diagnostics.push({
|
|
488
|
-
rule: "
|
|
676
|
+
rule: "operation-when-not-to-use-raw-prose",
|
|
489
677
|
level: "error",
|
|
490
|
-
field,
|
|
491
|
-
message:
|
|
678
|
+
field: "whenNotToUse",
|
|
679
|
+
message:
|
|
680
|
+
"Operation whenNotToUse must use whenNotToUseKeys instead of raw static prose.",
|
|
492
681
|
});
|
|
493
682
|
}
|
|
494
683
|
|
|
684
|
+
const lowerDescription = description.toLowerCase();
|
|
685
|
+
if (
|
|
686
|
+
!hasDescriptionKey &&
|
|
687
|
+
!(lowerDescription.includes("use") && lowerDescription.includes("when"))
|
|
688
|
+
) {
|
|
689
|
+
diagnostics.push({
|
|
690
|
+
rule: "description-has-when-clause",
|
|
691
|
+
level: "warn",
|
|
692
|
+
field: "description",
|
|
693
|
+
message: 'Operation description should include both "use" and "when".',
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
diagnostics.push(
|
|
698
|
+
...collectSchemaDescriptionKeyDiagnostics(op.input, "input"),
|
|
699
|
+
...collectSchemaDescriptionKeyDiagnostics(op.output, "output"),
|
|
700
|
+
);
|
|
701
|
+
|
|
495
702
|
if (!hasBidirectionalFixtures(op.fixtures)) {
|
|
496
703
|
diagnostics.push({
|
|
497
704
|
rule: "fixtures-both-directions",
|
|
@@ -511,39 +718,73 @@ export function lintOperation(op: {
|
|
|
511
718
|
});
|
|
512
719
|
}
|
|
513
720
|
|
|
721
|
+
for (const field of uniqueFields(
|
|
722
|
+
collectUnmarkedSensitiveFields(op.input, "input"),
|
|
723
|
+
)) {
|
|
724
|
+
diagnostics.push({
|
|
725
|
+
rule: "sensitive-field-unmarked",
|
|
726
|
+
level: "warn",
|
|
727
|
+
field,
|
|
728
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
|
|
729
|
+
});
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
for (const field of uniqueFields(
|
|
733
|
+
collectUnmarkedSensitiveFields(op.output, "output"),
|
|
734
|
+
)) {
|
|
735
|
+
diagnostics.push({
|
|
736
|
+
rule: "sensitive-field-unmarked",
|
|
737
|
+
level: "warn",
|
|
738
|
+
field,
|
|
739
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*(), field(..., { sensitive: true }), or sensitive(...).`,
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
|
|
514
743
|
return diagnostics;
|
|
515
744
|
}
|
|
516
745
|
|
|
517
746
|
export function lintProvider(provider: {
|
|
518
747
|
id?: string;
|
|
519
|
-
allowedHosts?: string[];
|
|
748
|
+
allowedHosts?: readonly string[];
|
|
749
|
+
stealth?: unknown;
|
|
520
750
|
auth?: ProviderAuthLike;
|
|
521
751
|
credential?: {
|
|
522
|
-
keys?: string[];
|
|
752
|
+
keys?: readonly string[];
|
|
523
753
|
storesReusableSecret?: boolean;
|
|
524
754
|
justification?: string;
|
|
525
755
|
};
|
|
526
756
|
context?: {
|
|
527
|
-
keys?: string[];
|
|
757
|
+
keys?: readonly string[];
|
|
528
758
|
};
|
|
529
759
|
authFlowSource?: string;
|
|
530
760
|
operations?: Record<
|
|
531
761
|
string,
|
|
532
762
|
{
|
|
533
763
|
description?: string;
|
|
764
|
+
descriptionKey?: string;
|
|
765
|
+
whenToUse?: readonly string[];
|
|
766
|
+
whenToUseKeys?: readonly string[];
|
|
767
|
+
whenNotToUse?: readonly string[];
|
|
768
|
+
whenNotToUseKeys?: readonly string[];
|
|
534
769
|
input: unknown;
|
|
535
770
|
output: unknown;
|
|
536
771
|
fixtures?: unknown;
|
|
537
|
-
inputExamples?: unknown[];
|
|
772
|
+
inputExamples?: readonly unknown[];
|
|
538
773
|
derivations?: Record<string, string>;
|
|
774
|
+
handler?: unknown;
|
|
775
|
+
source?: string;
|
|
539
776
|
}
|
|
540
777
|
>;
|
|
778
|
+
meta?: {
|
|
779
|
+
contract?: ProviderContractMetaLike;
|
|
780
|
+
};
|
|
541
781
|
reviewed?: string;
|
|
542
782
|
}): LintDiagnostic[] {
|
|
543
783
|
const diagnostics: LintDiagnostic[] = [
|
|
544
784
|
...lintAllowedHosts(provider.id, provider.allowedHosts),
|
|
545
785
|
...lintReviewed(provider.id, provider.reviewed),
|
|
546
786
|
...lintAuthModel(provider),
|
|
787
|
+
...lintStealthTransportUsage(provider),
|
|
547
788
|
];
|
|
548
789
|
|
|
549
790
|
if (!provider.operations) {
|
|
@@ -553,14 +794,28 @@ export function lintProvider(provider: {
|
|
|
553
794
|
diagnostics.push(
|
|
554
795
|
...Object.entries(provider.operations).flatMap(
|
|
555
796
|
([operationKey, operation]) =>
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
797
|
+
[
|
|
798
|
+
...lintOperation({
|
|
799
|
+
description: operation.description ?? "",
|
|
800
|
+
descriptionKey: operation.descriptionKey,
|
|
801
|
+
whenToUse: operation.whenToUse,
|
|
802
|
+
whenToUseKeys: operation.whenToUseKeys,
|
|
803
|
+
whenNotToUse: operation.whenNotToUse,
|
|
804
|
+
whenNotToUseKeys: operation.whenNotToUseKeys,
|
|
805
|
+
input: operation.input,
|
|
806
|
+
output: operation.output,
|
|
807
|
+
fixtures: operation.fixtures,
|
|
808
|
+
inputExamples: operation.inputExamples,
|
|
809
|
+
derivations: operation.derivations,
|
|
810
|
+
}),
|
|
811
|
+
...lintPublicSchemaFieldNames(
|
|
812
|
+
provider.id,
|
|
813
|
+
operationKey,
|
|
814
|
+
operation.input,
|
|
815
|
+
operation.output,
|
|
816
|
+
provider.meta?.contract?.publicSchemaFieldNames === "normalized",
|
|
817
|
+
),
|
|
818
|
+
].map((diagnostic) => ({
|
|
564
819
|
...diagnostic,
|
|
565
820
|
field: diagnostic.field
|
|
566
821
|
? `operations.${operationKey}.${diagnostic.field}`
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export const PROVIDER_OBSERVABILITY_TAXONOMY_VERSION = "2026-05-26";
|
|
2
|
+
|
|
3
|
+
export const PROVIDER_ERROR_CATEGORIES = [
|
|
4
|
+
"ok",
|
|
5
|
+
"timeout",
|
|
6
|
+
"network",
|
|
7
|
+
"upstream_http",
|
|
8
|
+
"upstream_rate_limited",
|
|
9
|
+
"upstream_auth",
|
|
10
|
+
"upstream_schema_drift",
|
|
11
|
+
"proxy_pool",
|
|
12
|
+
"anti_bot_blocked",
|
|
13
|
+
"credential_expired",
|
|
14
|
+
"credential_unavailable",
|
|
15
|
+
"input_validation",
|
|
16
|
+
"output_validation",
|
|
17
|
+
"provider_error",
|
|
18
|
+
"internal_error",
|
|
19
|
+
"unclassified",
|
|
20
|
+
] as const;
|
|
21
|
+
|
|
22
|
+
export type ProviderErrorCategory = (typeof PROVIDER_ERROR_CATEGORIES)[number];
|
|
23
|
+
|
|
24
|
+
export function categoryForStatus(status: number): ProviderErrorCategory {
|
|
25
|
+
if (status >= 200 && status < 400) return "ok";
|
|
26
|
+
if (status === 408 || status === 504) return "timeout";
|
|
27
|
+
if (status === 429) return "upstream_rate_limited";
|
|
28
|
+
if (status === 401 || status === 403) return "upstream_auth";
|
|
29
|
+
if (status >= 400) return "upstream_http";
|
|
30
|
+
return "unclassified";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function isRetryableCategory(category: ProviderErrorCategory): boolean {
|
|
34
|
+
return (
|
|
35
|
+
category === "timeout" ||
|
|
36
|
+
category === "network" ||
|
|
37
|
+
category === "upstream_rate_limited" ||
|
|
38
|
+
category === "upstream_http" ||
|
|
39
|
+
category === "proxy_pool"
|
|
40
|
+
);
|
|
41
|
+
}
|
package/src/provider.ts
CHANGED
|
@@ -1,13 +1,70 @@
|
|
|
1
|
-
export { z } from "zod";
|
|
2
|
-
|
|
3
1
|
export { createFormCeremony } from "./ceremonies";
|
|
4
|
-
export {
|
|
2
|
+
export {
|
|
3
|
+
assertFreshProviderChoiceIssuedAt,
|
|
4
|
+
createProviderChoiceToken,
|
|
5
|
+
ProviderChoiceTokenError,
|
|
6
|
+
type ProviderChoiceTokenErrorReason,
|
|
7
|
+
type ProviderChoiceTokenPayload,
|
|
8
|
+
parseProviderChoiceToken,
|
|
9
|
+
} from "./choice-token";
|
|
10
|
+
export {
|
|
11
|
+
defineHealthJourney,
|
|
12
|
+
defineOperation,
|
|
13
|
+
defineProvider,
|
|
14
|
+
defineSmsOtpMatcher,
|
|
15
|
+
every,
|
|
16
|
+
} from "./define";
|
|
5
17
|
export { AuthError, ProviderError, ValidationError } from "./errors";
|
|
18
|
+
export { providerLocaleKey, qualifyProviderLocaleKey } from "./i18n";
|
|
19
|
+
export {
|
|
20
|
+
APIFUSE_DESCRIPTION_KEY_META_KEY,
|
|
21
|
+
APIFUSE_REDACTION_MARKER,
|
|
22
|
+
APIFUSE_SENSITIVE_KIND_META_KEY,
|
|
23
|
+
APIFUSE_SENSITIVE_META_KEY,
|
|
24
|
+
collectSensitivePaths,
|
|
25
|
+
describeKey,
|
|
26
|
+
field,
|
|
27
|
+
fields,
|
|
28
|
+
isSensitiveSchema,
|
|
29
|
+
redactPayload,
|
|
30
|
+
type SensitiveFieldKind,
|
|
31
|
+
type SensitiveFieldOptions,
|
|
32
|
+
type SensitivePath,
|
|
33
|
+
sensitive,
|
|
34
|
+
z,
|
|
35
|
+
} from "./schema";
|
|
6
36
|
export type {
|
|
7
37
|
FlowContext,
|
|
38
|
+
HealthJourneyDefinition,
|
|
39
|
+
HealthJourneyEventContext,
|
|
40
|
+
HealthJourneyManualTriggerPolicy,
|
|
41
|
+
HealthJourneyRunContext,
|
|
42
|
+
HealthJourneyRunResult,
|
|
43
|
+
HttpRetryOptions,
|
|
44
|
+
HttpRetrySummary,
|
|
8
45
|
InferSchemaOutput,
|
|
9
46
|
OperationDefinition,
|
|
47
|
+
OperationObservabilityConfig,
|
|
48
|
+
OperationObservabilitySensitiveConfig,
|
|
49
|
+
OperationSensitivePath,
|
|
10
50
|
ProviderContext,
|
|
51
|
+
ProviderLocaleKeyInput,
|
|
52
|
+
ProviderProxyPolicy,
|
|
53
|
+
ProviderRuntimeState,
|
|
54
|
+
ProviderStateDurationString,
|
|
55
|
+
ProviderStateNamespace,
|
|
11
56
|
SchemaLike,
|
|
57
|
+
SmsOtpMatcherDefinition,
|
|
12
58
|
StandardSchemaV1,
|
|
59
|
+
StateCasResult,
|
|
60
|
+
StateNamespaceOptions,
|
|
61
|
+
StateValue,
|
|
62
|
+
StateWriteOptions,
|
|
63
|
+
} from "./types";
|
|
64
|
+
export {
|
|
65
|
+
HttpRetryAfterPolicy,
|
|
66
|
+
HttpRetryDelayStrategy,
|
|
67
|
+
HttpRetryJitter,
|
|
68
|
+
HttpRetryPreset,
|
|
69
|
+
HttpRetryUnsafeMethodPolicy,
|
|
13
70
|
} from "./types";
|