@apifuse/provider-sdk 2.2.0-beta.41 → 2.2.0-beta.42
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +4 -0
- package/bin/apifuse-migrate-shape.ts +141 -23
- package/bin/apifuse-submit-check.ts +99 -86
- package/dist/cli/commands.js +1 -1
- package/dist/cli/migrate-operation-shape.d.ts +44 -0
- package/dist/cli/migrate-operation-shape.js +113 -0
- package/dist/cli/migrate-provider-shape.js +64 -1
- package/dist/server/serve-implementation.js +40 -1
- package/package.json +3 -3
- package/src/cli/commands.ts +1 -1
- package/src/cli/migrate-operation-shape.ts +184 -0
- package/src/cli/migrate-provider-shape.ts +107 -36
- package/src/server/serve-implementation.ts +43 -1
|
@@ -1,4 +1,17 @@
|
|
|
1
|
-
import
|
|
1
|
+
import type TS from "typescript";
|
|
2
|
+
|
|
3
|
+
const ts: typeof import("typescript") = await loadTypeScript();
|
|
4
|
+
|
|
5
|
+
async function loadTypeScript(): Promise<typeof import("typescript")> {
|
|
6
|
+
try {
|
|
7
|
+
return await import("typescript");
|
|
8
|
+
} catch {
|
|
9
|
+
console.error(
|
|
10
|
+
"apifuse migrate-shape requires typescript; install it in the workspace running the CLI (bun add -d typescript)",
|
|
11
|
+
);
|
|
12
|
+
process.exit(1);
|
|
13
|
+
}
|
|
14
|
+
}
|
|
2
15
|
|
|
3
16
|
/**
|
|
4
17
|
* Provider authoring shape migration.
|
|
@@ -207,16 +220,16 @@ type ShapeClassification =
|
|
|
207
220
|
readonly kind: Exclude<ProviderShapeKind, "two-phase">;
|
|
208
221
|
/** Variable the declaration is currently bound to, when it is bound. */
|
|
209
222
|
readonly variableName?: string;
|
|
210
|
-
readonly variableStatement?:
|
|
223
|
+
readonly variableStatement?: TS.VariableStatement;
|
|
211
224
|
/** Extra properties on a `{ ...provider, deployment }` default export. */
|
|
212
225
|
readonly spreadExportExtras?: string;
|
|
213
226
|
}
|
|
214
227
|
| { readonly status: "skipped"; readonly reason: string };
|
|
215
228
|
|
|
216
229
|
function classifyShape(
|
|
217
|
-
source:
|
|
218
|
-
call:
|
|
219
|
-
exportAssignment:
|
|
230
|
+
source: TS.SourceFile,
|
|
231
|
+
call: TS.CallExpression,
|
|
232
|
+
exportAssignment: TS.ExportAssignment,
|
|
220
233
|
): ShapeClassification {
|
|
221
234
|
const variableStatement = enclosingVariableStatement(call);
|
|
222
235
|
|
|
@@ -307,9 +320,9 @@ function classifyShape(
|
|
|
307
320
|
};
|
|
308
321
|
}
|
|
309
322
|
|
|
310
|
-
function collectDefineProviderCalls(source:
|
|
311
|
-
const calls:
|
|
312
|
-
const visit = (node:
|
|
323
|
+
function collectDefineProviderCalls(source: TS.SourceFile): TS.CallExpression[] {
|
|
324
|
+
const calls: TS.CallExpression[] = [];
|
|
325
|
+
const visit = (node: TS.Node): void => {
|
|
313
326
|
if (
|
|
314
327
|
ts.isCallExpression(node) &&
|
|
315
328
|
ts.isIdentifier(node.expression) &&
|
|
@@ -323,7 +336,7 @@ function collectDefineProviderCalls(source: ts.SourceFile): ts.CallExpression[]
|
|
|
323
336
|
return calls;
|
|
324
337
|
}
|
|
325
338
|
|
|
326
|
-
function findDefaultExport(source:
|
|
339
|
+
function findDefaultExport(source: TS.SourceFile): TS.ExportAssignment | undefined {
|
|
327
340
|
for (const statement of source.statements) {
|
|
328
341
|
if (ts.isExportAssignment(statement) && statement.isExportEquals !== true) {
|
|
329
342
|
return statement;
|
|
@@ -333,8 +346,8 @@ function findDefaultExport(source: ts.SourceFile): ts.ExportAssignment | undefin
|
|
|
333
346
|
}
|
|
334
347
|
|
|
335
348
|
function defaultExportCallsBuilder(
|
|
336
|
-
exportAssignment:
|
|
337
|
-
declarationCall:
|
|
349
|
+
exportAssignment: TS.ExportAssignment,
|
|
350
|
+
declarationCall: TS.CallExpression,
|
|
338
351
|
): boolean {
|
|
339
352
|
const expression = exportAssignment.expression;
|
|
340
353
|
if (!ts.isCallExpression(expression)) return false;
|
|
@@ -353,9 +366,9 @@ function defaultExportCallsBuilder(
|
|
|
353
366
|
* breaks idempotency for repeated fan-out runs.
|
|
354
367
|
*/
|
|
355
368
|
function isAlreadyTwoPhase(
|
|
356
|
-
source:
|
|
357
|
-
exportAssignment:
|
|
358
|
-
declarationCall:
|
|
369
|
+
source: TS.SourceFile,
|
|
370
|
+
exportAssignment: TS.ExportAssignment,
|
|
371
|
+
declarationCall: TS.CallExpression,
|
|
359
372
|
): boolean {
|
|
360
373
|
if (defaultExportCallsBuilder(exportAssignment, declarationCall)) return true;
|
|
361
374
|
|
|
@@ -375,7 +388,7 @@ function isAlreadyTwoPhase(
|
|
|
375
388
|
// Does any spread/exported identifier bind a call to an identifier other
|
|
376
389
|
// than defineProvider — i.e. a builder call?
|
|
377
390
|
let found = false;
|
|
378
|
-
const visit = (node:
|
|
391
|
+
const visit = (node: TS.Node): void => {
|
|
379
392
|
if (found) return;
|
|
380
393
|
if (
|
|
381
394
|
ts.isVariableDeclaration(node) &&
|
|
@@ -397,8 +410,8 @@ function isAlreadyTwoPhase(
|
|
|
397
410
|
}
|
|
398
411
|
|
|
399
412
|
function findOperationsProperty(
|
|
400
|
-
declaration:
|
|
401
|
-
):
|
|
413
|
+
declaration: TS.ObjectLiteralExpression,
|
|
414
|
+
): TS.ObjectLiteralElementLike | undefined {
|
|
402
415
|
for (const property of declaration.properties) {
|
|
403
416
|
if (ts.isSpreadAssignment(property)) continue;
|
|
404
417
|
const name = property.name;
|
|
@@ -419,8 +432,8 @@ function findOperationsProperty(
|
|
|
419
432
|
* its initializer verbatim, including a multi-line inline map.
|
|
420
433
|
*/
|
|
421
434
|
function operationsPropertyValueText(
|
|
422
|
-
property:
|
|
423
|
-
source:
|
|
435
|
+
property: TS.ObjectLiteralElementLike,
|
|
436
|
+
source: TS.SourceFile,
|
|
424
437
|
): string | undefined {
|
|
425
438
|
if (ts.isShorthandPropertyAssignment(property)) {
|
|
426
439
|
return property.name.text;
|
|
@@ -432,9 +445,9 @@ function operationsPropertyValueText(
|
|
|
432
445
|
}
|
|
433
446
|
|
|
434
447
|
function removeOperationsProperty(
|
|
435
|
-
property:
|
|
436
|
-
declaration:
|
|
437
|
-
source:
|
|
448
|
+
property: TS.ObjectLiteralElementLike,
|
|
449
|
+
declaration: TS.ObjectLiteralExpression,
|
|
450
|
+
source: TS.SourceFile,
|
|
438
451
|
): TextEdit[] {
|
|
439
452
|
const properties = declaration.properties;
|
|
440
453
|
const index = properties.indexOf(property);
|
|
@@ -464,8 +477,8 @@ function removeOperationsProperty(
|
|
|
464
477
|
}
|
|
465
478
|
|
|
466
479
|
function introduceBuilder(
|
|
467
|
-
source:
|
|
468
|
-
call:
|
|
480
|
+
source: TS.SourceFile,
|
|
481
|
+
call: TS.CallExpression,
|
|
469
482
|
shape: Extract<ShapeClassification, { status: "ok" }>,
|
|
470
483
|
builderName: string,
|
|
471
484
|
): TextEdit[] {
|
|
@@ -501,8 +514,8 @@ function introduceBuilder(
|
|
|
501
514
|
}
|
|
502
515
|
|
|
503
516
|
function rewriteDefaultExport(
|
|
504
|
-
source:
|
|
505
|
-
exportAssignment:
|
|
517
|
+
source: TS.SourceFile,
|
|
518
|
+
exportAssignment: TS.ExportAssignment,
|
|
506
519
|
shape: Extract<ShapeClassification, { status: "ok" }>,
|
|
507
520
|
builderName: string,
|
|
508
521
|
operationsText: string,
|
|
@@ -564,8 +577,8 @@ function rewriteDefaultExport(
|
|
|
564
577
|
* type the two-phase shape exists to provide.
|
|
565
578
|
*/
|
|
566
579
|
function ensureProviderContextType(
|
|
567
|
-
source:
|
|
568
|
-
call:
|
|
580
|
+
source: TS.SourceFile,
|
|
581
|
+
call: TS.CallExpression,
|
|
569
582
|
shape: Extract<ShapeClassification, { status: "ok" }>,
|
|
570
583
|
builderName: string,
|
|
571
584
|
): TextEdit[] {
|
|
@@ -577,6 +590,15 @@ function ensureProviderContextType(
|
|
|
577
590
|
ts.isTypeAliasDeclaration(statement) &&
|
|
578
591
|
statement.name.text === PROVIDER_CONTEXT_TYPE_NAME,
|
|
579
592
|
);
|
|
593
|
+
// A named import of `ProviderContext` (the deprecated SDK-root context
|
|
594
|
+
// type, imported by legacy sources) occupies the same name: adding the
|
|
595
|
+
// alias alongside it is TS2440. The import must yield to the derived
|
|
596
|
+
// alias — the whole point of the two-phase shape — so drop it and let the
|
|
597
|
+
// alias own the name.
|
|
598
|
+
const conflictingImportSpecifier = findNamedImportSpecifier(
|
|
599
|
+
source,
|
|
600
|
+
PROVIDER_CONTEXT_TYPE_NAME,
|
|
601
|
+
);
|
|
580
602
|
|
|
581
603
|
if (!hasContextTypeAlias) {
|
|
582
604
|
// After the builder statement — which is the variable statement when the
|
|
@@ -591,6 +613,9 @@ function ensureProviderContextType(
|
|
|
591
613
|
end: insertAt,
|
|
592
614
|
text: `\n\nexport type ${PROVIDER_CONTEXT_TYPE_NAME} = ${PROVIDER_CONTEXT_OF_TYPE_NAME}<typeof ${builderName}>;`,
|
|
593
615
|
});
|
|
616
|
+
if (conflictingImportSpecifier !== undefined) {
|
|
617
|
+
edits.push(removeImportSpecifier(source, conflictingImportSpecifier));
|
|
618
|
+
}
|
|
594
619
|
}
|
|
595
620
|
}
|
|
596
621
|
|
|
@@ -622,8 +647,8 @@ function ensureProviderContextType(
|
|
|
622
647
|
}
|
|
623
648
|
|
|
624
649
|
function findProviderSdkImport(
|
|
625
|
-
source:
|
|
626
|
-
):
|
|
650
|
+
source: TS.SourceFile,
|
|
651
|
+
): TS.ImportDeclaration | undefined {
|
|
627
652
|
for (const statement of source.statements) {
|
|
628
653
|
if (!ts.isImportDeclaration(statement)) continue;
|
|
629
654
|
const moduleSpecifier = statement.moduleSpecifier;
|
|
@@ -640,13 +665,59 @@ function findProviderSdkImport(
|
|
|
640
665
|
return undefined;
|
|
641
666
|
}
|
|
642
667
|
|
|
668
|
+
/** Named import specifier binding `localName` in any import declaration. */
|
|
669
|
+
function findNamedImportSpecifier(
|
|
670
|
+
source: TS.SourceFile,
|
|
671
|
+
localName: string,
|
|
672
|
+
): TS.ImportSpecifier | undefined {
|
|
673
|
+
for (const statement of source.statements) {
|
|
674
|
+
if (!ts.isImportDeclaration(statement)) continue;
|
|
675
|
+
const named = statement.importClause?.namedBindings;
|
|
676
|
+
if (named === undefined || !ts.isNamedImports(named)) continue;
|
|
677
|
+
for (const element of named.elements) {
|
|
678
|
+
if (element.name.text === localName) return element;
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
return undefined;
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
/**
|
|
685
|
+
* Edit removing one specifier from its named-import list, absorbing one
|
|
686
|
+
* neighboring comma so the list stays well-formed. Callers guarantee the
|
|
687
|
+
* list has at least one other specifier (legacy sources always import
|
|
688
|
+
* defineProvider alongside the context type).
|
|
689
|
+
*/
|
|
690
|
+
function removeImportSpecifier(
|
|
691
|
+
source: TS.SourceFile,
|
|
692
|
+
specifier: TS.ImportSpecifier,
|
|
693
|
+
): TextEdit {
|
|
694
|
+
const list = specifier.parent;
|
|
695
|
+
const index = list.elements.indexOf(specifier);
|
|
696
|
+
const text = source.getFullText();
|
|
697
|
+
let start = specifier.getFullStart();
|
|
698
|
+
let end = specifier.getEnd();
|
|
699
|
+
let cursor = end;
|
|
700
|
+
while (cursor < text.length && /\s/.test(text.charAt(cursor))) cursor += 1;
|
|
701
|
+
if (text.charAt(cursor) === ",") {
|
|
702
|
+
end = cursor + 1;
|
|
703
|
+
} else if (index > 0) {
|
|
704
|
+
const previous = list.elements[index - 1];
|
|
705
|
+
if (previous !== undefined) {
|
|
706
|
+
let back = previous.getEnd();
|
|
707
|
+
while (back < text.length && /\s/.test(text.charAt(back))) back += 1;
|
|
708
|
+
if (text.charAt(back) === ",") start = back;
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
return { start, end, text: "" };
|
|
712
|
+
}
|
|
713
|
+
|
|
643
714
|
/**
|
|
644
715
|
* `buildProvider` unless the module already binds that name, in which case a
|
|
645
716
|
* numbered suffix keeps the transform from shadowing an existing binding.
|
|
646
717
|
*/
|
|
647
|
-
function pickBuilderName(source:
|
|
718
|
+
function pickBuilderName(source: TS.SourceFile): string {
|
|
648
719
|
const taken = new Set<string>();
|
|
649
|
-
const visit = (node:
|
|
720
|
+
const visit = (node: TS.Node): void => {
|
|
650
721
|
if (ts.isVariableDeclaration(node) && ts.isIdentifier(node.name)) {
|
|
651
722
|
taken.add(node.name.text);
|
|
652
723
|
}
|
|
@@ -668,8 +739,8 @@ function pickBuilderName(source: ts.SourceFile): string {
|
|
|
668
739
|
return `${DECLARATION_BUILDER_NAME}Migrated`;
|
|
669
740
|
}
|
|
670
741
|
|
|
671
|
-
function enclosingVariableStatement(node:
|
|
672
|
-
let current:
|
|
742
|
+
function enclosingVariableStatement(node: TS.Node): TS.VariableStatement | undefined {
|
|
743
|
+
let current: TS.Node | undefined = node.parent;
|
|
673
744
|
while (current !== undefined) {
|
|
674
745
|
if (ts.isVariableStatement(current)) return current;
|
|
675
746
|
if (ts.isSourceFile(current)) return undefined;
|
|
@@ -678,9 +749,9 @@ function enclosingVariableStatement(node: ts.Node): ts.VariableStatement | undef
|
|
|
678
749
|
return undefined;
|
|
679
750
|
}
|
|
680
751
|
|
|
681
|
-
function firstSyntaxError(source:
|
|
752
|
+
function firstSyntaxError(source: TS.SourceFile): string | undefined {
|
|
682
753
|
const diagnostics = (
|
|
683
|
-
source as
|
|
754
|
+
source as TS.SourceFile & { parseDiagnostics?: TS.DiagnosticWithLocation[] }
|
|
684
755
|
).parseDiagnostics;
|
|
685
756
|
if (diagnostics === undefined || diagnostics.length === 0) return undefined;
|
|
686
757
|
const first = diagnostics[0];
|
|
@@ -23,7 +23,10 @@ import {
|
|
|
23
23
|
type ProviderErrorObservability,
|
|
24
24
|
type ProviderErrorOptions,
|
|
25
25
|
} from "../errors.js";
|
|
26
|
-
import {
|
|
26
|
+
import {
|
|
27
|
+
REDACTED_FIXTURE_VALUE,
|
|
28
|
+
sanitizeDiagnosticText,
|
|
29
|
+
} from "../fixture-sanitization.js";
|
|
27
30
|
import {
|
|
28
31
|
loadProviderLocaleCatalogs,
|
|
29
32
|
localizeAuthTurn,
|
|
@@ -1519,9 +1522,48 @@ export type ProviderErrorCauseFrame = {
|
|
|
1519
1522
|
|
|
1520
1523
|
const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
|
|
1521
1524
|
const MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH = 300;
|
|
1525
|
+
const UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE = "[UNSTRUCTURED_UPSTREAM_TEXT]";
|
|
1526
|
+
const PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN = /https?:\/\/[^\s"'<>]+/giu;
|
|
1527
|
+
const PROVIDER_ERROR_CAUSE_TOKEN_RUN = /\S+/gu;
|
|
1528
|
+
const PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION =
|
|
1529
|
+
/^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu;
|
|
1530
|
+
const STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS = new Set([
|
|
1531
|
+
"completion",
|
|
1532
|
+
"diagnostic",
|
|
1533
|
+
"provider",
|
|
1534
|
+
"rejected",
|
|
1535
|
+
"returned",
|
|
1536
|
+
"upstream",
|
|
1537
|
+
]);
|
|
1538
|
+
|
|
1539
|
+
/**
|
|
1540
|
+
* Cause frames fail closed when sanitization leaves a plausible credential-shaped free-text run.
|
|
1541
|
+
* Redaction sentinels and retained URLs are ignored. Every other whitespace token (or each side
|
|
1542
|
+
* of a structured key=value token) with at least eight Unicode characters must reduce to this
|
|
1543
|
+
* small vocabulary drawn from SDK diagnostics; counting punctuation keeps bare passwords opaque.
|
|
1544
|
+
*/
|
|
1545
|
+
function isStructurallySafeProviderErrorCauseMessage(message: string): boolean {
|
|
1546
|
+
const classifiableMessage = message
|
|
1547
|
+
.replaceAll(REDACTED_FIXTURE_VALUE, " ")
|
|
1548
|
+
.replace(PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN, " ");
|
|
1549
|
+
for (const match of classifiableMessage.matchAll(PROVIDER_ERROR_CAUSE_TOKEN_RUN)) {
|
|
1550
|
+
const token = match[0];
|
|
1551
|
+
for (const run of token.split("=")) {
|
|
1552
|
+
const diagnosticWord = run
|
|
1553
|
+
.replace(PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION, "")
|
|
1554
|
+
.toLowerCase();
|
|
1555
|
+
if (STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS.has(diagnosticWord)) continue;
|
|
1556
|
+
if ([...run].length >= 8) return false;
|
|
1557
|
+
}
|
|
1558
|
+
}
|
|
1559
|
+
return true;
|
|
1560
|
+
}
|
|
1522
1561
|
|
|
1523
1562
|
function providerErrorCauseMessage(message: string): string {
|
|
1524
1563
|
const sanitized = sanitizeDiagnosticText(message);
|
|
1564
|
+
if (!isStructurallySafeProviderErrorCauseMessage(sanitized)) {
|
|
1565
|
+
return UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE;
|
|
1566
|
+
}
|
|
1525
1567
|
return sanitized.length > MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH
|
|
1526
1568
|
? `${sanitized.slice(0, MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH)}… [truncated]`
|
|
1527
1569
|
: sanitized;
|