@apifuse/provider-sdk 2.2.0-beta.59 → 2.2.0-beta.61
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 +33 -16
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +7 -8
- package/bin/apifuse-pack-types.ts +22 -0
- package/bin/apifuse-perf.ts +45 -10
- package/bin/apifuse-record.ts +6 -8
- package/bin/apifuse-submit-check.ts +2 -2
- package/dist/config/loader.js +13 -12
- package/dist/define.d.ts +3 -2
- package/dist/define.js +1 -1
- package/dist/dev.js +3 -1
- package/dist/engine.d.ts +7 -6
- package/dist/engine.js +6 -6
- package/dist/error-resolution.js +21 -4
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +9 -0
- package/dist/handle-meta.d.ts +39 -0
- package/dist/handle-meta.js +29 -0
- package/dist/handle.d.ts +187 -0
- package/dist/handle.js +254 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.js +4 -3
- package/dist/lint.d.ts +3 -0
- package/dist/lint.js +388 -62
- package/dist/provider.d.ts +4 -4
- package/dist/provider.js +3 -3
- package/dist/runtime/browser.js +2 -1
- package/dist/runtime/cache.js +3 -2
- package/dist/runtime/diagnostic-env-values.d.ts +5 -0
- package/dist/runtime/diagnostic-env-values.js +57 -0
- package/dist/runtime/diagnostic-env.d.ts +11 -0
- package/dist/runtime/diagnostic-env.js +35 -0
- package/dist/runtime/diagnostic-redactor.d.ts +49 -0
- package/dist/runtime/diagnostic-redactor.js +569 -0
- package/dist/runtime/egress-lease.d.ts +2 -0
- package/dist/runtime/egress-lease.js +4 -1
- package/dist/runtime/env.js +2 -1
- package/dist/runtime/handle-wordlist.d.ts +43 -0
- package/dist/runtime/{choice-wordlist.js → handle-wordlist.js} +127 -9
- package/dist/runtime/handle.d.ts +71 -0
- package/dist/runtime/handle.js +895 -0
- package/dist/runtime/instrumentation.js +3 -0
- package/dist/runtime/native-network.d.ts +10 -1
- package/dist/runtime/native-network.js +230 -29
- package/dist/runtime/native-telemetry.d.ts +142 -0
- package/dist/runtime/native-telemetry.js +231 -0
- package/dist/runtime/ocr.js +2 -1
- package/dist/runtime/otlp.js +5 -4
- package/dist/runtime/proxy-nodemaven.js +3 -2
- package/dist/runtime/request-options.d.ts +7 -0
- package/dist/runtime/request-options.js +62 -22
- package/dist/runtime/resolver-vendors/browser.js +0 -1
- package/dist/runtime/resolver.js +3 -2
- package/dist/runtime/state.d.ts +4 -1
- package/dist/runtime/state.js +32 -17
- package/dist/runtime/stt.js +3 -2
- package/dist/runtime/trace.d.ts +5 -0
- package/dist/runtime/trace.js +115 -20
- package/dist/schema.d.ts +11 -0
- package/dist/schema.js +56 -2
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.js +1 -0
- package/dist/server/self-test-redaction.js +8 -33
- package/dist/server/sensitive-values.d.ts +26 -0
- package/dist/server/sensitive-values.js +128 -0
- package/dist/server/serve-implementation.d.ts +4 -11
- package/dist/server/serve-implementation.js +305 -96
- package/dist/server/trace-output.d.ts +2 -1
- package/dist/server/trace-output.js +46 -14
- package/dist/testing/run.js +10 -7
- package/dist/trace-sanitization.d.ts +4 -3
- package/dist/trace-sanitization.js +45 -22
- package/dist/types.d.ts +4 -107
- package/package.json +2 -1
- package/src/config/loader.ts +13 -12
- package/src/define.ts +4 -3
- package/src/dev.ts +3 -1
- package/src/engine.ts +8 -8
- package/src/error-resolution.ts +21 -4
- package/src/errors.ts +13 -0
- package/src/handle-meta.ts +58 -0
- package/src/handle.ts +529 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/index.ts +28 -13
- package/src/lint.ts +456 -71
- package/src/provider.ts +10 -20
- package/src/runtime/browser.ts +2 -1
- package/src/runtime/cache.ts +3 -2
- package/src/runtime/diagnostic-env-values.ts +56 -0
- package/src/runtime/diagnostic-env.ts +55 -0
- package/src/runtime/diagnostic-redactor.ts +662 -0
- package/src/runtime/egress-lease.ts +6 -1
- package/src/runtime/env.ts +2 -1
- package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
- package/src/runtime/handle.ts +1162 -0
- package/src/runtime/instrumentation.ts +3 -0
- package/src/runtime/native-network.ts +281 -33
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/ocr.ts +2 -1
- package/src/runtime/otlp.ts +5 -4
- package/src/runtime/proxy-nodemaven.ts +3 -2
- package/src/runtime/request-options.ts +76 -28
- package/src/runtime/resolver-vendors/browser.ts +0 -1
- package/src/runtime/resolver.ts +3 -2
- package/src/runtime/state.ts +44 -16
- package/src/runtime/stt.ts +3 -2
- package/src/runtime/trace.ts +168 -26
- package/src/schema.ts +63 -2
- package/src/server/index.ts +17 -0
- package/src/server/self-test-redaction.ts +10 -40
- package/src/server/sensitive-values.ts +197 -0
- package/src/server/serve-implementation.ts +388 -128
- package/src/server/trace-output.ts +77 -17
- package/src/testing/run.ts +11 -7
- package/src/trace-sanitization.ts +70 -23
- package/src/types.ts +4 -137
- package/dist/choice-token.d.ts +0 -24
- package/dist/choice-token.js +0 -74
- package/dist/runtime/choice-wordlist.d.ts +0 -9
- package/dist/runtime/choice.d.ts +0 -27
- package/dist/runtime/choice.js +0 -801
- package/src/choice-token.ts +0 -165
- package/src/runtime/choice.ts +0 -1197
package/src/lint.ts
CHANGED
|
@@ -6,13 +6,16 @@ import {
|
|
|
6
6
|
SDK_RUNTIME_OWNED_ERROR_CODES,
|
|
7
7
|
SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES,
|
|
8
8
|
} from "./error-resolution.js";
|
|
9
|
+
import {
|
|
10
|
+
APIFUSE_HANDLE_META_KEY,
|
|
11
|
+
type HandleFieldMeta,
|
|
12
|
+
type HandleKindDeclaration,
|
|
13
|
+
handleFieldDescription,
|
|
14
|
+
isHandleFieldMeta,
|
|
15
|
+
} from "./handle-meta.js";
|
|
9
16
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
10
17
|
import { APIFUSE_DESCRIPTION_KEY_META_KEY, APIFUSE_SENSITIVE_META_KEY } from "./schema.js";
|
|
11
|
-
import type {
|
|
12
|
-
AuthMode,
|
|
13
|
-
OperationApprovalPolicy,
|
|
14
|
-
OperationRiskClass,
|
|
15
|
-
} from "./types.js";
|
|
18
|
+
import type { AuthMode, OperationApprovalPolicy, OperationRiskClass } from "./types.js";
|
|
16
19
|
|
|
17
20
|
const requireModule = createRequire(import.meta.url);
|
|
18
21
|
// `typeof import(...)` keeps the type without emitting a static import: the
|
|
@@ -92,12 +95,7 @@ const AUTH_LIFECYCLE_SEGMENT_WORDS = new Set([
|
|
|
92
95
|
]);
|
|
93
96
|
|
|
94
97
|
// Ambiguous as a prefix, unambiguous when they are the whole operation id.
|
|
95
|
-
const AUTH_LIFECYCLE_WHOLE_ID_WORDS = new Set([
|
|
96
|
-
"authorize",
|
|
97
|
-
"revoke",
|
|
98
|
-
"unlink",
|
|
99
|
-
"disconnect",
|
|
100
|
-
]);
|
|
98
|
+
const AUTH_LIFECYCLE_WHOLE_ID_WORDS = new Set(["authorize", "revoke", "unlink", "disconnect"]);
|
|
101
99
|
|
|
102
100
|
// A verb stem followed by a direction word across two segments: `sign-out`,
|
|
103
101
|
// `user_sign_up_flow`, and the spelled-out `log-in` / `shop-log-out` forms
|
|
@@ -465,19 +463,30 @@ function getObjectShape(schema: SchemaLike): Record<string, SchemaLike> {
|
|
|
465
463
|
return {};
|
|
466
464
|
}
|
|
467
465
|
|
|
468
|
-
|
|
466
|
+
/**
|
|
467
|
+
* Child edges of a schema node. `origin: "shape"` marks object properties whose
|
|
468
|
+
* key is provider-chosen (and may collide with a structural edge name such as
|
|
469
|
+
* `item`, `type`, or `schema`); `origin: "structural"` marks zod internals
|
|
470
|
+
* (wrappers, array elements, union options, pipe stages).
|
|
471
|
+
*/
|
|
472
|
+
type SchemaChildEdge = { key: string; schema: SchemaLike; origin: "shape" | "structural" };
|
|
473
|
+
|
|
474
|
+
function getChildSchemas(schema: SchemaLike): SchemaChildEdge[] {
|
|
469
475
|
const seen = new Map<string, SchemaLike>();
|
|
476
|
+
const origins = new Map<string, "shape" | "structural">();
|
|
470
477
|
const def = getSchemaDef(schema);
|
|
471
478
|
|
|
472
|
-
const add = (key: string, value: unknown) => {
|
|
479
|
+
const add = (key: string, value: unknown, origin: "shape" | "structural" = "structural") => {
|
|
473
480
|
if (!isSchema(value)) {
|
|
474
481
|
return;
|
|
475
482
|
}
|
|
476
|
-
|
|
483
|
+
const id = `${key}:${seen.size}`;
|
|
484
|
+
seen.set(id, value);
|
|
485
|
+
origins.set(id, origin);
|
|
477
486
|
};
|
|
478
487
|
|
|
479
488
|
for (const [key, value] of Object.entries(getObjectShape(schema))) {
|
|
480
|
-
add(key, value);
|
|
489
|
+
add(key, value, "shape");
|
|
481
490
|
}
|
|
482
491
|
|
|
483
492
|
add("element", schema.element);
|
|
@@ -520,6 +529,9 @@ function getChildSchemas(schema: SchemaLike): Array<{ key: string; schema: Schem
|
|
|
520
529
|
"schema",
|
|
521
530
|
"innerType",
|
|
522
531
|
"type",
|
|
532
|
+
// zod v4 intersections keep their branches on the definition.
|
|
533
|
+
"left",
|
|
534
|
+
"right",
|
|
523
535
|
"valueType",
|
|
524
536
|
"keyType",
|
|
525
537
|
"item",
|
|
@@ -543,6 +555,7 @@ function getChildSchemas(schema: SchemaLike): Array<{ key: string; schema: Schem
|
|
|
543
555
|
return Array.from(seen.entries()).map(([entryKey, child]) => ({
|
|
544
556
|
key: entryKey.split(":")[0] ?? entryKey,
|
|
545
557
|
schema: child,
|
|
558
|
+
origin: origins.get(entryKey) ?? "structural",
|
|
546
559
|
}));
|
|
547
560
|
}
|
|
548
561
|
|
|
@@ -550,18 +563,29 @@ function uniqueFields(fields: string[]): string[] {
|
|
|
550
563
|
return Array.from(new Set(fields));
|
|
551
564
|
}
|
|
552
565
|
|
|
553
|
-
function
|
|
554
|
-
|
|
555
|
-
|
|
566
|
+
function readSensitivityDeclaration(
|
|
567
|
+
schema: unknown,
|
|
568
|
+
seen = new Set<unknown>(),
|
|
569
|
+
): boolean | undefined {
|
|
570
|
+
// Declarations sit on the leaf; same-path wrappers such as .optional(),
|
|
571
|
+
// .nullable(), .pipe() or z.lazy() carry no metadata of their own, so look
|
|
572
|
+
// through them.
|
|
573
|
+
if (!isSchema(schema) || seen.has(schema)) return undefined;
|
|
574
|
+
seen.add(schema);
|
|
575
|
+
const value = Reflect.get(getSchemaMetadata(schema), APIFUSE_SENSITIVE_META_KEY);
|
|
576
|
+
if (typeof value === "boolean") return value;
|
|
577
|
+
const def = getSchemaDef(schema);
|
|
578
|
+
const children =
|
|
579
|
+
def.type === "pipe"
|
|
580
|
+
? [def.in, def.out]
|
|
581
|
+
: def.type === "lazy" && typeof def.getter === "function"
|
|
582
|
+
? [def.getter()]
|
|
583
|
+
: [def.innerType];
|
|
584
|
+
for (const child of children) {
|
|
585
|
+
const declaration = readSensitivityDeclaration(child, seen);
|
|
586
|
+
if (declaration !== undefined) return declaration;
|
|
556
587
|
}
|
|
557
|
-
|
|
558
|
-
if (typeof meta !== "function") return false;
|
|
559
|
-
const metadata = meta.call(schema);
|
|
560
|
-
return (
|
|
561
|
-
!!metadata &&
|
|
562
|
-
typeof metadata === "object" &&
|
|
563
|
-
Reflect.get(metadata, APIFUSE_SENSITIVE_META_KEY) === true
|
|
564
|
-
);
|
|
588
|
+
return undefined;
|
|
565
589
|
}
|
|
566
590
|
|
|
567
591
|
function getSchemaMetadata(schema: SchemaLike): Record<string, unknown> {
|
|
@@ -591,9 +615,95 @@ const SENSITIVE_FIELD_NAMES = new Set([
|
|
|
591
615
|
"paymenturl",
|
|
592
616
|
]);
|
|
593
617
|
|
|
618
|
+
// Only phone-shaped keys can hold reviewed public data (facility phones); the
|
|
619
|
+
// rest are credentials, which a public declaration must not launder.
|
|
620
|
+
const PUBLIC_DECLARABLE_FIELD_NAMES = new Set(["phone", "phonenumber"]);
|
|
621
|
+
|
|
622
|
+
function normalizeFieldName(name: string): string {
|
|
623
|
+
return name.toLowerCase().replace(/[-_\s]/g, "");
|
|
624
|
+
}
|
|
625
|
+
|
|
594
626
|
function isSensitiveFieldName(name: string): boolean {
|
|
595
|
-
|
|
596
|
-
|
|
627
|
+
return SENSITIVE_FIELD_NAMES.has(normalizeFieldName(name));
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
function isDeclaredSensitiveField(key: string, schema: unknown): boolean {
|
|
631
|
+
const declaration = readSensitivityDeclaration(schema);
|
|
632
|
+
return (
|
|
633
|
+
declaration === true ||
|
|
634
|
+
(declaration === false && PUBLIC_DECLARABLE_FIELD_NAMES.has(normalizeFieldName(key)))
|
|
635
|
+
);
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Child keys produced by getChildSchemas() that wrap a schema without adding a
|
|
639
|
+
// property path segment (optional/nullable/default/pipe/effects/etc.).
|
|
640
|
+
const SCHEMA_WRAPPER_NODE_KEYS: readonly string[] = [
|
|
641
|
+
"unwrap",
|
|
642
|
+
"innerType",
|
|
643
|
+
"sourceType",
|
|
644
|
+
"schema",
|
|
645
|
+
"type",
|
|
646
|
+
"in",
|
|
647
|
+
"out",
|
|
648
|
+
"option",
|
|
649
|
+
"pipe",
|
|
650
|
+
"payload",
|
|
651
|
+
"item",
|
|
652
|
+
"rest",
|
|
653
|
+
"catchall",
|
|
654
|
+
"keyType",
|
|
655
|
+
"valueType",
|
|
656
|
+
// zod v4 intersection branches: transparent, the enclosing property key applies.
|
|
657
|
+
"left",
|
|
658
|
+
"right",
|
|
659
|
+
];
|
|
660
|
+
|
|
661
|
+
function isSchemaWrapperNodeKey(key: string): boolean {
|
|
662
|
+
return SCHEMA_WRAPPER_NODE_KEYS.includes(key) || key.startsWith("pipe.");
|
|
663
|
+
}
|
|
664
|
+
|
|
665
|
+
function getOwnHandleFieldMeta(schema: SchemaLike): HandleFieldMeta | undefined {
|
|
666
|
+
const value = Reflect.get(getSchemaMetadata(schema), APIFUSE_HANDLE_META_KEY);
|
|
667
|
+
return isHandleFieldMeta(value) ? value : undefined;
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* Handle meta placed by `kind.field()` lives on the string schema itself; zod
|
|
672
|
+
* wrappers (`.optional()`, `.nullable()`, `.default()`, pipes) do not inherit
|
|
673
|
+
* registry metadata, so look through wrapper nodes until a non-wrapper is hit.
|
|
674
|
+
*/
|
|
675
|
+
function getHandleFieldMeta(
|
|
676
|
+
schema: unknown,
|
|
677
|
+
seen = new Set<SchemaLike>(),
|
|
678
|
+
): HandleFieldMeta | undefined {
|
|
679
|
+
if (!isSchema(schema) || seen.has(schema)) return undefined;
|
|
680
|
+
seen.add(schema);
|
|
681
|
+
const own = getOwnHandleFieldMeta(schema);
|
|
682
|
+
if (own) return own;
|
|
683
|
+
for (const child of getChildSchemas(schema)) {
|
|
684
|
+
if (child.origin === "shape" || !isSchemaWrapperNodeKey(child.key)) continue;
|
|
685
|
+
const found = getHandleFieldMeta(child.schema, seen);
|
|
686
|
+
if (found) return found;
|
|
687
|
+
}
|
|
688
|
+
return undefined;
|
|
689
|
+
}
|
|
690
|
+
|
|
691
|
+
/**
|
|
692
|
+
* True when `schema` is a handle field (directly or through zod wrappers) whose
|
|
693
|
+
* description is still the SDK-generated wording and no wrapper adds its own
|
|
694
|
+
* description.
|
|
695
|
+
*/
|
|
696
|
+
function isSdkOwnedHandleField(schema: unknown, seen = new Set<SchemaLike>()): boolean {
|
|
697
|
+
if (!isSchema(schema) || seen.has(schema)) return false;
|
|
698
|
+
seen.add(schema);
|
|
699
|
+
const own = getOwnHandleFieldMeta(schema);
|
|
700
|
+
if (own) return schema.description === handleFieldDescription(own);
|
|
701
|
+
if (schema.description) return false;
|
|
702
|
+
for (const child of getChildSchemas(schema)) {
|
|
703
|
+
if (child.origin === "shape" || !isSchemaWrapperNodeKey(child.key)) continue;
|
|
704
|
+
if (isSdkOwnedHandleField(child.schema, seen)) return true;
|
|
705
|
+
}
|
|
706
|
+
return false;
|
|
597
707
|
}
|
|
598
708
|
|
|
599
709
|
function collectUnmarkedSensitiveFields(
|
|
@@ -608,30 +718,14 @@ function collectUnmarkedSensitiveFields(
|
|
|
608
718
|
const out: string[] = [];
|
|
609
719
|
for (const [key, child] of Object.entries(getObjectShape(schema))) {
|
|
610
720
|
const childPath = basePath ? `${basePath}.${key}` : key;
|
|
611
|
-
if (isSensitiveFieldName(key) && !
|
|
721
|
+
if (isSensitiveFieldName(key) && !isDeclaredSensitiveField(key, child)) {
|
|
612
722
|
out.push(childPath);
|
|
613
723
|
}
|
|
614
724
|
out.push(...collectUnmarkedSensitiveFields(child, childPath, seen));
|
|
615
725
|
}
|
|
616
726
|
for (const child of getChildSchemas(schema)) {
|
|
617
727
|
if (Object.hasOwn(getObjectShape(schema), child.key)) continue;
|
|
618
|
-
const isWrapperNode =
|
|
619
|
-
"unwrap",
|
|
620
|
-
"innerType",
|
|
621
|
-
"sourceType",
|
|
622
|
-
"schema",
|
|
623
|
-
"type",
|
|
624
|
-
"in",
|
|
625
|
-
"out",
|
|
626
|
-
"option",
|
|
627
|
-
"pipe",
|
|
628
|
-
"payload",
|
|
629
|
-
"item",
|
|
630
|
-
"rest",
|
|
631
|
-
"catchall",
|
|
632
|
-
"keyType",
|
|
633
|
-
"valueType",
|
|
634
|
-
].includes(child.key);
|
|
728
|
+
const isWrapperNode = SCHEMA_WRAPPER_NODE_KEYS.includes(child.key);
|
|
635
729
|
const childPath =
|
|
636
730
|
child.key === "element" || child.key.startsWith("element.")
|
|
637
731
|
? `${basePath}[]`
|
|
@@ -660,6 +754,15 @@ function collectSchemaDescriptionKeyDiagnostics(
|
|
|
660
754
|
const currentPath = basePath || "schema";
|
|
661
755
|
const hasDescriptionKey = getSchemaDescriptionKey(schema) !== undefined;
|
|
662
756
|
|
|
757
|
+
// Handle fields (`kind.field()`, ADR-0012) carry SDK-owned wording via
|
|
758
|
+
// .describe() plus x-apifuse-handle meta; they count as described and are
|
|
759
|
+
// leaves, so neither this node nor its wrapper chain needs a describeKey.
|
|
760
|
+
// A provider override of that wording (`.describe("…")` on the field or on
|
|
761
|
+
// a wrapper) is provider prose again and stays subject to the rules below.
|
|
762
|
+
if (isSdkOwnedHandleField(schema)) {
|
|
763
|
+
return diagnostics;
|
|
764
|
+
}
|
|
765
|
+
|
|
663
766
|
if (schema.description && !hasDescriptionKey) {
|
|
664
767
|
diagnostics.push({
|
|
665
768
|
rule: "schema-description-raw-prose",
|
|
@@ -681,23 +784,7 @@ function collectSchemaDescriptionKeyDiagnostics(
|
|
|
681
784
|
}
|
|
682
785
|
|
|
683
786
|
for (const child of getChildSchemas(schema)) {
|
|
684
|
-
const isWrapperNode =
|
|
685
|
-
"unwrap",
|
|
686
|
-
"innerType",
|
|
687
|
-
"sourceType",
|
|
688
|
-
"schema",
|
|
689
|
-
"type",
|
|
690
|
-
"in",
|
|
691
|
-
"out",
|
|
692
|
-
"option",
|
|
693
|
-
"pipe",
|
|
694
|
-
"payload",
|
|
695
|
-
"item",
|
|
696
|
-
"rest",
|
|
697
|
-
"catchall",
|
|
698
|
-
"keyType",
|
|
699
|
-
"valueType",
|
|
700
|
-
].includes(child.key);
|
|
787
|
+
const isWrapperNode = SCHEMA_WRAPPER_NODE_KEYS.includes(child.key);
|
|
701
788
|
const isStructuralNode =
|
|
702
789
|
isWrapperNode ||
|
|
703
790
|
child.key.startsWith("pipe.") ||
|
|
@@ -1249,7 +1336,10 @@ function collectLiteralErrorCodeValues(args: string): string[] {
|
|
|
1249
1336
|
after++;
|
|
1250
1337
|
}
|
|
1251
1338
|
const nextChar = after < args.length ? (args[after] ?? "") : "";
|
|
1252
|
-
if (
|
|
1339
|
+
if (
|
|
1340
|
+
!value.includes("\\") &&
|
|
1341
|
+
(nextChar === "," || nextChar === "}" || nextChar === "")
|
|
1342
|
+
) {
|
|
1253
1343
|
codes.push(value);
|
|
1254
1344
|
}
|
|
1255
1345
|
index = end;
|
|
@@ -1362,6 +1452,300 @@ function lintUndeclaredThrownErrorCodes(provider: {
|
|
|
1362
1452
|
return diagnostics;
|
|
1363
1453
|
}
|
|
1364
1454
|
|
|
1455
|
+
type HandleFieldOccurrence = {
|
|
1456
|
+
/** Diagnostic path, e.g. `output.offers[].waiting_token`. */
|
|
1457
|
+
path: string;
|
|
1458
|
+
/** Property key the field is declared under (inherited through arrays/wrappers). */
|
|
1459
|
+
key: string;
|
|
1460
|
+
meta: HandleFieldMeta;
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
function collectHandleFields(
|
|
1464
|
+
schema: unknown,
|
|
1465
|
+
basePath: string,
|
|
1466
|
+
key: string | undefined = undefined,
|
|
1467
|
+
seen = new Set<SchemaLike>(),
|
|
1468
|
+
): HandleFieldOccurrence[] {
|
|
1469
|
+
if (!isSchema(schema)) return [];
|
|
1470
|
+
|
|
1471
|
+
// Record before the cycle guard so one `kind.field()` instance reused under
|
|
1472
|
+
// two property keys is still reported at both positions.
|
|
1473
|
+
const own = getOwnHandleFieldMeta(schema);
|
|
1474
|
+
if (own && key !== undefined) {
|
|
1475
|
+
return [{ path: basePath, key, meta: own }];
|
|
1476
|
+
}
|
|
1477
|
+
// Path-local cycle guard: the same wrapped schema instance may legitimately
|
|
1478
|
+
// appear under several property keys (`Kind.field().optional()` reused), and
|
|
1479
|
+
// each position must be checked. Only true ancestor cycles are cut.
|
|
1480
|
+
if (seen.has(schema)) return [];
|
|
1481
|
+
seen.add(schema);
|
|
1482
|
+
try {
|
|
1483
|
+
return collectHandleFieldChildren(schema, basePath, key, seen);
|
|
1484
|
+
} finally {
|
|
1485
|
+
seen.delete(schema);
|
|
1486
|
+
}
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
function collectHandleFieldChildren(
|
|
1490
|
+
schema: SchemaLike,
|
|
1491
|
+
basePath: string,
|
|
1492
|
+
key: string | undefined,
|
|
1493
|
+
seen: Set<SchemaLike>,
|
|
1494
|
+
): HandleFieldOccurrence[] {
|
|
1495
|
+
const out: HandleFieldOccurrence[] = [];
|
|
1496
|
+
// zod exposes the same inner schema under several structural edges
|
|
1497
|
+
// (`unwrap`/`innerType`, `element`/`def.element`); traverse each structural
|
|
1498
|
+
// child object once. Shape children are always traversed: the same instance
|
|
1499
|
+
// under two property keys is two fields.
|
|
1500
|
+
const structuralSeen = new Set<SchemaLike>();
|
|
1501
|
+
for (const child of getChildSchemas(schema)) {
|
|
1502
|
+
if (child.origin !== "shape") {
|
|
1503
|
+
if (structuralSeen.has(child.schema)) continue;
|
|
1504
|
+
structuralSeen.add(child.schema);
|
|
1505
|
+
}
|
|
1506
|
+
if (child.origin === "shape") {
|
|
1507
|
+
// Provider-chosen property key: always a real field, even when it is
|
|
1508
|
+
// spelled like a structural edge (`item`, `type`, `schema`, ...).
|
|
1509
|
+
out.push(...collectHandleFields(child.schema, `${basePath}.${child.key}`, child.key, seen));
|
|
1510
|
+
} else if (isSchemaWrapperNodeKey(child.key)) {
|
|
1511
|
+
out.push(...collectHandleFields(child.schema, basePath, key, seen));
|
|
1512
|
+
} else if (child.key === "element" || child.key.startsWith("element.")) {
|
|
1513
|
+
out.push(...collectHandleFields(child.schema, `${basePath}[]`, key, seen));
|
|
1514
|
+
} else if (/^\d+$/.test(child.key)) {
|
|
1515
|
+
out.push(...collectHandleFields(child.schema, `${basePath}[${child.key}]`, key, seen));
|
|
1516
|
+
} else {
|
|
1517
|
+
out.push(...collectHandleFields(child.schema, `${basePath}.${child.key}`, child.key, seen));
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
return out;
|
|
1521
|
+
}
|
|
1522
|
+
|
|
1523
|
+
/**
|
|
1524
|
+
* Reads `provider.handle` structurally. The declaration type is owned by
|
|
1525
|
+
* define.ts/types.ts; lint only needs the HandleKindDeclaration view, so
|
|
1526
|
+
* malformed entries are skipped rather than reported.
|
|
1527
|
+
*/
|
|
1528
|
+
function readHandleKindDeclarations(value: unknown): HandleKindDeclaration[] {
|
|
1529
|
+
if (!Array.isArray(value)) return [];
|
|
1530
|
+
return value.filter((entry): entry is HandleKindDeclaration => {
|
|
1531
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
1532
|
+
const record = entry as Record<string, unknown>;
|
|
1533
|
+
return (
|
|
1534
|
+
typeof record.name === "string" &&
|
|
1535
|
+
(record.type === "cursor" || record.type === "draft") &&
|
|
1536
|
+
typeof record.fieldName === "string" &&
|
|
1537
|
+
(record.issuedBy === undefined || typeof record.issuedBy === "string")
|
|
1538
|
+
);
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
/**
|
|
1543
|
+
* ADR-0012 handle rules: every `x-apifuse-handle` schema field must belong to
|
|
1544
|
+
* a declared kind under its declared property key; every declared kind must be
|
|
1545
|
+
* both issued (some output) and accepted (some input); `issuedBy` must name an
|
|
1546
|
+
* operation whose output carries the field; and `ctx.handle` needs `state`.
|
|
1547
|
+
*/
|
|
1548
|
+
function lintHandleDeclarations(provider: {
|
|
1549
|
+
id?: string;
|
|
1550
|
+
handle?: unknown;
|
|
1551
|
+
state?: unknown;
|
|
1552
|
+
operations?: Record<string, { input: unknown; output: unknown }>;
|
|
1553
|
+
}): LintDiagnostic[] {
|
|
1554
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
1555
|
+
const providerLabel = provider.id ? `Provider "${provider.id}"` : "Provider";
|
|
1556
|
+
const kinds = readHandleKindDeclarations(provider.handle);
|
|
1557
|
+
const kindByName = new Map(kinds.map((kind) => [kind.name, kind]));
|
|
1558
|
+
const operations = provider.operations ?? {};
|
|
1559
|
+
|
|
1560
|
+
if (kinds.length > 0 && !provider.state) {
|
|
1561
|
+
diagnostics.push({
|
|
1562
|
+
rule: "handle-requires-state",
|
|
1563
|
+
level: "warn",
|
|
1564
|
+
field: "handle",
|
|
1565
|
+
message: `${providerLabel} declares handle kinds but not the state capability; ctx.handle stores records through ctx.state, so declare state: true.`,
|
|
1566
|
+
});
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
const kindsInInputs = new Set<string>();
|
|
1570
|
+
const kindsInOutputs = new Set<string>();
|
|
1571
|
+
const outputKindsByOperation = new Map<string, Set<string>>();
|
|
1572
|
+
|
|
1573
|
+
for (const [operationKey, operation] of Object.entries(operations)) {
|
|
1574
|
+
const positions = [
|
|
1575
|
+
{ side: "input", fields: collectHandleFields(operation.input, "input") },
|
|
1576
|
+
{ side: "output", fields: collectHandleFields(operation.output, "output") },
|
|
1577
|
+
] as const;
|
|
1578
|
+
for (const { side, fields } of positions) {
|
|
1579
|
+
for (const occurrence of fields) {
|
|
1580
|
+
const field = `operations.${operationKey}.${occurrence.path}`;
|
|
1581
|
+
const { kind, fieldName } = occurrence.meta;
|
|
1582
|
+
|
|
1583
|
+
if (!kindByName.has(kind)) {
|
|
1584
|
+
diagnostics.push({
|
|
1585
|
+
rule: "handle-kind-undeclared",
|
|
1586
|
+
level: "error",
|
|
1587
|
+
field,
|
|
1588
|
+
message: `${providerLabel} operation "${operationKey}" ${side} field "${occurrence.path}" is a handle of kind "${kind}", which is not declared in provider.handle. Add the kind to handle: [...] or remove the field.`,
|
|
1589
|
+
});
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
if (occurrence.key !== fieldName) {
|
|
1593
|
+
diagnostics.push({
|
|
1594
|
+
rule: "handle-field-name",
|
|
1595
|
+
level: "error",
|
|
1596
|
+
field,
|
|
1597
|
+
message: `${providerLabel} operation "${operationKey}" ${side} declares handle kind "${kind}" under property "${occurrence.key}" but the kind's fieldName is "${fieldName}". Use the same key in every schema so the LLM sees one name.`,
|
|
1598
|
+
});
|
|
1599
|
+
}
|
|
1600
|
+
|
|
1601
|
+
if (side === "input") {
|
|
1602
|
+
kindsInInputs.add(kind);
|
|
1603
|
+
} else {
|
|
1604
|
+
kindsInOutputs.add(kind);
|
|
1605
|
+
let outputKinds = outputKindsByOperation.get(operationKey);
|
|
1606
|
+
if (!outputKinds) {
|
|
1607
|
+
outputKinds = new Set<string>();
|
|
1608
|
+
outputKindsByOperation.set(operationKey, outputKinds);
|
|
1609
|
+
}
|
|
1610
|
+
outputKinds.add(kind);
|
|
1611
|
+
}
|
|
1612
|
+
}
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
for (const kind of kinds) {
|
|
1617
|
+
const field = `handle.${kind.name}`;
|
|
1618
|
+
const issued = kindsInOutputs.has(kind.name);
|
|
1619
|
+
const accepted = kindsInInputs.has(kind.name);
|
|
1620
|
+
|
|
1621
|
+
if (!issued) {
|
|
1622
|
+
diagnostics.push({
|
|
1623
|
+
rule: "handle-field-parity",
|
|
1624
|
+
level: "error",
|
|
1625
|
+
field,
|
|
1626
|
+
message: `${providerLabel} handle kind "${kind.name}" is never issued: no operation output contains its field "${kind.fieldName}". Add ${kind.name}.field() to the issuing operation's output.`,
|
|
1627
|
+
});
|
|
1628
|
+
}
|
|
1629
|
+
if (!accepted) {
|
|
1630
|
+
diagnostics.push({
|
|
1631
|
+
rule: "handle-field-parity",
|
|
1632
|
+
level: "error",
|
|
1633
|
+
field,
|
|
1634
|
+
message: `${providerLabel} handle kind "${kind.name}" is never accepted: no operation input contains its field "${kind.fieldName}". Add ${kind.name}.field() to the consuming operation's input.`,
|
|
1635
|
+
});
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
if (kind.issuedBy === undefined) continue;
|
|
1639
|
+
if (!Object.hasOwn(operations, kind.issuedBy)) {
|
|
1640
|
+
diagnostics.push({
|
|
1641
|
+
rule: "handle-issued-by",
|
|
1642
|
+
level: "error",
|
|
1643
|
+
field,
|
|
1644
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${kind.issuedBy}", which is not an operation key.`,
|
|
1645
|
+
});
|
|
1646
|
+
} else if (!outputKindsByOperation.get(kind.issuedBy)?.has(kind.name)) {
|
|
1647
|
+
diagnostics.push({
|
|
1648
|
+
rule: "handle-issued-by",
|
|
1649
|
+
level: "error",
|
|
1650
|
+
field,
|
|
1651
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${kind.issuedBy}", but that operation's output has no "${kind.fieldName}" handle field.`,
|
|
1652
|
+
});
|
|
1653
|
+
}
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
return diagnostics;
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
const LEGACY_CHOICE_USAGE_PATTERN =
|
|
1660
|
+
/\bctx\.choice\b|\b(?:createProviderChoiceToken|parseProviderChoiceToken|ProviderChoiceTokenError|createTestProviderChoiceContext)\b/;
|
|
1661
|
+
|
|
1662
|
+
const LEGACY_CHOICE_USAGE_MESSAGE =
|
|
1663
|
+
"Choice tokens were replaced by handles (ADR-0012). See docs/migrations/handle.md.";
|
|
1664
|
+
|
|
1665
|
+
/**
|
|
1666
|
+
* The inline choice-token API was removed outright (no aliases). Any surviving
|
|
1667
|
+
* reference in provider source would fail at runtime, so report it at check time.
|
|
1668
|
+
*/
|
|
1669
|
+
const LEGACY_CHOICE_IDENTIFIERS = new Set([
|
|
1670
|
+
"createProviderChoiceToken",
|
|
1671
|
+
"parseProviderChoiceToken",
|
|
1672
|
+
"ProviderChoiceTokenError",
|
|
1673
|
+
"createTestProviderChoiceContext",
|
|
1674
|
+
]);
|
|
1675
|
+
|
|
1676
|
+
/**
|
|
1677
|
+
* Finds the first legacy choice API reference in executable code. Uses the
|
|
1678
|
+
* TypeScript AST when available so comments, string and template literal text
|
|
1679
|
+
* never count (a migration note such as `// moved from ctx.choice` must not fail
|
|
1680
|
+
* the provider) while code inside `${}` interpolations still does. Falls back
|
|
1681
|
+
* to a raw-source regex when TypeScript is not installed.
|
|
1682
|
+
*/
|
|
1683
|
+
function findLegacyChoiceUsage(source: string): string | undefined {
|
|
1684
|
+
let ts: typeof import("typescript");
|
|
1685
|
+
try {
|
|
1686
|
+
ts = getTypeScript();
|
|
1687
|
+
} catch {
|
|
1688
|
+
return LEGACY_CHOICE_USAGE_PATTERN.exec(source)?.[0];
|
|
1689
|
+
}
|
|
1690
|
+
const file = ts.createSourceFile(
|
|
1691
|
+
"provider.ts",
|
|
1692
|
+
source,
|
|
1693
|
+
ts.ScriptTarget.Latest,
|
|
1694
|
+
false,
|
|
1695
|
+
ts.ScriptKind.TSX,
|
|
1696
|
+
);
|
|
1697
|
+
let found: string | undefined;
|
|
1698
|
+
const visit = (node: import("typescript").Node): void => {
|
|
1699
|
+
if (found) return;
|
|
1700
|
+
if (ts.isIdentifier(node) && LEGACY_CHOICE_IDENTIFIERS.has(node.text)) {
|
|
1701
|
+
found = node.text;
|
|
1702
|
+
return;
|
|
1703
|
+
}
|
|
1704
|
+
if (
|
|
1705
|
+
ts.isPropertyAccessExpression(node) &&
|
|
1706
|
+
ts.isIdentifier(node.expression) &&
|
|
1707
|
+
node.expression.text === "ctx" &&
|
|
1708
|
+
node.name.text === "choice"
|
|
1709
|
+
) {
|
|
1710
|
+
found = "ctx.choice";
|
|
1711
|
+
return;
|
|
1712
|
+
}
|
|
1713
|
+
ts.forEachChild(node, visit);
|
|
1714
|
+
};
|
|
1715
|
+
visit(file);
|
|
1716
|
+
return found;
|
|
1717
|
+
}
|
|
1718
|
+
|
|
1719
|
+
function lintLegacyChoiceUsage(provider: ProviderSourceLike): LintDiagnostic[] {
|
|
1720
|
+
const sources: Array<{ field: string; source: string }> = [];
|
|
1721
|
+
|
|
1722
|
+
if (provider.authFlowSource) {
|
|
1723
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
1724
|
+
}
|
|
1725
|
+
for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
|
|
1726
|
+
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
1727
|
+
}
|
|
1728
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1729
|
+
const source = getOperationSource(operation);
|
|
1730
|
+
if (source) {
|
|
1731
|
+
sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
1732
|
+
}
|
|
1733
|
+
}
|
|
1734
|
+
|
|
1735
|
+
return sources.flatMap(({ field, source }) => {
|
|
1736
|
+
const match = findLegacyChoiceUsage(source);
|
|
1737
|
+
if (!match) return [];
|
|
1738
|
+
return [
|
|
1739
|
+
{
|
|
1740
|
+
rule: "legacy-choice-usage",
|
|
1741
|
+
level: "error" as const,
|
|
1742
|
+
field,
|
|
1743
|
+
message: `Legacy choice API "${match}" is no longer supported. ${LEGACY_CHOICE_USAGE_MESSAGE}`,
|
|
1744
|
+
},
|
|
1745
|
+
];
|
|
1746
|
+
});
|
|
1747
|
+
}
|
|
1748
|
+
|
|
1365
1749
|
export function lintOperation(op: {
|
|
1366
1750
|
descriptionKey?: string;
|
|
1367
1751
|
whenToUseKeys?: readonly string[];
|
|
@@ -1401,7 +1785,7 @@ export function lintOperation(op: {
|
|
|
1401
1785
|
rule: "sensitive-field-unmarked",
|
|
1402
1786
|
level: "warn",
|
|
1403
1787
|
field,
|
|
1404
|
-
message: `Schema field "${field}" looks sensitive; mark it with fields.*()
|
|
1788
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*() or sensitive(...); publicField(...) is accepted only for reviewed public phone-shaped fields.`,
|
|
1405
1789
|
});
|
|
1406
1790
|
}
|
|
1407
1791
|
|
|
@@ -1410,7 +1794,7 @@ export function lintOperation(op: {
|
|
|
1410
1794
|
rule: "sensitive-field-unmarked",
|
|
1411
1795
|
level: "warn",
|
|
1412
1796
|
field,
|
|
1413
|
-
message: `Schema field "${field}" looks sensitive; mark it with fields.*()
|
|
1797
|
+
message: `Schema field "${field}" looks sensitive; mark it with fields.*() or sensitive(...); publicField(...) is accepted only for reviewed public phone-shaped fields.`,
|
|
1414
1798
|
});
|
|
1415
1799
|
}
|
|
1416
1800
|
|
|
@@ -1515,6 +1899,9 @@ export function lintProvider(
|
|
|
1515
1899
|
contract?: ProviderContractMetaLike;
|
|
1516
1900
|
};
|
|
1517
1901
|
reviewed?: string;
|
|
1902
|
+
/** Declared handle kinds (ADR-0012); read structurally, see readHandleKindDeclarations. */
|
|
1903
|
+
handle?: unknown;
|
|
1904
|
+
state?: unknown;
|
|
1518
1905
|
},
|
|
1519
1906
|
options: ProviderLintOptions = {},
|
|
1520
1907
|
): LintDiagnostic[] {
|
|
@@ -1536,6 +1923,8 @@ export function lintProviderWithInformation(
|
|
|
1536
1923
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1537
1924
|
...lintBrowserVersionLiterals(provider),
|
|
1538
1925
|
...lintUndeclaredThrownErrorCodes(provider),
|
|
1926
|
+
...lintLegacyChoiceUsage(provider),
|
|
1927
|
+
...lintHandleDeclarations(provider),
|
|
1539
1928
|
];
|
|
1540
1929
|
|
|
1541
1930
|
if (provider.operations) {
|
|
@@ -1565,11 +1954,7 @@ export function lintProviderWithInformation(
|
|
|
1565
1954
|
// Every authenticated mode owns an auth.flow; `oauth2_proxied` was
|
|
1566
1955
|
// previously exempt, which let auth-lifecycle operations ship on
|
|
1567
1956
|
// proxied providers unchecked.
|
|
1568
|
-
if (
|
|
1569
|
-
authMode === "credentials" ||
|
|
1570
|
-
authMode === "oauth2" ||
|
|
1571
|
-
authMode === "oauth2_proxied"
|
|
1572
|
-
) {
|
|
1957
|
+
if (authMode === "credentials" || authMode === "oauth2" || authMode === "oauth2_proxied") {
|
|
1573
1958
|
for (const operationKey of Object.keys(provider.operations)) {
|
|
1574
1959
|
if (isAuthLifecycleOperationId(operationKey, authMode)) {
|
|
1575
1960
|
diagnostics.push({
|