@apifuse/provider-sdk 2.2.0-beta.41 → 2.2.0-beta.43

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.
@@ -0,0 +1,113 @@
1
+ const ts = await loadTypeScript();
2
+ async function loadTypeScript() {
3
+ try {
4
+ return await import("typescript");
5
+ }
6
+ catch {
7
+ console.error("apifuse migrate-shape requires typescript; install it in the workspace running the CLI (bun add -d typescript)");
8
+ process.exit(1);
9
+ }
10
+ }
11
+ const OPERATION_HELPERS = new Set(["defineOperation", "defineStreamOperation"]);
12
+ const PROVIDER_CONTEXT_TYPE_NAME = "ProviderContext";
13
+ const PROVIDER_SDK_PROVIDER_SUBPATH = "@apifuse/provider-sdk/provider";
14
+ /**
15
+ * Migrate legacy `defineOperation(config)` calls in one source file to the
16
+ * curried `defineOperation<ProviderContext>()(config)` form.
17
+ *
18
+ * @param contextImportSpecifier module specifier the `ProviderContext` type
19
+ * import should come from when one has to be added — `"../index"` for
20
+ * operation modules, `"./index"` is never needed because index.ts declares
21
+ * the alias itself. Callers pass the correct relative path per file.
22
+ */
23
+ export function migrateOperationShape(sourceText, fileName, contextImportSpecifier) {
24
+ const source = ts.createSourceFile(fileName, sourceText, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
25
+ const parseError = firstSyntaxError(source);
26
+ if (parseError !== undefined) {
27
+ return { status: "skipped", reason: parseError };
28
+ }
29
+ // Every legacy call site: a direct call to the bare helper identifier
30
+ // whose single argument is the config (i.e. NOT the curried form, whose
31
+ // outer call has zero arguments and optional type arguments).
32
+ const legacyCalls = [];
33
+ let sawCurried = false;
34
+ const visit = (node) => {
35
+ if (ts.isCallExpression(node) &&
36
+ ts.isIdentifier(node.expression) &&
37
+ OPERATION_HELPERS.has(node.expression.text)) {
38
+ if (node.arguments.length === 0) {
39
+ sawCurried = true; // already-migrated outer call
40
+ }
41
+ else {
42
+ legacyCalls.push(node);
43
+ }
44
+ }
45
+ ts.forEachChild(node, visit);
46
+ };
47
+ visit(source);
48
+ if (legacyCalls.length === 0) {
49
+ return { status: "unchanged", code: sourceText };
50
+ }
51
+ // A file mixing both forms almost certainly had a partial hand-migration;
52
+ // refuse rather than guess which convention the author wants.
53
+ if (sawCurried) {
54
+ return {
55
+ status: "skipped",
56
+ reason: "File mixes legacy defineOperation(config) with curried defineOperation<...>()(config); migrate it by hand.",
57
+ };
58
+ }
59
+ const declaresContextAlias = source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) &&
60
+ statement.name.text === PROVIDER_CONTEXT_TYPE_NAME);
61
+ const importsContext = sourceText.includes(PROVIDER_CONTEXT_TYPE_NAME);
62
+ const needsImport = !declaresContextAlias && !importsContext;
63
+ const edits = [];
64
+ for (const call of legacyCalls) {
65
+ // `defineOperation(` -> `defineOperation<ProviderContext>()(`
66
+ const callee = call.expression;
67
+ edits.push({
68
+ start: callee.getEnd(),
69
+ end: callee.getEnd(),
70
+ text: `<${PROVIDER_CONTEXT_TYPE_NAME}>()`,
71
+ });
72
+ }
73
+ let importAdded = false;
74
+ if (needsImport) {
75
+ const lastImport = [...source.statements]
76
+ .reverse()
77
+ .find((statement) => ts.isImportDeclaration(statement));
78
+ const insertAt = lastImport ? lastImport.getEnd() : 0;
79
+ const importText = `${lastImport ? "\n" : ""}import type { ${PROVIDER_CONTEXT_TYPE_NAME} } from "${contextImportSpecifier}";${lastImport ? "" : "\n"}`;
80
+ edits.push({ start: insertAt, end: insertAt, text: importText });
81
+ importAdded = true;
82
+ }
83
+ const ordered = edits.sort((a, b) => b.start - a.start);
84
+ let code = sourceText;
85
+ for (const edit of ordered) {
86
+ code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
87
+ }
88
+ const verified = ts.createSourceFile(fileName, code, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
89
+ const outputError = firstSyntaxError(verified);
90
+ if (outputError !== undefined) {
91
+ return {
92
+ status: "skipped",
93
+ reason: `Transform produced source that does not parse (${outputError}); refusing to emit a partial migration.`,
94
+ };
95
+ }
96
+ return {
97
+ status: "migrated",
98
+ code,
99
+ rewrites: legacyCalls.length,
100
+ importAdded,
101
+ };
102
+ }
103
+ function firstSyntaxError(source) {
104
+ const diagnostics = source.parseDiagnostics;
105
+ if (diagnostics === undefined || diagnostics.length === 0)
106
+ return undefined;
107
+ const first = diagnostics[0];
108
+ if (first === undefined)
109
+ return undefined;
110
+ const message = ts.flattenDiagnosticMessageText(first.messageText, " ");
111
+ const { line } = source.getLineAndCharacterOfPosition(first.start);
112
+ return `${message} (line ${line + 1})`;
113
+ }
@@ -1,4 +1,13 @@
1
- import ts from "typescript";
1
+ const ts = await loadTypeScript();
2
+ async function loadTypeScript() {
3
+ try {
4
+ return await import("typescript");
5
+ }
6
+ catch {
7
+ console.error("apifuse migrate-shape requires typescript; install it in the workspace running the CLI (bun add -d typescript)");
8
+ process.exit(1);
9
+ }
10
+ }
2
11
  const DECLARATION_BUILDER_NAME = "buildProvider";
3
12
  const PROVIDER_CONTEXT_TYPE_NAME = "ProviderContext";
4
13
  const PROVIDER_CONTEXT_OF_TYPE_NAME = "ProviderContextOf";
@@ -397,6 +406,12 @@ function ensureProviderContextType(source, call, shape, builderName) {
397
406
  const edits = [];
398
407
  const hasContextTypeAlias = source.statements.some((statement) => ts.isTypeAliasDeclaration(statement) &&
399
408
  statement.name.text === PROVIDER_CONTEXT_TYPE_NAME);
409
+ // A named import of `ProviderContext` (the deprecated SDK-root context
410
+ // type, imported by legacy sources) occupies the same name: adding the
411
+ // alias alongside it is TS2440. The import must yield to the derived
412
+ // alias — the whole point of the two-phase shape — so drop it and let the
413
+ // alias own the name.
414
+ const conflictingImportSpecifier = findNamedImportSpecifier(source, PROVIDER_CONTEXT_TYPE_NAME);
400
415
  if (!hasContextTypeAlias) {
401
416
  // After the builder statement — which is the variable statement when the
402
417
  // declaration was bound, or the rewritten export statement otherwise.
@@ -409,6 +424,9 @@ function ensureProviderContextType(source, call, shape, builderName) {
409
424
  end: insertAt,
410
425
  text: `\n\nexport type ${PROVIDER_CONTEXT_TYPE_NAME} = ${PROVIDER_CONTEXT_OF_TYPE_NAME}<typeof ${builderName}>;`,
411
426
  });
427
+ if (conflictingImportSpecifier !== undefined) {
428
+ edits.push(removeImportSpecifier(source, conflictingImportSpecifier));
429
+ }
412
430
  }
413
431
  }
414
432
  if (text.includes(PROVIDER_CONTEXT_OF_TYPE_NAME)) {
@@ -454,6 +472,51 @@ function findProviderSdkImport(source) {
454
472
  }
455
473
  return undefined;
456
474
  }
475
+ /** Named import specifier binding `localName` in any import declaration. */
476
+ function findNamedImportSpecifier(source, localName) {
477
+ for (const statement of source.statements) {
478
+ if (!ts.isImportDeclaration(statement))
479
+ continue;
480
+ const named = statement.importClause?.namedBindings;
481
+ if (named === undefined || !ts.isNamedImports(named))
482
+ continue;
483
+ for (const element of named.elements) {
484
+ if (element.name.text === localName)
485
+ return element;
486
+ }
487
+ }
488
+ return undefined;
489
+ }
490
+ /**
491
+ * Edit removing one specifier from its named-import list, absorbing one
492
+ * neighboring comma so the list stays well-formed. Callers guarantee the
493
+ * list has at least one other specifier (legacy sources always import
494
+ * defineProvider alongside the context type).
495
+ */
496
+ function removeImportSpecifier(source, specifier) {
497
+ const list = specifier.parent;
498
+ const index = list.elements.indexOf(specifier);
499
+ const text = source.getFullText();
500
+ let start = specifier.getFullStart();
501
+ let end = specifier.getEnd();
502
+ let cursor = end;
503
+ while (cursor < text.length && /\s/.test(text.charAt(cursor)))
504
+ cursor += 1;
505
+ if (text.charAt(cursor) === ",") {
506
+ end = cursor + 1;
507
+ }
508
+ else if (index > 0) {
509
+ const previous = list.elements[index - 1];
510
+ if (previous !== undefined) {
511
+ let back = previous.getEnd();
512
+ while (back < text.length && /\s/.test(text.charAt(back)))
513
+ back += 1;
514
+ if (text.charAt(back) === ",")
515
+ start = back;
516
+ }
517
+ }
518
+ return { start, end, text: "" };
519
+ }
457
520
  /**
458
521
  * `buildProvider` unless the module already binds that name, in which case a
459
522
  * numbered suffix keeps the transform from shadowing an existing binding.
package/dist/define.d.ts CHANGED
@@ -130,6 +130,10 @@ export type ProviderBuilder<TDeclaration extends ProviderDeclaration> = <TOperat
130
130
  };
131
131
  /** Extract the declaration-derived operation context from a provider builder. */
132
132
  export type ProviderContextOf<TBuilder> = TBuilder extends ProviderBuilder<infer TDeclaration> ? ProviderContextFor<TDeclaration> : never;
133
+ /** Annotate an operation while preserving the declaration-derived context. */
134
+ export type OperationDefinitionFor<TBuilder, TInput extends SchemaLike = SchemaLike, TOutput extends SchemaLike = SchemaLike> = OperationDefinition<TInput, TOutput, ProviderContextOf<TBuilder>>;
135
+ /** Annotate a built provider while preserving the declaration-derived context. */
136
+ export type ProviderDefinitionFor<TBuilder> = ProviderDefinition<ProviderContextOf<TBuilder>>;
133
137
  /** Establish a provider declaration before its operations are contextually typed. */
134
138
  export declare function defineProvider<const TDeclaration extends ProviderDeclaration>(declaration: TDeclaration & Record<Exclude<keyof TDeclaration, keyof ProviderDeclaration>, never> & AuthStartNoInputGuard<TDeclaration>): ProviderBuilder<TDeclaration>;
135
139
  export {};
package/dist/index.d.ts CHANGED
@@ -4,7 +4,7 @@ export * from "./choice-token.js";
4
4
  export type { ApiFuseConfig, BrowserConfig, ProxyCacheStatus, ProxyProtocol, ProxyResolutionOptions, ProxyResolutionSource, ProxyUserAgentSource, ProxyVendorName, ResolvedProxyConfig, SessionConfig, SmartproxyAllocatorBodyClass, } from "./config/loader.js";
5
5
  export { defineConfig, loadApiFuseConfig, resolveProxy } from "./config/loader.js";
6
6
  export { canonicalJson, digestProviderContract, extractProviderContract, type JsonPrimitive, type JsonValue, PROVIDER_CONTRACT_SCHEMA_VERSION, type ProviderContractOperation, type ProviderContractSnapshot, } from "./contract.js";
7
- export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, type AuthStartNoInputGuard, type ProviderBuilder, type ProviderContextOf, type ProviderDeclaration, } from "./define.js";
7
+ export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, defineStreamOperation, every, type AuthStartNoInputGuard, type OperationDefinitionFor, type ProviderBuilder, type ProviderContextOf, type ProviderDeclaration, type ProviderDefinitionFor, } from "./define.js";
8
8
  export { AssertionExpressionSchema, AssertionPredicateSchema, AssertStepSchema, BoundedJsonPathSchema, CandidateBlockSchema, CandidatePolicySchema, CredentialRefDeclarationSchema, defineHealthScenario, HealthScenarioSchema, HealthStepSchema, ExtractStepSchema, FindFirstSchema, GuardStepSchema, JournalPolicySchema, JsonTemplateSchema, ManualTriggerPolicySchema, OperandSchema, OperationStepSchema, QuantifierSchema, ReferenceSchema, RetryPolicySchema, SafeRegexSchema, ScopedAssertionExpressionSchema, ScopedAssertionPredicateSchema, ScopedItemReferenceSchema, ScopedOperandSchema, StepReferenceSchema, RelativeDateNodeSchema, ValueTypeSchema, } from "./health-scenario.js";
9
9
  export type { AssertionExpression, AssertionPredicate, AttemptReference, AssertResult, AssertStep, BoundedJsonPath, CandidateBlock, CandidatePolicy, CandidateReference, CredentialReference, CredentialRefDeclaration, EstablishedConnectionReference, ExtractStep, FindFirst, GuardAttribution, GuardReasonCode, GuardResult, GuardStep, HealthScenario, HealthStep, JsonTemplate, JournalPolicy, ManualTriggerPolicy, NonEmpty, OperationResult, OperationStep, Operand, Quantifier, ReferenceNode, Reference, RelativeDateNode, RetryPolicy, SafeRegex, ScopedAssertionExpression, ScopedAssertionPredicate, ScopedItemReference, ScopedOperand, StepBase, StepReference, ExtractResult, ValueType, } from "./health-scenario.js";
10
10
  export type { DevServerOptions } from "./dev.js";
@@ -3,7 +3,7 @@ export type { CredentialsAuthChallengeDefinition, CredentialsAuthChallengeReques
3
3
  export { createFormCeremony } from "./ceremonies/index.js";
4
4
  export { assertFreshProviderChoiceIssuedAt, createProviderChoiceToken, ProviderChoiceTokenError, type ProviderChoiceTokenErrorReason, type ProviderChoiceTokenPayload, parseProviderChoiceToken, } from "./choice-token.js";
5
5
  export { centered, delayed, defineHealthJourney, defineOperation, defineProvider, defineSmsOtpMatcher, every, } from "./define.js";
6
- export type { ProviderBuilder, ProviderContextOf, ProviderDeclaration, } from "./define.js";
6
+ export type { OperationDefinitionFor, ProviderBuilder, ProviderContextOf, ProviderDeclaration, ProviderDefinitionFor, } from "./define.js";
7
7
  export type { JsonPrimitive, JsonValue } from "./contract-json.js";
8
8
  export { AssertionExpressionSchema, AssertionPredicateSchema, AssertStepSchema, BoundedJsonPathSchema, CandidateBlockSchema, CandidatePolicySchema, CredentialRefDeclarationSchema, defineHealthScenario, HealthScenarioSchema, HealthStepSchema, ExtractStepSchema, FindFirstSchema, GuardStepSchema, JournalPolicySchema, JsonTemplateSchema, ManualTriggerPolicySchema, OperandSchema, OperationStepSchema, QuantifierSchema, ReferenceSchema, RetryPolicySchema, SafeRegexSchema, ScopedAssertionExpressionSchema, ScopedAssertionPredicateSchema, ScopedItemReferenceSchema, ScopedOperandSchema, StepReferenceSchema, RelativeDateNodeSchema, ValueTypeSchema, } from "./health-scenario.js";
9
9
  export type { AssertionExpression, AssertionPredicate, AttemptReference, AssertResult, AssertStep, BoundedJsonPath, CandidateBlock, CandidatePolicy, CandidateReference, CredentialReference, CredentialRefDeclaration, EstablishedConnectionReference, ExtractStep, FindFirst, GuardAttribution, GuardReasonCode, GuardResult, GuardStep, HealthScenario, HealthStep, JsonTemplate, JournalPolicy, ManualTriggerPolicy, NonEmpty, OperationResult, OperationStep, Operand, Quantifier, ReferenceNode, Reference, RelativeDateNode, RetryPolicy, SafeRegex, ScopedAssertionExpression, ScopedAssertionPredicate, ScopedItemReference, ScopedOperand, StepBase, StepReference, ExtractResult, ValueType, } from "./health-scenario.js";
@@ -53,17 +53,17 @@ export declare const ProviderServerStatefulForwardEnvelopeSchema: z.ZodObject<{
53
53
  export type ProviderServerStatefulForwardEnvelope = Readonly<z.infer<typeof ProviderServerStatefulForwardEnvelopeSchema>>;
54
54
  export type ProviderServerStatefulOwnerFence = Readonly<Pick<ProviderServerStatefulForwardEnvelope, "providerId" | "sessionKey" | "ownerPodId" | "generation" | "sourcePodId" | "forwardedAt" | "requestId" | "idempotencyKey">>;
55
55
  export type ProviderServerStatefulOwnerFenceValidator = (fence: ProviderServerStatefulOwnerFence, signal: AbortSignal) => boolean | Promise<boolean>;
56
- export type ProviderServerOperationExecutorInput = {
57
- readonly provider: ProviderDefinition;
56
+ export type ProviderServerOperationExecutorInput<TContext extends Partial<ProviderContext> = ProviderContext> = {
57
+ readonly provider: ProviderDefinition<TContext>;
58
58
  readonly operationId: string;
59
- readonly ctx: ProviderContext;
59
+ readonly ctx: TContext;
60
60
  readonly request: OperationRequest & {
61
61
  readonly deadlineAt?: string;
62
62
  };
63
63
  readonly signal?: AbortSignal;
64
64
  readonly internalStatefulForward?: ProviderServerStatefulForwardEnvelope;
65
65
  };
66
- export type ProviderServerOperationExecutor = (input: ProviderServerOperationExecutorInput) => Promise<unknown>;
66
+ export type ProviderServerOperationExecutor<TContext extends Partial<ProviderContext> = ProviderContext> = (input: ProviderServerOperationExecutorInput<TContext>) => Promise<unknown>;
67
67
  export declare function resolveProviderProxyAffinityKey(provider: ProviderDefinition, request: OperationRequest, operationId: string): string;
68
68
  export declare function resolveProviderResolverIdentityScope(provider: ProviderDefinition, affinityKey: string, contextId: string): string;
69
69
  export declare function resolveAuthFlowProxyAffinityKey(provider: ProviderDefinition, request: Pick<AuthFlowRequest, "connection" | "connectionId" | "externalRef" | "tenantId" | "providerId">): string;
@@ -138,12 +138,12 @@ export type ProviderServerLogEvent = (ProviderServerLogEventBase & {
138
138
  message: string;
139
139
  } | SelfTestCancellationLogEvent;
140
140
  export type ProviderServerLogger = (event: ProviderServerLogEvent) => void;
141
- export type ProviderServerOptions = {
141
+ export type ProviderServerOptions<TContext extends Partial<ProviderContext> = ProviderContext> = {
142
142
  logger?: ProviderServerLogger;
143
143
  /** Optional provider-specific operation executor. Stateful providers use this to preserve provider-local runtime semantics. */
144
- operationExecutor?: ProviderServerOperationExecutor;
144
+ operationExecutor?: ProviderServerOperationExecutor<TContext>;
145
145
  /** Optional signed internal executor for stateful owner forwarding. */
146
- internalOperationExecutor?: ProviderServerOperationExecutor;
146
+ internalOperationExecutor?: ProviderServerOperationExecutor<TContext>;
147
147
  statefulForwarding?: {
148
148
  readonly secret: string;
149
149
  readonly maxSkewMs?: number;
@@ -196,14 +196,14 @@ export type ProviderErrorCauseFrame = {
196
196
  * Primary, cross-runtime app factory. Declared capability ESM is preloaded
197
197
  * asynchronously, so this path works on Bun and every supported Node release.
198
198
  */
199
- export declare function createServerAppAsync(provider: ProviderDefinition, options?: ProviderServerOptions): Promise<Hono>;
199
+ export declare function createServerAppAsync<TContext extends Partial<ProviderContext> = ProviderContext>(provider: ProviderDefinition<TContext>, options?: ProviderServerOptions<TContext>): Promise<Hono>;
200
200
  /**
201
201
  * Synchronous compatibility factory. Standard providers remain synchronous on
202
202
  * every runtime because they load no capability modules. Providers declaring a
203
203
  * capability require Bun or Node >=22.12; older Node releases receive an
204
204
  * actionable error directing them to createServerAppAsync().
205
205
  */
206
- export declare function createServerApp(provider: ProviderDefinition, options?: ProviderServerOptions): Hono;
206
+ export declare function createServerApp<TContext extends Partial<ProviderContext> = ProviderContext>(provider: ProviderDefinition<TContext>, options?: ProviderServerOptions<TContext>): Hono;
207
207
  export type ProviderServerCloseOptions = {
208
208
  readonly timeoutMs?: number;
209
209
  };
@@ -211,7 +211,7 @@ export type ProviderServerHandle = {
211
211
  readonly port: number;
212
212
  close(options?: ProviderServerCloseOptions): Promise<void>;
213
213
  };
214
- export interface ServeOptions extends ProviderServerOptions {
214
+ export interface ServeOptions<TContext extends Partial<ProviderContext> = ProviderContext> extends ProviderServerOptions<TContext> {
215
215
  host?: string;
216
216
  port?: number;
217
217
  /**
@@ -221,5 +221,5 @@ export interface ServeOptions extends ProviderServerOptions {
221
221
  */
222
222
  selfTestPort?: number;
223
223
  }
224
- export declare function serve(provider: ProviderDefinition, options?: ServeOptions): Promise<ProviderServerHandle>;
224
+ export declare function serve<TContext extends Partial<ProviderContext> = ProviderContext>(provider: ProviderDefinition<TContext>, options?: ServeOptions<TContext>): Promise<ProviderServerHandle>;
225
225
  export {};
@@ -9,7 +9,7 @@ import { validateFailClosedDeclaration } from "../declaration-validation.js";
9
9
  import { safeProviderErrorObservability } from "../error-observability.js";
10
10
  import { SDK_OWNED_PROVIDER_ERROR_CODES, SDK_RUNTIME_OWNED_ERROR_CODES, SDK_STATUS_MAPPED_PROVIDER_ERROR_CODES, } from "../error-resolution.js";
11
11
  import { AuthError, isProviderError, isSessionExpiredError, isTransportError, isValidationError, ProviderError, } from "../errors.js";
12
- import { sanitizeDiagnosticText } from "../fixture-sanitization.js";
12
+ import { REDACTED_FIXTURE_VALUE, sanitizeDiagnosticText, } from "../fixture-sanitization.js";
13
13
  import { loadProviderLocaleCatalogs, localizeAuthTurn, } from "../i18n/catalog.js";
14
14
  import { categoryForStatus, sourceForCategory, isRetryableCategory, PROVIDER_OBSERVABILITY_TAXONOMY_VERSION, } from "../observability.js";
15
15
  import { createScratchpad } from "../runtime/auth-flow.js";
@@ -979,8 +979,47 @@ function extractRequestId(raw) {
979
979
  }
980
980
  const MAX_PROVIDER_ERROR_CAUSE_FRAMES = 5;
981
981
  const MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH = 300;
982
+ const UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE = "[UNSTRUCTURED_UPSTREAM_TEXT]";
983
+ const PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN = /https?:\/\/[^\s"'<>]+/giu;
984
+ const PROVIDER_ERROR_CAUSE_TOKEN_RUN = /\S+/gu;
985
+ const PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION = /^[^\p{L}\p{N}]+|[^\p{L}\p{N}]+$/gu;
986
+ const STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS = new Set([
987
+ "completion",
988
+ "diagnostic",
989
+ "provider",
990
+ "rejected",
991
+ "returned",
992
+ "upstream",
993
+ ]);
994
+ /**
995
+ * Cause frames fail closed when sanitization leaves a plausible credential-shaped free-text run.
996
+ * Redaction sentinels and retained URLs are ignored. Every other whitespace token (or each side
997
+ * of a structured key=value token) with at least eight Unicode characters must reduce to this
998
+ * small vocabulary drawn from SDK diagnostics; counting punctuation keeps bare passwords opaque.
999
+ */
1000
+ function isStructurallySafeProviderErrorCauseMessage(message) {
1001
+ const classifiableMessage = message
1002
+ .replaceAll(REDACTED_FIXTURE_VALUE, " ")
1003
+ .replace(PROVIDER_ERROR_CAUSE_RETAINED_URL_RUN, " ");
1004
+ for (const match of classifiableMessage.matchAll(PROVIDER_ERROR_CAUSE_TOKEN_RUN)) {
1005
+ const token = match[0];
1006
+ for (const run of token.split("=")) {
1007
+ const diagnosticWord = run
1008
+ .replace(PROVIDER_ERROR_CAUSE_TOKEN_EDGE_PUNCTUATION, "")
1009
+ .toLowerCase();
1010
+ if (STRUCTURALLY_SAFE_PROVIDER_ERROR_CAUSE_WORDS.has(diagnosticWord))
1011
+ continue;
1012
+ if ([...run].length >= 8)
1013
+ return false;
1014
+ }
1015
+ }
1016
+ return true;
1017
+ }
982
1018
  function providerErrorCauseMessage(message) {
983
1019
  const sanitized = sanitizeDiagnosticText(message);
1020
+ if (!isStructurallySafeProviderErrorCauseMessage(sanitized)) {
1021
+ return UNSTRUCTURED_PROVIDER_ERROR_CAUSE_MESSAGE;
1022
+ }
984
1023
  return sanitized.length > MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH
985
1024
  ? `${sanitized.slice(0, MAX_PROVIDER_ERROR_CAUSE_MESSAGE_LENGTH)}… [truncated]`
986
1025
  : sanitized;
@@ -1646,9 +1685,11 @@ function parseStatefulForwardingEnvelope(rawBody) {
1646
1685
  * asynchronously, so this path works on Bun and every supported Node release.
1647
1686
  */
1648
1687
  export async function createServerAppAsync(provider, options = {}) {
1649
- validateFailClosedDeclaration(provider);
1650
- validateStatefulServerConfig(options);
1651
- return createServerAppWithCapabilityModules(provider, options, await loadProviderCapabilityModules(provider));
1688
+ const runtimeProvider = provider;
1689
+ const runtimeOptions = options;
1690
+ validateFailClosedDeclaration(runtimeProvider);
1691
+ validateStatefulServerConfig(runtimeOptions);
1692
+ return createServerAppWithCapabilityModules(runtimeProvider, runtimeOptions, await loadProviderCapabilityModules(runtimeProvider));
1652
1693
  }
1653
1694
  /**
1654
1695
  * Synchronous compatibility factory. Standard providers remain synchronous on
@@ -1657,9 +1698,11 @@ export async function createServerAppAsync(provider, options = {}) {
1657
1698
  * actionable error directing them to createServerAppAsync().
1658
1699
  */
1659
1700
  export function createServerApp(provider, options = {}) {
1660
- validateFailClosedDeclaration(provider);
1661
- validateStatefulServerConfig(options);
1662
- return createServerAppWithCapabilityModules(provider, options, loadProviderCapabilityModulesSync(provider));
1701
+ const runtimeProvider = provider;
1702
+ const runtimeOptions = options;
1703
+ validateFailClosedDeclaration(runtimeProvider);
1704
+ validateStatefulServerConfig(runtimeOptions);
1705
+ return createServerAppWithCapabilityModules(runtimeProvider, runtimeOptions, loadProviderCapabilityModulesSync(runtimeProvider));
1663
1706
  }
1664
1707
  function createServerAppWithCapabilityModules(provider, serverOptions, capabilityModules) {
1665
1708
  const options = { ...serverOptions, capabilityModules };
package/dist/types.d.ts CHANGED
@@ -2024,7 +2024,7 @@ export interface ProviderDeploymentOverrides {
2024
2024
  };
2025
2025
  buildContext?: string;
2026
2026
  }
2027
- export interface ProviderDefinition {
2027
+ export interface ProviderDefinition<TContext = ProviderContext> {
2028
2028
  id: string;
2029
2029
  version: string;
2030
2030
  runtime: "standard" | "shared" | "browser";
@@ -2054,7 +2054,7 @@ export interface ProviderDefinition {
2054
2054
  credential?: CredentialDeclaration;
2055
2055
  context?: ContextDeclaration;
2056
2056
  meta: ProviderMeta;
2057
- operations: Record<string, OperationDefinition<SchemaLike, SchemaLike>>;
2057
+ operations: Record<string, OperationDefinition<SchemaLike, SchemaLike, TContext>>;
2058
2058
  healthMonitor?: ProviderHealthMonitorConfig;
2059
2059
  /** Transitional alias for `healthMonitor`; `defineProvider` mirrors both. */
2060
2060
  healthProbe?: ProviderHealthProbeConfig;
package/package.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "version": "2.2.0-beta.41",
2
+ "version": "2.2.0-beta.43",
3
3
  "name": "@apifuse/provider-sdk",
4
4
  "private": false,
5
5
  "type": "module",
@@ -128,7 +128,8 @@
128
128
  "@microsoft/api-extractor": "^7.58.13",
129
129
  "@types/bun": "latest",
130
130
  "@types/node": "^25.9.3",
131
- "ajv": "^8.17"
131
+ "ajv": "^8.17",
132
+ "typescript": "6.0.3"
132
133
  },
133
134
  "dependencies": {
134
135
  "@clack/prompts": "^1.5.1",
@@ -145,7 +146,6 @@
145
146
  "safe-regex": "^2.1",
146
147
  "socks": "^2.8.9",
147
148
  "tough-cookie": "^6.0.2",
148
- "typescript": "6.0.3",
149
149
  "wreq-js": "3.0.0",
150
150
  "zod": "^4.4.3"
151
151
  },
@@ -59,7 +59,7 @@ export const COMMAND_MANIFEST: Record<
59
59
  "migrate-shape": {
60
60
  name: "migrate-shape",
61
61
  summary:
62
- "Migrate a provider index.ts from the single-phase defineProvider shape to the two-phase declaration builder.",
62
+ "Migrate a provider to the phase-separated SDK: two-phase defineProvider in index.ts and curried defineOperation across sources.",
63
63
  usage: "apifuse migrate-shape [path] [--check] [--json]",
64
64
  examples: ["apifuse migrate-shape .", "apifuse migrate-shape . --check"],
65
65
  modulePath: "./apifuse-migrate-shape",
package/src/cli/create.ts CHANGED
@@ -26,7 +26,7 @@ export const CATEGORY_OPTIONS = [
26
26
  "other",
27
27
  ] as const;
28
28
  export const AUTH_MODE_OPTIONS = ["none", "platform-managed", "credentials", "oauth2"] as const;
29
- export const RUNTIME_OPTIONS = ["standard", "browser"] as const;
29
+ export const RUNTIME_OPTIONS = ["standard", "shared", "browser"] as const;
30
30
  export const PRESET_OPTIONS = ["standalone", "monorepo"] as const;
31
31
 
32
32
  export type CreateCategory = (typeof CATEGORY_OPTIONS)[number];
@@ -102,7 +102,7 @@ Options:
102
102
  --display-name <name>
103
103
  --category <category>
104
104
  --auth-mode <mode>
105
- --runtime <standard|browser>
105
+ --runtime <standard|shared|browser>
106
106
  --yes
107
107
  --dry-run
108
108
  --json
@@ -0,0 +1,184 @@
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
+ }
15
+
16
+ /**
17
+ * Operation authoring-shape migration.
18
+ *
19
+ * `defineOperation` / `defineStreamOperation` changed alongside the provider
20
+ * builder in 2.2.0-beta.37: the identity helper became a curried,
21
+ * context-typed factory.
22
+ *
23
+ * before: defineOperation({ ...config })
24
+ * after: defineOperation<ProviderContext>()({ ...config })
25
+ *
26
+ * The old call still type-checks against an older pin, but under the new SDK
27
+ * it returns the INNER FACTORY FUNCTION with the config swallowed as an
28
+ * ignored argument, so every operation in the map becomes a function and the
29
+ * provider fails `finalizeProvider` with a misleading "declares neither
30
+ * healthCheck nor healthCheckUnsupported" error. Measured fleet blast radius
31
+ * (2026-08-29): 53 of 84 repositories, 450 legacy call sites.
32
+ *
33
+ * Same contract as the provider-shape transform: rewrite only what is fully
34
+ * understood, report a reasoned skip otherwise, never emit a partial file.
35
+ */
36
+
37
+ export type OperationShapeMigration =
38
+ | {
39
+ readonly status: "migrated";
40
+ readonly code: string;
41
+ /** Number of call sites rewritten in this file. */
42
+ readonly rewrites: number;
43
+ /** True when a `ProviderContext` type import was added. */
44
+ readonly importAdded: boolean;
45
+ }
46
+ | { readonly status: "unchanged"; readonly code: string }
47
+ | { readonly status: "skipped"; readonly reason: string };
48
+
49
+ const OPERATION_HELPERS = new Set(["defineOperation", "defineStreamOperation"]);
50
+ const PROVIDER_CONTEXT_TYPE_NAME = "ProviderContext";
51
+ const PROVIDER_SDK_PROVIDER_SUBPATH = "@apifuse/provider-sdk/provider";
52
+
53
+ /**
54
+ * Migrate legacy `defineOperation(config)` calls in one source file to the
55
+ * curried `defineOperation<ProviderContext>()(config)` form.
56
+ *
57
+ * @param contextImportSpecifier module specifier the `ProviderContext` type
58
+ * import should come from when one has to be added — `"../index"` for
59
+ * operation modules, `"./index"` is never needed because index.ts declares
60
+ * the alias itself. Callers pass the correct relative path per file.
61
+ */
62
+ export function migrateOperationShape(
63
+ sourceText: string,
64
+ fileName: string,
65
+ contextImportSpecifier: string,
66
+ ): OperationShapeMigration {
67
+ const source = ts.createSourceFile(
68
+ fileName,
69
+ sourceText,
70
+ ts.ScriptTarget.Latest,
71
+ true,
72
+ ts.ScriptKind.TS,
73
+ );
74
+
75
+ const parseError = firstSyntaxError(source);
76
+ if (parseError !== undefined) {
77
+ return { status: "skipped", reason: parseError };
78
+ }
79
+
80
+ // Every legacy call site: a direct call to the bare helper identifier
81
+ // whose single argument is the config (i.e. NOT the curried form, whose
82
+ // outer call has zero arguments and optional type arguments).
83
+ const legacyCalls: TS.CallExpression[] = [];
84
+ let sawCurried = false;
85
+ const visit = (node: TS.Node): void => {
86
+ if (
87
+ ts.isCallExpression(node) &&
88
+ ts.isIdentifier(node.expression) &&
89
+ OPERATION_HELPERS.has(node.expression.text)
90
+ ) {
91
+ if (node.arguments.length === 0) {
92
+ sawCurried = true; // already-migrated outer call
93
+ } else {
94
+ legacyCalls.push(node);
95
+ }
96
+ }
97
+ ts.forEachChild(node, visit);
98
+ };
99
+ visit(source);
100
+
101
+ if (legacyCalls.length === 0) {
102
+ return { status: "unchanged", code: sourceText };
103
+ }
104
+
105
+ // A file mixing both forms almost certainly had a partial hand-migration;
106
+ // refuse rather than guess which convention the author wants.
107
+ if (sawCurried) {
108
+ return {
109
+ status: "skipped",
110
+ reason:
111
+ "File mixes legacy defineOperation(config) with curried defineOperation<...>()(config); migrate it by hand.",
112
+ };
113
+ }
114
+
115
+ const declaresContextAlias = source.statements.some(
116
+ (statement) =>
117
+ ts.isTypeAliasDeclaration(statement) &&
118
+ statement.name.text === PROVIDER_CONTEXT_TYPE_NAME,
119
+ );
120
+ const importsContext = sourceText.includes(PROVIDER_CONTEXT_TYPE_NAME);
121
+ const needsImport = !declaresContextAlias && !importsContext;
122
+
123
+ const edits: { start: number; end: number; text: string }[] = [];
124
+ for (const call of legacyCalls) {
125
+ // `defineOperation(` -> `defineOperation<ProviderContext>()(`
126
+ const callee = call.expression;
127
+ edits.push({
128
+ start: callee.getEnd(),
129
+ end: callee.getEnd(),
130
+ text: `<${PROVIDER_CONTEXT_TYPE_NAME}>()`,
131
+ });
132
+ }
133
+
134
+ let importAdded = false;
135
+ if (needsImport) {
136
+ const lastImport = [...source.statements]
137
+ .reverse()
138
+ .find((statement) => ts.isImportDeclaration(statement));
139
+ const insertAt = lastImport ? lastImport.getEnd() : 0;
140
+ const importText = `${lastImport ? "\n" : ""}import type { ${PROVIDER_CONTEXT_TYPE_NAME} } from "${contextImportSpecifier}";${lastImport ? "" : "\n"}`;
141
+ edits.push({ start: insertAt, end: insertAt, text: importText });
142
+ importAdded = true;
143
+ }
144
+
145
+ const ordered = edits.sort((a, b) => b.start - a.start);
146
+ let code = sourceText;
147
+ for (const edit of ordered) {
148
+ code = code.slice(0, edit.start) + edit.text + code.slice(edit.end);
149
+ }
150
+
151
+ const verified = ts.createSourceFile(
152
+ fileName,
153
+ code,
154
+ ts.ScriptTarget.Latest,
155
+ true,
156
+ ts.ScriptKind.TS,
157
+ );
158
+ const outputError = firstSyntaxError(verified);
159
+ if (outputError !== undefined) {
160
+ return {
161
+ status: "skipped",
162
+ reason: `Transform produced source that does not parse (${outputError}); refusing to emit a partial migration.`,
163
+ };
164
+ }
165
+
166
+ return {
167
+ status: "migrated",
168
+ code,
169
+ rewrites: legacyCalls.length,
170
+ importAdded,
171
+ };
172
+ }
173
+
174
+ function firstSyntaxError(source: TS.SourceFile): string | undefined {
175
+ const diagnostics = (
176
+ source as TS.SourceFile & { parseDiagnostics?: TS.DiagnosticWithLocation[] }
177
+ ).parseDiagnostics;
178
+ if (diagnostics === undefined || diagnostics.length === 0) return undefined;
179
+ const first = diagnostics[0];
180
+ if (first === undefined) return undefined;
181
+ const message = ts.flattenDiagnosticMessageText(first.messageText, " ");
182
+ const { line } = source.getLineAndCharacterOfPosition(first.start);
183
+ return `${message} (line ${line + 1})`;
184
+ }