@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.
@@ -1,4 +1,17 @@
1
- import ts from "typescript";
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?: ts.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: ts.SourceFile,
218
- call: ts.CallExpression,
219
- exportAssignment: ts.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: ts.SourceFile): ts.CallExpression[] {
311
- const calls: ts.CallExpression[] = [];
312
- const visit = (node: ts.Node): void => {
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: ts.SourceFile): ts.ExportAssignment | undefined {
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: ts.ExportAssignment,
337
- declarationCall: ts.CallExpression,
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: ts.SourceFile,
357
- exportAssignment: ts.ExportAssignment,
358
- declarationCall: ts.CallExpression,
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: ts.Node): void => {
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: ts.ObjectLiteralExpression,
401
- ): ts.ObjectLiteralElementLike | undefined {
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: ts.ObjectLiteralElementLike,
423
- source: ts.SourceFile,
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: ts.ObjectLiteralElementLike,
436
- declaration: ts.ObjectLiteralExpression,
437
- source: ts.SourceFile,
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: ts.SourceFile,
468
- call: ts.CallExpression,
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: ts.SourceFile,
505
- exportAssignment: ts.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: ts.SourceFile,
568
- call: ts.CallExpression,
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: ts.SourceFile,
626
- ): ts.ImportDeclaration | undefined {
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: ts.SourceFile): string {
718
+ function pickBuilderName(source: TS.SourceFile): string {
648
719
  const taken = new Set<string>();
649
- const visit = (node: ts.Node): void => {
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: ts.Node): ts.VariableStatement | undefined {
672
- let current: ts.Node | undefined = node.parent;
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: ts.SourceFile): string | undefined {
752
+ function firstSyntaxError(source: TS.SourceFile): string | undefined {
682
753
  const diagnostics = (
683
- source as ts.SourceFile & { parseDiagnostics?: ts.DiagnosticWithLocation[] }
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 { sanitizeDiagnosticText } from "../fixture-sanitization.js";
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;