@apifuse/provider-sdk 2.1.0-beta.5 → 2.1.0-beta.6

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,215 @@
1
+ import { createHash } from "node:crypto";
2
+ import {
3
+ canonicalJson,
4
+ compactObject,
5
+ copyRecordWithout,
6
+ type JsonPrimitive,
7
+ type JsonValue,
8
+ toJsonValue,
9
+ } from "./contract-json";
10
+ import { describeSchema, serializeSmsMatcher } from "./contract-serialization";
11
+ import {
12
+ PROVIDER_CONTRACT_SCHEMA_VERSION,
13
+ type ProviderContractOperation,
14
+ type ProviderContractSnapshot,
15
+ } from "./contract-types";
16
+ import type {
17
+ HealthCheckSuite,
18
+ HealthCheckUnsupported,
19
+ HealthJourneyDefinition,
20
+ OperationDefinition,
21
+ OperationTransport,
22
+ ProviderDefinition,
23
+ } from "./types";
24
+
25
+ export {
26
+ canonicalJson,
27
+ type JsonPrimitive,
28
+ type JsonValue,
29
+ PROVIDER_CONTRACT_SCHEMA_VERSION,
30
+ type ProviderContractOperation,
31
+ type ProviderContractSnapshot,
32
+ };
33
+
34
+ export function extractProviderContract(
35
+ provider: ProviderDefinition,
36
+ ): ProviderContractSnapshot {
37
+ const auth = extractAuth(provider.auth);
38
+ const stealth = toJsonValue(provider.stealth);
39
+ const proxy = toJsonValue(provider.proxy);
40
+ const stt = toJsonValue(provider.stt);
41
+ const browser = toJsonValue(provider.browser);
42
+ const reviewed = toJsonValue(provider.reviewed);
43
+ const access = toJsonValue(provider.access);
44
+ const secrets = toJsonValue(provider.secrets);
45
+ const credential = toJsonValue(provider.credential);
46
+ const context = toJsonValue(provider.context);
47
+ const healthMonitor = toJsonValue(provider.healthMonitor);
48
+ const healthJourneys = provider.healthJourneys?.map(extractHealthJourney);
49
+
50
+ return {
51
+ schemaVersion: PROVIDER_CONTRACT_SCHEMA_VERSION,
52
+ provider: {
53
+ id: provider.id,
54
+ version: provider.version,
55
+ runtime: provider.runtime,
56
+ },
57
+ meta: toJsonValue(provider.meta) ?? null,
58
+ operations: Object.entries(provider.operations)
59
+ .sort(([leftId], [rightId]) => leftId.localeCompare(rightId))
60
+ .map(([operationId, operation]) =>
61
+ extractOperation(operationId, operation),
62
+ ),
63
+ ...(provider.allowedHosts
64
+ ? { allowedHosts: [...provider.allowedHosts].sort() }
65
+ : {}),
66
+ ...(stealth === undefined ? {} : { stealth }),
67
+ ...(proxy === undefined ? {} : { proxy }),
68
+ ...(stt === undefined ? {} : { stt }),
69
+ ...(browser === undefined ? {} : { browser }),
70
+ ...(auth === undefined ? {} : { auth }),
71
+ ...(reviewed === undefined ? {} : { reviewed }),
72
+ ...(access === undefined ? {} : { access }),
73
+ ...(secrets === undefined ? {} : { secrets }),
74
+ ...(credential === undefined ? {} : { credential }),
75
+ ...(context === undefined ? {} : { context }),
76
+ ...(healthMonitor === undefined ? {} : { healthMonitor }),
77
+ ...(healthJourneys === undefined ? {} : { healthJourneys }),
78
+ };
79
+ }
80
+
81
+ export function digestProviderContract(
82
+ snapshot: ProviderContractSnapshot,
83
+ ): string {
84
+ return createHash("sha256").update(canonicalJson(snapshot)).digest("hex");
85
+ }
86
+
87
+ function extractOperation(
88
+ operationId: string,
89
+ operation: OperationDefinition,
90
+ ): ProviderContractOperation {
91
+ const descriptionKey = toJsonValue(operation.descriptionKey);
92
+ const docs = toJsonValue(operation.docs);
93
+ const whenToUseKeys = toJsonValue(operation.whenToUseKeys);
94
+ const whenNotToUseKeys = toJsonValue(operation.whenNotToUseKeys);
95
+ const derivations = toJsonValue(operation.derivations);
96
+ const inputExamples = toJsonValue(operation.inputExamples);
97
+ const annotations = toJsonValue(operation.annotations);
98
+ const contract = toJsonValue(operation.contract);
99
+ const tags = toJsonValue(operation.tags);
100
+ const relatedOperations = toJsonValue(operation.relatedOperations);
101
+ const toolRouter = toJsonValue(operation.toolRouter);
102
+ const observability = toJsonValue(operation.observability);
103
+ const transport = extractTransport(operation.transport);
104
+ const fixtures = toJsonValue(operation.fixtures);
105
+ const upstream = toJsonValue(operation.upstream);
106
+ const hints = toJsonValue(operation.hints);
107
+ const healthCheck = extractHealthCheck(operation.healthCheck);
108
+ const healthCheckUnsupported = extractHealthCheckUnsupported(
109
+ operation.healthCheckUnsupported,
110
+ );
111
+
112
+ return {
113
+ id: operationId,
114
+ inputSchema: describeSchema(operation.input),
115
+ outputSchema: describeSchema(operation.output),
116
+ ...(descriptionKey === undefined ? {} : { descriptionKey }),
117
+ ...(docs === undefined ? {} : { docs }),
118
+ ...(whenToUseKeys === undefined ? {} : { whenToUseKeys }),
119
+ ...(whenNotToUseKeys === undefined ? {} : { whenNotToUseKeys }),
120
+ ...(derivations === undefined ? {} : { derivations }),
121
+ ...(inputExamples === undefined ? {} : { inputExamples }),
122
+ ...(annotations === undefined ? {} : { annotations }),
123
+ ...(contract === undefined ? {} : { contract }),
124
+ ...(tags === undefined ? {} : { tags }),
125
+ ...(relatedOperations === undefined ? {} : { relatedOperations }),
126
+ ...(toolRouter === undefined ? {} : { toolRouter }),
127
+ ...(observability === undefined ? {} : { observability }),
128
+ ...(transport === undefined ? {} : { transport }),
129
+ ...(fixtures === undefined ? {} : { fixtures }),
130
+ ...(upstream === undefined ? {} : { upstream }),
131
+ ...(hints === undefined ? {} : { hints }),
132
+ ...(healthCheck === undefined ? {} : { healthCheck }),
133
+ ...(healthCheckUnsupported === undefined ? {} : { healthCheckUnsupported }),
134
+ };
135
+ }
136
+
137
+ function extractAuth(value: ProviderDefinition["auth"]): JsonValue | undefined {
138
+ if (!value) return undefined;
139
+ return compactObject({
140
+ mode: value.mode,
141
+ flow: value.flow
142
+ ? compactObject({
143
+ start: true,
144
+ continue: true,
145
+ poll: value.flow.poll === undefined ? undefined : true,
146
+ abort: value.flow.abort === undefined ? undefined : true,
147
+ })
148
+ : undefined,
149
+ });
150
+ }
151
+
152
+ function extractTransport(
153
+ value: OperationTransport | undefined,
154
+ ): JsonValue | undefined {
155
+ if (!value) return undefined;
156
+ if (value.kind !== "sse") return toJsonValue(value);
157
+ return compactObject({
158
+ ...copyRecordWithout(value, new Set(["events"])),
159
+ events: Object.fromEntries(
160
+ Object.entries(value.events)
161
+ .sort(([leftId], [rightId]) => leftId.localeCompare(rightId))
162
+ .map(([eventName, schema]) => [eventName, describeSchema(schema)]),
163
+ ),
164
+ });
165
+ }
166
+
167
+ function extractHealthCheck(
168
+ value: HealthCheckSuite | undefined,
169
+ ): JsonValue | undefined {
170
+ if (!value) return undefined;
171
+ return compactObject({
172
+ interval: value.interval,
173
+ timeoutMs: value.timeoutMs,
174
+ degradedThresholdMs: value.degradedThresholdMs,
175
+ requiresConnection: value.requiresConnection,
176
+ cases: value.cases.map((item) =>
177
+ compactObject({
178
+ name: item.name,
179
+ description: item.description,
180
+ input: toJsonValue(item.input),
181
+ degradedThresholdMs: item.degradedThresholdMs,
182
+ timeoutMs: item.timeoutMs,
183
+ expectedStatus: item.expectedStatus,
184
+ }),
185
+ ),
186
+ });
187
+ }
188
+
189
+ function extractHealthCheckUnsupported(
190
+ value: HealthCheckUnsupported | undefined,
191
+ ): JsonValue | undefined {
192
+ return toJsonValue(value);
193
+ }
194
+
195
+ function extractHealthJourney(value: HealthJourneyDefinition): JsonValue {
196
+ return compactObject({
197
+ id: value.id,
198
+ title: value.title,
199
+ description: value.description,
200
+ schedule: toJsonValue(value.schedule),
201
+ coversOperations: toJsonValue(value.coversOperations),
202
+ timeout: value.timeout,
203
+ cooldown: value.cooldown,
204
+ smsMatchers: toJsonValue(
205
+ value.smsMatchers?.map((matcher) =>
206
+ serializeSmsMatcher(
207
+ copyRecordWithout(matcher, new Set(["extractOtp"])),
208
+ ),
209
+ ),
210
+ ),
211
+ requiredSecrets: toJsonValue(value.requiredSecrets),
212
+ manualTrigger: toJsonValue(value.manualTrigger),
213
+ steps: toJsonValue(value.steps),
214
+ });
215
+ }
package/src/define.ts CHANGED
@@ -230,6 +230,18 @@ type WebSocketOperationConfig<
230
230
  | Promise<Response | ReadableStream<Uint8Array>>;
231
231
  };
232
232
 
233
+ type AuthStartNoInputGuard<TConfig> = TConfig extends {
234
+ auth?: { flow?: { start: infer TStart } };
235
+ }
236
+ ? TStart extends (...args: infer TArgs) => unknown
237
+ ? TArgs extends [unknown]
238
+ ? unknown
239
+ : {
240
+ "auth start handlers must not declare input parameters; return a form turn from start and receive user input in continue": never;
241
+ }
242
+ : unknown
243
+ : unknown;
244
+
233
245
  export interface ProviderConfig<
234
246
  TOperations extends Record<string, ProviderOperation>,
235
247
  > {
@@ -362,6 +374,23 @@ function validateProviderShape(config: unknown): void {
362
374
  VALID_AUTH_MODES,
363
375
  String(config.id),
364
376
  );
377
+ if (
378
+ auth &&
379
+ typeof auth === "object" &&
380
+ "flow" in auth &&
381
+ auth.flow &&
382
+ typeof auth.flow === "object" &&
383
+ "start" in auth.flow &&
384
+ typeof auth.flow.start === "function" &&
385
+ auth.flow.start.length > 1
386
+ ) {
387
+ throw new ProviderError(
388
+ `Provider "${String(config.id)}" auth.flow.start must not declare an input parameter`,
389
+ {
390
+ fix: "Return a form turn from start(ctx), then receive user input in continue(ctx, input).",
391
+ },
392
+ );
393
+ }
365
394
  const access = config.access;
366
395
  if (access !== undefined) {
367
396
  if (!access || typeof access !== "object" || Array.isArray(access)) {
@@ -1096,6 +1125,7 @@ const HEALTH_CHECK_CASE_FIELDS = new Set([
1096
1125
  "name",
1097
1126
  "description",
1098
1127
  "input",
1128
+ "prepareInput",
1099
1129
  "assertions",
1100
1130
  "degradedThresholdMs",
1101
1131
  "timeoutMs",
@@ -1371,6 +1401,10 @@ function validateHealthCheckCase(
1371
1401
  fix: `Set ${fieldPath}.assertions to (ctx) => { ... } that throws on failure.`,
1372
1402
  },
1373
1403
  );
1404
+ if (c.prepareInput !== undefined && typeof c.prepareInput !== "function")
1405
+ throw new ValidationError(
1406
+ `Provider "${providerId}" ${fieldPath}.prepareInput must be a function.`,
1407
+ );
1374
1408
  if (
1375
1409
  c.degradedThresholdMs !== undefined &&
1376
1410
  (typeof c.degradedThresholdMs !== "number" ||
@@ -2198,13 +2232,14 @@ function validateOperationFixtures(
2198
2232
 
2199
2233
  export function defineProvider<
2200
2234
  TOperations extends Record<string, ProviderOperation>,
2235
+ TConfig extends ProviderConfig<TOperations>,
2201
2236
  >(
2202
- config: ProviderConfig<TOperations>,
2237
+ config: TConfig & AuthStartNoInputGuard<TConfig>,
2203
2238
  ): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
2204
2239
  validateProviderShape(config);
2205
2240
  if (!CONNECTOR_ID_REGEX.test(config.id))
2206
2241
  throw new ProviderError(`Invalid provider id: "${config.id}"`, {
2207
- fix: 'Use lowercase alphanumeric with dashes, e.g., "airkorea-realtime"',
2242
+ fix: 'Use lowercase alphanumeric with dashes, e.g., "korea-air-quality"',
2208
2243
  });
2209
2244
  if (Object.keys(config.operations).length === 0)
2210
2245
  throw new ProviderError(
package/src/errors.ts CHANGED
@@ -48,6 +48,21 @@ export class AuthError extends ProviderError {
48
48
  }
49
49
  }
50
50
 
51
+ export class SessionExpiredError extends AuthError {
52
+ constructor(
53
+ message = "Provider session expired",
54
+ options?: ProviderErrorOptions,
55
+ ) {
56
+ super(message, {
57
+ code: "reauth_required",
58
+ category: "credential_expired",
59
+ retryable: false,
60
+ ...options,
61
+ });
62
+ this.name = "SessionExpiredError";
63
+ }
64
+ }
65
+
51
66
  export type ValidationErrorOptions = ProviderErrorOptions & {
52
67
  zodError?: unknown;
53
68
  };
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
 
4
+ import type { AuthTurn } from "../types";
4
5
  import {
5
6
  getProviderLocalePath,
6
7
  isProviderLocaleValue,
@@ -57,6 +58,161 @@ export function resolveProviderLocaleValue(
57
58
  );
58
59
  }
59
60
 
61
+ function asStringValue(
62
+ value: ProviderLocaleValue | undefined,
63
+ ): string | undefined {
64
+ return typeof value === "string" ? value : undefined;
65
+ }
66
+
67
+ function isStringRecord(value: unknown): value is Record<string, string> {
68
+ return (
69
+ !!value &&
70
+ typeof value === "object" &&
71
+ !Array.isArray(value) &&
72
+ Object.values(value).every((entry) => typeof entry === "string")
73
+ );
74
+ }
75
+
76
+ function localizeAuthInputSchema(
77
+ expectedInput: Record<string, unknown> | undefined,
78
+ options: {
79
+ catalogs: ProviderLocaleCatalogMap;
80
+ locale: ProviderLocale;
81
+ fallbackLocale: ProviderLocale;
82
+ },
83
+ ): Record<string, unknown> | undefined {
84
+ if (!expectedInput) return undefined;
85
+ const schema = isRecord(expectedInput.schema)
86
+ ? expectedInput.schema
87
+ : expectedInput;
88
+ const localizedSchema = localizeAuthSchemaObject(schema, options);
89
+ if (localizedSchema === schema) return undefined;
90
+ return isRecord(expectedInput.schema)
91
+ ? { ...expectedInput, schema: localizedSchema }
92
+ : localizedSchema;
93
+ }
94
+
95
+ function isRecord(value: unknown): value is Record<string, unknown> {
96
+ return !!value && typeof value === "object" && !Array.isArray(value);
97
+ }
98
+
99
+ function localizeAuthSchemaObject(
100
+ schema: Record<string, unknown>,
101
+ options: {
102
+ catalogs: ProviderLocaleCatalogMap;
103
+ locale: ProviderLocale;
104
+ fallbackLocale: ProviderLocale;
105
+ },
106
+ ): Record<string, unknown> {
107
+ const properties = isRecord(schema.properties)
108
+ ? schema.properties
109
+ : undefined;
110
+ if (!properties) return schema;
111
+
112
+ let changed = false;
113
+ const localizedProperties = Object.fromEntries(
114
+ Object.entries(properties).map(([fieldName, property]) => {
115
+ if (!isRecord(property)) return [fieldName, property];
116
+ const nameKey =
117
+ typeof property.nameKey === "string" ? property.nameKey : undefined;
118
+ const descriptionKey =
119
+ typeof property.descriptionKey === "string"
120
+ ? property.descriptionKey
121
+ : undefined;
122
+ const title = nameKey
123
+ ? asStringValue(
124
+ resolveProviderLocaleValue(
125
+ options.catalogs,
126
+ nameKey,
127
+ options.locale,
128
+ options.fallbackLocale,
129
+ ),
130
+ )
131
+ : undefined;
132
+ const description = descriptionKey
133
+ ? asStringValue(
134
+ resolveProviderLocaleValue(
135
+ options.catalogs,
136
+ descriptionKey,
137
+ options.locale,
138
+ options.fallbackLocale,
139
+ ),
140
+ )
141
+ : undefined;
142
+ if (!title && !description) return [fieldName, property];
143
+ changed = true;
144
+ return [
145
+ fieldName,
146
+ {
147
+ ...property,
148
+ ...(title ? { title } : {}),
149
+ ...(description ? { description } : {}),
150
+ },
151
+ ];
152
+ }),
153
+ );
154
+
155
+ return changed ? { ...schema, properties: localizedProperties } : schema;
156
+ }
157
+
158
+ export function localizeAuthTurn(
159
+ turn: AuthTurn,
160
+ options: {
161
+ catalogs: ProviderLocaleCatalogMap;
162
+ locale: ProviderLocale;
163
+ fallbackLocale?: ProviderLocale;
164
+ },
165
+ ): AuthTurn {
166
+ const fallbackLocale = options.fallbackLocale ?? "en";
167
+ const hint = turn.hintKey
168
+ ? asStringValue(
169
+ resolveProviderLocaleValue(
170
+ options.catalogs,
171
+ turn.hintKey,
172
+ options.locale,
173
+ fallbackLocale,
174
+ ),
175
+ )
176
+ : undefined;
177
+ const expectedInput = localizeAuthInputSchema(turn.expectedInput, {
178
+ catalogs: options.catalogs,
179
+ locale: options.locale,
180
+ fallbackLocale,
181
+ });
182
+ const fieldErrorKeys = turn.data?.fieldErrorKeys;
183
+ const fieldErrors = isStringRecord(fieldErrorKeys)
184
+ ? Object.fromEntries(
185
+ Object.entries(fieldErrorKeys).flatMap(([fieldName, key]) => {
186
+ const message = asStringValue(
187
+ resolveProviderLocaleValue(
188
+ options.catalogs,
189
+ key,
190
+ options.locale,
191
+ fallbackLocale,
192
+ ),
193
+ );
194
+ return message ? [[fieldName, message]] : [];
195
+ }),
196
+ )
197
+ : undefined;
198
+
199
+ if (!hint && !fieldErrors && !expectedInput) return turn;
200
+
201
+ return {
202
+ ...turn,
203
+ ...(hint ? { hint } : {}),
204
+ ...(expectedInput ? { expectedInput } : {}),
205
+ ...(fieldErrors
206
+ ? {
207
+ data: {
208
+ ...(turn.data ?? {}),
209
+ fieldErrors,
210
+ },
211
+ }
212
+ : {}),
213
+ };
214
+ }
215
+
60
216
  export function validateProviderLocaleCatalogs(options: {
61
217
  catalogs: ProviderLocaleCatalogMap;
62
218
  requiredLocales: readonly ProviderLocale[];
package/src/index.ts CHANGED
@@ -9,6 +9,16 @@ export type {
9
9
  SessionConfig,
10
10
  } from "./config/loader";
11
11
  export { defineConfig, loadApiFuseConfig } from "./config/loader";
12
+ export {
13
+ canonicalJson,
14
+ digestProviderContract,
15
+ extractProviderContract,
16
+ type JsonPrimitive,
17
+ type JsonValue,
18
+ PROVIDER_CONTRACT_SCHEMA_VERSION,
19
+ type ProviderContractOperation,
20
+ type ProviderContractSnapshot,
21
+ } from "./contract";
12
22
  export {
13
23
  defineHealthJourney,
14
24
  defineOperation,
@@ -38,6 +48,12 @@ export {
38
48
  type ProviderCacheOptions,
39
49
  resetProviderCacheForTests,
40
50
  } from "./runtime/cache";
51
+ export {
52
+ type CreateProviderChoiceContextOptions,
53
+ createProviderChoiceContext,
54
+ createTestProviderChoiceContext,
55
+ PROVIDER_RUNTIME_CHOICE_TOKEN_MASTER_SECRET_ENV,
56
+ } from "./runtime/choice";
41
57
  export {
42
58
  type CreateCredentialContextOptions,
43
59
  createCredentialContext,
@@ -99,7 +115,8 @@ export type {
99
115
  AuthConfig,
100
116
  AuthContext,
101
117
  AuthFlowDefinition,
102
- AuthFlowHandler,
118
+ AuthFlowInputHandler,
119
+ AuthFlowStartHandler,
103
120
  AuthMode,
104
121
  AuthTurn,
105
122
  Bcp47Locale,
@@ -167,6 +184,10 @@ export type {
167
184
  ProviderCacheLookupMeta,
168
185
  ProviderCacheResponseMeta,
169
186
  ProviderCacheResult,
187
+ ProviderChoiceBindingOptions,
188
+ ProviderChoiceContext,
189
+ ProviderChoiceIssueOptions,
190
+ ProviderChoiceParseOptions,
170
191
  ProviderContext,
171
192
  ProviderDefinition,
172
193
  ProviderHealthMonitorConfig,