@apifuse/provider-sdk 2.2.0-beta.60 → 2.2.0-beta.62
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 +17 -16
- package/CHANGELOG.md +8 -0
- package/bin/apifuse-dev.ts +5 -7
- package/bin/apifuse-pack-types.ts +26 -0
- package/bin/apifuse-perf.ts +4 -6
- package/bin/apifuse-record.ts +4 -7
- package/bin/apifuse-submit-check.ts +2 -2
- 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 +3 -4
- 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 +55 -0
- package/dist/handle-meta.js +57 -0
- package/dist/handle.d.ts +208 -0
- package/dist/handle.js +299 -0
- package/dist/i18n/catalog.js +21 -1
- package/dist/index.d.ts +7 -4
- package/dist/index.js +6 -3
- package/dist/lint.d.ts +3 -0
- package/dist/lint.js +372 -47
- package/dist/provider.d.ts +5 -3
- package/dist/provider.js +4 -2
- 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 +900 -0
- package/dist/runtime/http-telemetry-binding.d.ts +10 -0
- package/dist/runtime/http-telemetry-binding.js +152 -0
- package/dist/runtime/http-telemetry-guard.d.ts +6 -0
- package/dist/runtime/http-telemetry-guard.js +150 -0
- package/dist/runtime/http-telemetry.d.ts +104 -0
- package/dist/runtime/http-telemetry.js +190 -0
- package/dist/runtime/http.d.ts +31 -0
- package/dist/runtime/http.js +222 -146
- 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/state.d.ts +4 -1
- package/dist/runtime/state.js +32 -17
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/sensitive-values.d.ts +1 -1
- package/dist/server/sensitive-values.js +0 -3
- package/dist/server/serve-implementation.d.ts +6 -11
- package/dist/server/serve-implementation.js +119 -52
- package/dist/testing/run.js +10 -7
- package/dist/types.d.ts +4 -107
- package/package.json +1 -1
- package/src/define.ts +4 -3
- package/src/dev.ts +3 -1
- package/src/engine.ts +5 -6
- package/src/error-resolution.ts +21 -4
- package/src/errors.ts +13 -0
- package/src/handle-meta.ts +93 -0
- package/src/handle.ts +612 -0
- package/src/i18n/catalog.ts +23 -1
- package/src/index.ts +45 -13
- package/src/lint.ts +437 -55
- package/src/provider.ts +19 -20
- package/src/runtime/{choice-wordlist.ts → handle-wordlist.ts} +130 -9
- package/src/runtime/handle.ts +1172 -0
- package/src/runtime/http-telemetry-binding.ts +162 -0
- package/src/runtime/http-telemetry-guard.ts +155 -0
- package/src/runtime/http-telemetry.ts +292 -0
- package/src/runtime/http.ts +295 -173
- package/src/runtime/native-network.ts +281 -33
- package/src/runtime/native-telemetry.ts +366 -0
- package/src/runtime/state.ts +44 -16
- package/src/server/index.ts +30 -0
- package/src/server/sensitive-values.ts +0 -3
- package/src/server/serve-implementation.ts +127 -89
- package/src/testing/run.ts +11 -7
- 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,18 @@ 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
|
+
handleIssuerList,
|
|
15
|
+
isHandleIssuedBy,
|
|
16
|
+
isHandleFieldMeta,
|
|
17
|
+
} from "./handle-meta.js";
|
|
9
18
|
import { lintPublicSchemaFieldNames } from "./public-schema-field-lint.js";
|
|
10
19
|
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";
|
|
20
|
+
import type { AuthMode, OperationApprovalPolicy, OperationRiskClass } from "./types.js";
|
|
16
21
|
|
|
17
22
|
const requireModule = createRequire(import.meta.url);
|
|
18
23
|
// `typeof import(...)` keeps the type without emitting a static import: the
|
|
@@ -92,12 +97,7 @@ const AUTH_LIFECYCLE_SEGMENT_WORDS = new Set([
|
|
|
92
97
|
]);
|
|
93
98
|
|
|
94
99
|
// 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
|
-
]);
|
|
100
|
+
const AUTH_LIFECYCLE_WHOLE_ID_WORDS = new Set(["authorize", "revoke", "unlink", "disconnect"]);
|
|
101
101
|
|
|
102
102
|
// A verb stem followed by a direction word across two segments: `sign-out`,
|
|
103
103
|
// `user_sign_up_flow`, and the spelled-out `log-in` / `shop-log-out` forms
|
|
@@ -465,19 +465,30 @@ function getObjectShape(schema: SchemaLike): Record<string, SchemaLike> {
|
|
|
465
465
|
return {};
|
|
466
466
|
}
|
|
467
467
|
|
|
468
|
-
|
|
468
|
+
/**
|
|
469
|
+
* Child edges of a schema node. `origin: "shape"` marks object properties whose
|
|
470
|
+
* key is provider-chosen (and may collide with a structural edge name such as
|
|
471
|
+
* `item`, `type`, or `schema`); `origin: "structural"` marks zod internals
|
|
472
|
+
* (wrappers, array elements, union options, pipe stages).
|
|
473
|
+
*/
|
|
474
|
+
type SchemaChildEdge = { key: string; schema: SchemaLike; origin: "shape" | "structural" };
|
|
475
|
+
|
|
476
|
+
function getChildSchemas(schema: SchemaLike): SchemaChildEdge[] {
|
|
469
477
|
const seen = new Map<string, SchemaLike>();
|
|
478
|
+
const origins = new Map<string, "shape" | "structural">();
|
|
470
479
|
const def = getSchemaDef(schema);
|
|
471
480
|
|
|
472
|
-
const add = (key: string, value: unknown) => {
|
|
481
|
+
const add = (key: string, value: unknown, origin: "shape" | "structural" = "structural") => {
|
|
473
482
|
if (!isSchema(value)) {
|
|
474
483
|
return;
|
|
475
484
|
}
|
|
476
|
-
|
|
485
|
+
const id = `${key}:${seen.size}`;
|
|
486
|
+
seen.set(id, value);
|
|
487
|
+
origins.set(id, origin);
|
|
477
488
|
};
|
|
478
489
|
|
|
479
490
|
for (const [key, value] of Object.entries(getObjectShape(schema))) {
|
|
480
|
-
add(key, value);
|
|
491
|
+
add(key, value, "shape");
|
|
481
492
|
}
|
|
482
493
|
|
|
483
494
|
add("element", schema.element);
|
|
@@ -520,6 +531,9 @@ function getChildSchemas(schema: SchemaLike): Array<{ key: string; schema: Schem
|
|
|
520
531
|
"schema",
|
|
521
532
|
"innerType",
|
|
522
533
|
"type",
|
|
534
|
+
// zod v4 intersections keep their branches on the definition.
|
|
535
|
+
"left",
|
|
536
|
+
"right",
|
|
523
537
|
"valueType",
|
|
524
538
|
"keyType",
|
|
525
539
|
"item",
|
|
@@ -543,6 +557,7 @@ function getChildSchemas(schema: SchemaLike): Array<{ key: string; schema: Schem
|
|
|
543
557
|
return Array.from(seen.entries()).map(([entryKey, child]) => ({
|
|
544
558
|
key: entryKey.split(":")[0] ?? entryKey,
|
|
545
559
|
schema: child,
|
|
560
|
+
origin: origins.get(entryKey) ?? "structural",
|
|
546
561
|
}));
|
|
547
562
|
}
|
|
548
563
|
|
|
@@ -622,6 +637,77 @@ function isDeclaredSensitiveField(key: string, schema: unknown): boolean {
|
|
|
622
637
|
);
|
|
623
638
|
}
|
|
624
639
|
|
|
640
|
+
// Child keys produced by getChildSchemas() that wrap a schema without adding a
|
|
641
|
+
// property path segment (optional/nullable/default/pipe/effects/etc.).
|
|
642
|
+
const SCHEMA_WRAPPER_NODE_KEYS: readonly string[] = [
|
|
643
|
+
"unwrap",
|
|
644
|
+
"innerType",
|
|
645
|
+
"sourceType",
|
|
646
|
+
"schema",
|
|
647
|
+
"type",
|
|
648
|
+
"in",
|
|
649
|
+
"out",
|
|
650
|
+
"option",
|
|
651
|
+
"pipe",
|
|
652
|
+
"payload",
|
|
653
|
+
"item",
|
|
654
|
+
"rest",
|
|
655
|
+
"catchall",
|
|
656
|
+
"keyType",
|
|
657
|
+
"valueType",
|
|
658
|
+
// zod v4 intersection branches: transparent, the enclosing property key applies.
|
|
659
|
+
"left",
|
|
660
|
+
"right",
|
|
661
|
+
];
|
|
662
|
+
|
|
663
|
+
function isSchemaWrapperNodeKey(key: string): boolean {
|
|
664
|
+
return SCHEMA_WRAPPER_NODE_KEYS.includes(key) || key.startsWith("pipe.");
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function getOwnHandleFieldMeta(schema: SchemaLike): HandleFieldMeta | undefined {
|
|
668
|
+
const value = Reflect.get(getSchemaMetadata(schema), APIFUSE_HANDLE_META_KEY);
|
|
669
|
+
return isHandleFieldMeta(value) ? value : undefined;
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
/**
|
|
673
|
+
* Handle meta placed by `kind.field()` lives on the string schema itself; zod
|
|
674
|
+
* wrappers (`.optional()`, `.nullable()`, `.default()`, pipes) do not inherit
|
|
675
|
+
* registry metadata, so look through wrapper nodes until a non-wrapper is hit.
|
|
676
|
+
*/
|
|
677
|
+
function getHandleFieldMeta(
|
|
678
|
+
schema: unknown,
|
|
679
|
+
seen = new Set<SchemaLike>(),
|
|
680
|
+
): HandleFieldMeta | undefined {
|
|
681
|
+
if (!isSchema(schema) || seen.has(schema)) return undefined;
|
|
682
|
+
seen.add(schema);
|
|
683
|
+
const own = getOwnHandleFieldMeta(schema);
|
|
684
|
+
if (own) return own;
|
|
685
|
+
for (const child of getChildSchemas(schema)) {
|
|
686
|
+
if (child.origin === "shape" || !isSchemaWrapperNodeKey(child.key)) continue;
|
|
687
|
+
const found = getHandleFieldMeta(child.schema, seen);
|
|
688
|
+
if (found) return found;
|
|
689
|
+
}
|
|
690
|
+
return undefined;
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
/**
|
|
694
|
+
* True when `schema` is a handle field (directly or through zod wrappers) whose
|
|
695
|
+
* description is still the SDK-generated wording and no wrapper adds its own
|
|
696
|
+
* description.
|
|
697
|
+
*/
|
|
698
|
+
function isSdkOwnedHandleField(schema: unknown, seen = new Set<SchemaLike>()): boolean {
|
|
699
|
+
if (!isSchema(schema) || seen.has(schema)) return false;
|
|
700
|
+
seen.add(schema);
|
|
701
|
+
const own = getOwnHandleFieldMeta(schema);
|
|
702
|
+
if (own) return schema.description === handleFieldDescription(own);
|
|
703
|
+
if (schema.description) return false;
|
|
704
|
+
for (const child of getChildSchemas(schema)) {
|
|
705
|
+
if (child.origin === "shape" || !isSchemaWrapperNodeKey(child.key)) continue;
|
|
706
|
+
if (isSdkOwnedHandleField(child.schema, seen)) return true;
|
|
707
|
+
}
|
|
708
|
+
return false;
|
|
709
|
+
}
|
|
710
|
+
|
|
625
711
|
function collectUnmarkedSensitiveFields(
|
|
626
712
|
schema: unknown,
|
|
627
713
|
basePath: string,
|
|
@@ -641,23 +727,7 @@ function collectUnmarkedSensitiveFields(
|
|
|
641
727
|
}
|
|
642
728
|
for (const child of getChildSchemas(schema)) {
|
|
643
729
|
if (Object.hasOwn(getObjectShape(schema), child.key)) continue;
|
|
644
|
-
const isWrapperNode =
|
|
645
|
-
"unwrap",
|
|
646
|
-
"innerType",
|
|
647
|
-
"sourceType",
|
|
648
|
-
"schema",
|
|
649
|
-
"type",
|
|
650
|
-
"in",
|
|
651
|
-
"out",
|
|
652
|
-
"option",
|
|
653
|
-
"pipe",
|
|
654
|
-
"payload",
|
|
655
|
-
"item",
|
|
656
|
-
"rest",
|
|
657
|
-
"catchall",
|
|
658
|
-
"keyType",
|
|
659
|
-
"valueType",
|
|
660
|
-
].includes(child.key);
|
|
730
|
+
const isWrapperNode = SCHEMA_WRAPPER_NODE_KEYS.includes(child.key);
|
|
661
731
|
const childPath =
|
|
662
732
|
child.key === "element" || child.key.startsWith("element.")
|
|
663
733
|
? `${basePath}[]`
|
|
@@ -686,6 +756,15 @@ function collectSchemaDescriptionKeyDiagnostics(
|
|
|
686
756
|
const currentPath = basePath || "schema";
|
|
687
757
|
const hasDescriptionKey = getSchemaDescriptionKey(schema) !== undefined;
|
|
688
758
|
|
|
759
|
+
// Handle fields (`kind.field()`, ADR-0012) carry SDK-owned wording via
|
|
760
|
+
// .describe() plus x-apifuse-handle meta; they count as described and are
|
|
761
|
+
// leaves, so neither this node nor its wrapper chain needs a describeKey.
|
|
762
|
+
// A provider override of that wording (`.describe("…")` on the field or on
|
|
763
|
+
// a wrapper) is provider prose again and stays subject to the rules below.
|
|
764
|
+
if (isSdkOwnedHandleField(schema)) {
|
|
765
|
+
return diagnostics;
|
|
766
|
+
}
|
|
767
|
+
|
|
689
768
|
if (schema.description && !hasDescriptionKey) {
|
|
690
769
|
diagnostics.push({
|
|
691
770
|
rule: "schema-description-raw-prose",
|
|
@@ -707,23 +786,7 @@ function collectSchemaDescriptionKeyDiagnostics(
|
|
|
707
786
|
}
|
|
708
787
|
|
|
709
788
|
for (const child of getChildSchemas(schema)) {
|
|
710
|
-
const isWrapperNode =
|
|
711
|
-
"unwrap",
|
|
712
|
-
"innerType",
|
|
713
|
-
"sourceType",
|
|
714
|
-
"schema",
|
|
715
|
-
"type",
|
|
716
|
-
"in",
|
|
717
|
-
"out",
|
|
718
|
-
"option",
|
|
719
|
-
"pipe",
|
|
720
|
-
"payload",
|
|
721
|
-
"item",
|
|
722
|
-
"rest",
|
|
723
|
-
"catchall",
|
|
724
|
-
"keyType",
|
|
725
|
-
"valueType",
|
|
726
|
-
].includes(child.key);
|
|
789
|
+
const isWrapperNode = SCHEMA_WRAPPER_NODE_KEYS.includes(child.key);
|
|
727
790
|
const isStructuralNode =
|
|
728
791
|
isWrapperNode ||
|
|
729
792
|
child.key.startsWith("pipe.") ||
|
|
@@ -1275,7 +1338,10 @@ function collectLiteralErrorCodeValues(args: string): string[] {
|
|
|
1275
1338
|
after++;
|
|
1276
1339
|
}
|
|
1277
1340
|
const nextChar = after < args.length ? (args[after] ?? "") : "";
|
|
1278
|
-
if (
|
|
1341
|
+
if (
|
|
1342
|
+
!value.includes("\\") &&
|
|
1343
|
+
(nextChar === "," || nextChar === "}" || nextChar === "")
|
|
1344
|
+
) {
|
|
1279
1345
|
codes.push(value);
|
|
1280
1346
|
}
|
|
1281
1347
|
index = end;
|
|
@@ -1388,6 +1454,321 @@ function lintUndeclaredThrownErrorCodes(provider: {
|
|
|
1388
1454
|
return diagnostics;
|
|
1389
1455
|
}
|
|
1390
1456
|
|
|
1457
|
+
type HandleFieldOccurrence = {
|
|
1458
|
+
/** Diagnostic path, e.g. `output.offers[].waiting_token`. */
|
|
1459
|
+
path: string;
|
|
1460
|
+
/** Property key the field is declared under (inherited through arrays/wrappers). */
|
|
1461
|
+
key: string;
|
|
1462
|
+
meta: HandleFieldMeta;
|
|
1463
|
+
};
|
|
1464
|
+
|
|
1465
|
+
function collectHandleFields(
|
|
1466
|
+
schema: unknown,
|
|
1467
|
+
basePath: string,
|
|
1468
|
+
key: string | undefined = undefined,
|
|
1469
|
+
seen = new Set<SchemaLike>(),
|
|
1470
|
+
): HandleFieldOccurrence[] {
|
|
1471
|
+
if (!isSchema(schema)) return [];
|
|
1472
|
+
|
|
1473
|
+
// Record before the cycle guard so one `kind.field()` instance reused under
|
|
1474
|
+
// two property keys is still reported at both positions.
|
|
1475
|
+
const own = getOwnHandleFieldMeta(schema);
|
|
1476
|
+
if (own && key !== undefined) {
|
|
1477
|
+
return [{ path: basePath, key, meta: own }];
|
|
1478
|
+
}
|
|
1479
|
+
// Path-local cycle guard: the same wrapped schema instance may legitimately
|
|
1480
|
+
// appear under several property keys (`Kind.field().optional()` reused), and
|
|
1481
|
+
// each position must be checked. Only true ancestor cycles are cut.
|
|
1482
|
+
if (seen.has(schema)) return [];
|
|
1483
|
+
seen.add(schema);
|
|
1484
|
+
try {
|
|
1485
|
+
return collectHandleFieldChildren(schema, basePath, key, seen);
|
|
1486
|
+
} finally {
|
|
1487
|
+
seen.delete(schema);
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
|
|
1491
|
+
function collectHandleFieldChildren(
|
|
1492
|
+
schema: SchemaLike,
|
|
1493
|
+
basePath: string,
|
|
1494
|
+
key: string | undefined,
|
|
1495
|
+
seen: Set<SchemaLike>,
|
|
1496
|
+
): HandleFieldOccurrence[] {
|
|
1497
|
+
const out: HandleFieldOccurrence[] = [];
|
|
1498
|
+
// zod exposes the same inner schema under several structural edges
|
|
1499
|
+
// (`unwrap`/`innerType`, `element`/`def.element`); traverse each structural
|
|
1500
|
+
// child object once. Shape children are always traversed: the same instance
|
|
1501
|
+
// under two property keys is two fields.
|
|
1502
|
+
const structuralSeen = new Set<SchemaLike>();
|
|
1503
|
+
for (const child of getChildSchemas(schema)) {
|
|
1504
|
+
if (child.origin !== "shape") {
|
|
1505
|
+
if (structuralSeen.has(child.schema)) continue;
|
|
1506
|
+
structuralSeen.add(child.schema);
|
|
1507
|
+
}
|
|
1508
|
+
if (child.origin === "shape") {
|
|
1509
|
+
// Provider-chosen property key: always a real field, even when it is
|
|
1510
|
+
// spelled like a structural edge (`item`, `type`, `schema`, ...).
|
|
1511
|
+
out.push(...collectHandleFields(child.schema, `${basePath}.${child.key}`, child.key, seen));
|
|
1512
|
+
} else if (isSchemaWrapperNodeKey(child.key)) {
|
|
1513
|
+
out.push(...collectHandleFields(child.schema, basePath, key, seen));
|
|
1514
|
+
} else if (child.key === "element" || child.key.startsWith("element.")) {
|
|
1515
|
+
out.push(...collectHandleFields(child.schema, `${basePath}[]`, key, seen));
|
|
1516
|
+
} else if (/^\d+$/.test(child.key)) {
|
|
1517
|
+
out.push(...collectHandleFields(child.schema, `${basePath}[${child.key}]`, key, seen));
|
|
1518
|
+
} else {
|
|
1519
|
+
out.push(...collectHandleFields(child.schema, `${basePath}.${child.key}`, child.key, seen));
|
|
1520
|
+
}
|
|
1521
|
+
}
|
|
1522
|
+
return out;
|
|
1523
|
+
}
|
|
1524
|
+
|
|
1525
|
+
/**
|
|
1526
|
+
* Reads `provider.handle` structurally. The declaration type is owned by
|
|
1527
|
+
* define.ts/types.ts; lint only needs the HandleKindDeclaration view, so
|
|
1528
|
+
* malformed entries are skipped rather than reported.
|
|
1529
|
+
*/
|
|
1530
|
+
function readHandleKindDeclarations(value: unknown): HandleKindDeclaration[] {
|
|
1531
|
+
if (!Array.isArray(value)) return [];
|
|
1532
|
+
return value.filter((entry): entry is HandleKindDeclaration => {
|
|
1533
|
+
if (typeof entry !== "object" || entry === null) return false;
|
|
1534
|
+
const record = entry as Record<string, unknown>;
|
|
1535
|
+
return (
|
|
1536
|
+
typeof record.name === "string" &&
|
|
1537
|
+
(record.type === "cursor" || record.type === "draft") &&
|
|
1538
|
+
typeof record.fieldName === "string" &&
|
|
1539
|
+
(record.issuedBy === undefined || isHandleIssuedBy(record.issuedBy))
|
|
1540
|
+
);
|
|
1541
|
+
});
|
|
1542
|
+
}
|
|
1543
|
+
|
|
1544
|
+
/**
|
|
1545
|
+
* ADR-0012 handle rules: every `x-apifuse-handle` schema field must belong to
|
|
1546
|
+
* a declared kind under its declared property key; every declared kind must be
|
|
1547
|
+
* both issued (some output) and accepted (some input); every `issuedBy` entry
|
|
1548
|
+
* must be an operation key and at least one of them must output the field; and
|
|
1549
|
+
* `ctx.handle` needs `state`.
|
|
1550
|
+
*/
|
|
1551
|
+
function lintHandleDeclarations(provider: {
|
|
1552
|
+
id?: string;
|
|
1553
|
+
handle?: unknown;
|
|
1554
|
+
state?: unknown;
|
|
1555
|
+
operations?: Record<string, { input: unknown; output: unknown }>;
|
|
1556
|
+
}): LintDiagnostic[] {
|
|
1557
|
+
const diagnostics: LintDiagnostic[] = [];
|
|
1558
|
+
const providerLabel = provider.id ? `Provider "${provider.id}"` : "Provider";
|
|
1559
|
+
const kinds = readHandleKindDeclarations(provider.handle);
|
|
1560
|
+
const kindByName = new Map(kinds.map((kind) => [kind.name, kind]));
|
|
1561
|
+
const operations = provider.operations ?? {};
|
|
1562
|
+
|
|
1563
|
+
if (kinds.length > 0 && !provider.state) {
|
|
1564
|
+
diagnostics.push({
|
|
1565
|
+
rule: "handle-requires-state",
|
|
1566
|
+
level: "warn",
|
|
1567
|
+
field: "handle",
|
|
1568
|
+
message: `${providerLabel} declares handle kinds but not the state capability; ctx.handle stores records through ctx.state, so declare state: true.`,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
const kindsInInputs = new Set<string>();
|
|
1573
|
+
const kindsInOutputs = new Set<string>();
|
|
1574
|
+
const outputKindsByOperation = new Map<string, Set<string>>();
|
|
1575
|
+
|
|
1576
|
+
for (const [operationKey, operation] of Object.entries(operations)) {
|
|
1577
|
+
const positions = [
|
|
1578
|
+
{ side: "input", fields: collectHandleFields(operation.input, "input") },
|
|
1579
|
+
{ side: "output", fields: collectHandleFields(operation.output, "output") },
|
|
1580
|
+
] as const;
|
|
1581
|
+
for (const { side, fields } of positions) {
|
|
1582
|
+
for (const occurrence of fields) {
|
|
1583
|
+
const field = `operations.${operationKey}.${occurrence.path}`;
|
|
1584
|
+
const { kind, fieldName } = occurrence.meta;
|
|
1585
|
+
|
|
1586
|
+
if (!kindByName.has(kind)) {
|
|
1587
|
+
diagnostics.push({
|
|
1588
|
+
rule: "handle-kind-undeclared",
|
|
1589
|
+
level: "error",
|
|
1590
|
+
field,
|
|
1591
|
+
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.`,
|
|
1592
|
+
});
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
if (occurrence.key !== fieldName) {
|
|
1596
|
+
diagnostics.push({
|
|
1597
|
+
rule: "handle-field-name",
|
|
1598
|
+
level: "error",
|
|
1599
|
+
field,
|
|
1600
|
+
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.`,
|
|
1601
|
+
});
|
|
1602
|
+
}
|
|
1603
|
+
|
|
1604
|
+
if (side === "input") {
|
|
1605
|
+
kindsInInputs.add(kind);
|
|
1606
|
+
} else {
|
|
1607
|
+
kindsInOutputs.add(kind);
|
|
1608
|
+
let outputKinds = outputKindsByOperation.get(operationKey);
|
|
1609
|
+
if (!outputKinds) {
|
|
1610
|
+
outputKinds = new Set<string>();
|
|
1611
|
+
outputKindsByOperation.set(operationKey, outputKinds);
|
|
1612
|
+
}
|
|
1613
|
+
outputKinds.add(kind);
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1617
|
+
}
|
|
1618
|
+
|
|
1619
|
+
for (const kind of kinds) {
|
|
1620
|
+
const field = `handle.${kind.name}`;
|
|
1621
|
+
const issued = kindsInOutputs.has(kind.name);
|
|
1622
|
+
const accepted = kindsInInputs.has(kind.name);
|
|
1623
|
+
|
|
1624
|
+
if (!issued) {
|
|
1625
|
+
diagnostics.push({
|
|
1626
|
+
rule: "handle-field-parity",
|
|
1627
|
+
level: "error",
|
|
1628
|
+
field,
|
|
1629
|
+
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.`,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
if (!accepted) {
|
|
1633
|
+
diagnostics.push({
|
|
1634
|
+
rule: "handle-field-parity",
|
|
1635
|
+
level: "error",
|
|
1636
|
+
field,
|
|
1637
|
+
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.`,
|
|
1638
|
+
});
|
|
1639
|
+
}
|
|
1640
|
+
|
|
1641
|
+
if (kind.issuedBy === undefined) continue;
|
|
1642
|
+
const issuers = handleIssuerList(kind.issuedBy);
|
|
1643
|
+
const known = issuers.filter((operationKey) => Object.hasOwn(operations, operationKey));
|
|
1644
|
+
for (const operationKey of issuers) {
|
|
1645
|
+
if (known.includes(operationKey)) continue;
|
|
1646
|
+
diagnostics.push({
|
|
1647
|
+
rule: "handle-issued-by",
|
|
1648
|
+
level: "error",
|
|
1649
|
+
field,
|
|
1650
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${operationKey}", which is not an operation key.`,
|
|
1651
|
+
});
|
|
1652
|
+
}
|
|
1653
|
+
if (known.length === 0) continue;
|
|
1654
|
+
const issuing = known.filter((operationKey) =>
|
|
1655
|
+
outputKindsByOperation.get(operationKey)?.has(kind.name),
|
|
1656
|
+
);
|
|
1657
|
+
if (issuing.length > 0) continue;
|
|
1658
|
+
if (known.length === 1) {
|
|
1659
|
+
diagnostics.push({
|
|
1660
|
+
rule: "handle-issued-by",
|
|
1661
|
+
level: "error",
|
|
1662
|
+
field,
|
|
1663
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy "${known[0]}", but that operation's output has no "${kind.fieldName}" handle field.`,
|
|
1664
|
+
});
|
|
1665
|
+
} else {
|
|
1666
|
+
diagnostics.push({
|
|
1667
|
+
rule: "handle-issued-by",
|
|
1668
|
+
level: "error",
|
|
1669
|
+
field,
|
|
1670
|
+
message: `${providerLabel} handle kind "${kind.name}" declares issuedBy ${known
|
|
1671
|
+
.map((operationKey) => `"${operationKey}"`)
|
|
1672
|
+
.join(
|
|
1673
|
+
", ",
|
|
1674
|
+
)}, but none of those operations' outputs has a "${kind.fieldName}" handle field. At least one listed issuer must output it.`,
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
return diagnostics;
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
const LEGACY_CHOICE_USAGE_PATTERN =
|
|
1683
|
+
/\bctx\.choice\b|\b(?:createProviderChoiceToken|parseProviderChoiceToken|ProviderChoiceTokenError|createTestProviderChoiceContext)\b/;
|
|
1684
|
+
|
|
1685
|
+
const LEGACY_CHOICE_USAGE_MESSAGE =
|
|
1686
|
+
"Choice tokens were replaced by handles (ADR-0012). See docs/migrations/handle.md.";
|
|
1687
|
+
|
|
1688
|
+
/**
|
|
1689
|
+
* The inline choice-token API was removed outright (no aliases). Any surviving
|
|
1690
|
+
* reference in provider source would fail at runtime, so report it at check time.
|
|
1691
|
+
*/
|
|
1692
|
+
const LEGACY_CHOICE_IDENTIFIERS = new Set([
|
|
1693
|
+
"createProviderChoiceToken",
|
|
1694
|
+
"parseProviderChoiceToken",
|
|
1695
|
+
"ProviderChoiceTokenError",
|
|
1696
|
+
"createTestProviderChoiceContext",
|
|
1697
|
+
]);
|
|
1698
|
+
|
|
1699
|
+
/**
|
|
1700
|
+
* Finds the first legacy choice API reference in executable code. Uses the
|
|
1701
|
+
* TypeScript AST when available so comments, string and template literal text
|
|
1702
|
+
* never count (a migration note such as `// moved from ctx.choice` must not fail
|
|
1703
|
+
* the provider) while code inside `${}` interpolations still does. Falls back
|
|
1704
|
+
* to a raw-source regex when TypeScript is not installed.
|
|
1705
|
+
*/
|
|
1706
|
+
function findLegacyChoiceUsage(source: string): string | undefined {
|
|
1707
|
+
let ts: typeof import("typescript");
|
|
1708
|
+
try {
|
|
1709
|
+
ts = getTypeScript();
|
|
1710
|
+
} catch {
|
|
1711
|
+
return LEGACY_CHOICE_USAGE_PATTERN.exec(source)?.[0];
|
|
1712
|
+
}
|
|
1713
|
+
const file = ts.createSourceFile(
|
|
1714
|
+
"provider.ts",
|
|
1715
|
+
source,
|
|
1716
|
+
ts.ScriptTarget.Latest,
|
|
1717
|
+
false,
|
|
1718
|
+
ts.ScriptKind.TSX,
|
|
1719
|
+
);
|
|
1720
|
+
let found: string | undefined;
|
|
1721
|
+
const visit = (node: import("typescript").Node): void => {
|
|
1722
|
+
if (found) return;
|
|
1723
|
+
if (ts.isIdentifier(node) && LEGACY_CHOICE_IDENTIFIERS.has(node.text)) {
|
|
1724
|
+
found = node.text;
|
|
1725
|
+
return;
|
|
1726
|
+
}
|
|
1727
|
+
if (
|
|
1728
|
+
ts.isPropertyAccessExpression(node) &&
|
|
1729
|
+
ts.isIdentifier(node.expression) &&
|
|
1730
|
+
node.expression.text === "ctx" &&
|
|
1731
|
+
node.name.text === "choice"
|
|
1732
|
+
) {
|
|
1733
|
+
found = "ctx.choice";
|
|
1734
|
+
return;
|
|
1735
|
+
}
|
|
1736
|
+
ts.forEachChild(node, visit);
|
|
1737
|
+
};
|
|
1738
|
+
visit(file);
|
|
1739
|
+
return found;
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
function lintLegacyChoiceUsage(provider: ProviderSourceLike): LintDiagnostic[] {
|
|
1743
|
+
const sources: Array<{ field: string; source: string }> = [];
|
|
1744
|
+
|
|
1745
|
+
if (provider.authFlowSource) {
|
|
1746
|
+
sources.push({ field: "auth.flow", source: provider.authFlowSource });
|
|
1747
|
+
}
|
|
1748
|
+
for (const [filePath, source] of Object.entries(provider.providerSourceFiles ?? {})) {
|
|
1749
|
+
sources.push({ field: `sourceFiles.${filePath}`, source });
|
|
1750
|
+
}
|
|
1751
|
+
for (const [operationKey, operation] of Object.entries(provider.operations ?? {})) {
|
|
1752
|
+
const source = getOperationSource(operation);
|
|
1753
|
+
if (source) {
|
|
1754
|
+
sources.push({ field: `operations.${operationKey}.handler`, source });
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
return sources.flatMap(({ field, source }) => {
|
|
1759
|
+
const match = findLegacyChoiceUsage(source);
|
|
1760
|
+
if (!match) return [];
|
|
1761
|
+
return [
|
|
1762
|
+
{
|
|
1763
|
+
rule: "legacy-choice-usage",
|
|
1764
|
+
level: "error" as const,
|
|
1765
|
+
field,
|
|
1766
|
+
message: `Legacy choice API "${match}" is no longer supported. ${LEGACY_CHOICE_USAGE_MESSAGE}`,
|
|
1767
|
+
},
|
|
1768
|
+
];
|
|
1769
|
+
});
|
|
1770
|
+
}
|
|
1771
|
+
|
|
1391
1772
|
export function lintOperation(op: {
|
|
1392
1773
|
descriptionKey?: string;
|
|
1393
1774
|
whenToUseKeys?: readonly string[];
|
|
@@ -1541,6 +1922,9 @@ export function lintProvider(
|
|
|
1541
1922
|
contract?: ProviderContractMetaLike;
|
|
1542
1923
|
};
|
|
1543
1924
|
reviewed?: string;
|
|
1925
|
+
/** Declared handle kinds (ADR-0012); read structurally, see readHandleKindDeclarations. */
|
|
1926
|
+
handle?: unknown;
|
|
1927
|
+
state?: unknown;
|
|
1544
1928
|
},
|
|
1545
1929
|
options: ProviderLintOptions = {},
|
|
1546
1930
|
): LintDiagnostic[] {
|
|
@@ -1562,6 +1946,8 @@ export function lintProviderWithInformation(
|
|
|
1562
1946
|
...lintSelfHostedBrowserPatterns(provider, options),
|
|
1563
1947
|
...lintBrowserVersionLiterals(provider),
|
|
1564
1948
|
...lintUndeclaredThrownErrorCodes(provider),
|
|
1949
|
+
...lintLegacyChoiceUsage(provider),
|
|
1950
|
+
...lintHandleDeclarations(provider),
|
|
1565
1951
|
];
|
|
1566
1952
|
|
|
1567
1953
|
if (provider.operations) {
|
|
@@ -1591,11 +1977,7 @@ export function lintProviderWithInformation(
|
|
|
1591
1977
|
// Every authenticated mode owns an auth.flow; `oauth2_proxied` was
|
|
1592
1978
|
// previously exempt, which let auth-lifecycle operations ship on
|
|
1593
1979
|
// proxied providers unchecked.
|
|
1594
|
-
if (
|
|
1595
|
-
authMode === "credentials" ||
|
|
1596
|
-
authMode === "oauth2" ||
|
|
1597
|
-
authMode === "oauth2_proxied"
|
|
1598
|
-
) {
|
|
1980
|
+
if (authMode === "credentials" || authMode === "oauth2" || authMode === "oauth2_proxied") {
|
|
1599
1981
|
for (const operationKey of Object.keys(provider.operations)) {
|
|
1600
1982
|
if (isAuthLifecycleOperationId(operationKey, authMode)) {
|
|
1601
1983
|
diagnostics.push({
|
package/src/provider.ts
CHANGED
|
@@ -18,14 +18,6 @@ export type {
|
|
|
18
18
|
DefinedCredentialsAuth,
|
|
19
19
|
} from "./auth.js";
|
|
20
20
|
export { createFormCeremony } from "./ceremonies/index.js";
|
|
21
|
-
export {
|
|
22
|
-
assertFreshProviderChoiceIssuedAt,
|
|
23
|
-
createProviderChoiceToken,
|
|
24
|
-
ProviderChoiceTokenError,
|
|
25
|
-
type ProviderChoiceTokenErrorReason,
|
|
26
|
-
type ProviderChoiceTokenPayload,
|
|
27
|
-
parseProviderChoiceToken,
|
|
28
|
-
} from "./choice-token.js";
|
|
29
21
|
export {
|
|
30
22
|
centered,
|
|
31
23
|
delayed,
|
|
@@ -161,12 +153,26 @@ export {
|
|
|
161
153
|
providerLocaleKey,
|
|
162
154
|
qualifyProviderLocaleKey,
|
|
163
155
|
} from "./i18n/index.js";
|
|
156
|
+
export * from "./handle.js";
|
|
157
|
+
export {
|
|
158
|
+
APIFUSE_HANDLE_META_KEY,
|
|
159
|
+
HANDLE_KIND_NAME_PATTERN,
|
|
160
|
+
handleFieldDescription,
|
|
161
|
+
isHandleFieldMeta,
|
|
162
|
+
} from "./handle-meta.js";
|
|
163
|
+
export {
|
|
164
|
+
type CreateHandleContextOptions,
|
|
165
|
+
type CreateTestHandleContextOptions,
|
|
166
|
+
createHandleContext,
|
|
167
|
+
createTestHandleContext,
|
|
168
|
+
type HandleNormalizationClass,
|
|
169
|
+
type NormalizedHandle,
|
|
170
|
+
normalizeHandle,
|
|
171
|
+
} from "./runtime/handle.js";
|
|
164
172
|
export {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
|
|
169
|
-
} from "./runtime/choice.js";
|
|
173
|
+
createMemoryProviderRuntimeState,
|
|
174
|
+
createUnsupportedProviderRuntimeState,
|
|
175
|
+
} from "./runtime/state.js";
|
|
170
176
|
export {
|
|
171
177
|
APIFUSE_CONTENT_PROVENANCE_META_KEY,
|
|
172
178
|
APIFUSE_CONTENT_TRUST_META_KEY,
|
|
@@ -245,13 +251,6 @@ export type {
|
|
|
245
251
|
OperationSensitivePath,
|
|
246
252
|
OperationTransport,
|
|
247
253
|
ProviderAccessVisibility,
|
|
248
|
-
ProviderChoiceBindingOptions,
|
|
249
|
-
ProviderChoiceConsumeMode,
|
|
250
|
-
ProviderChoiceConsumeResult,
|
|
251
|
-
ProviderChoiceContext,
|
|
252
|
-
ProviderChoiceExplicitParseResult,
|
|
253
|
-
ProviderChoiceIssueOptions,
|
|
254
|
-
ProviderChoiceParseOptions,
|
|
255
254
|
ProviderContext,
|
|
256
255
|
ProviderContextFor,
|
|
257
256
|
ProviderDefinition,
|