@apifuse/provider-sdk 2.1.0-beta.5 → 2.1.0-beta.8
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 +16 -0
- package/README.md +2 -2
- package/SUBMISSION.md +2 -1
- package/bin/apifuse-check.ts +60 -6
- package/bin/apifuse-dev.ts +48 -5
- package/bin/apifuse-perf.ts +50 -11
- package/bin/apifuse-record.ts +35 -11
- package/bin/apifuse-submit-check.ts +1425 -3
- package/dist/ceremonies/index.d.ts +41 -0
- package/dist/ceremonies/index.js +490 -0
- package/dist/choice-token.d.ts +24 -0
- package/dist/choice-token.js +74 -0
- package/dist/cli/commands.d.ts +10 -0
- package/dist/cli/commands.js +80 -0
- package/dist/cli/create.d.ts +47 -0
- package/dist/cli/create.js +762 -0
- package/dist/config/loader.d.ts +107 -0
- package/dist/config/loader.js +935 -0
- package/dist/contract-json.d.ts +9 -0
- package/dist/contract-json.js +51 -0
- package/dist/contract-serialization.d.ts +4 -0
- package/dist/contract-serialization.js +78 -0
- package/dist/contract-types.d.ts +49 -0
- package/dist/contract-types.js +1 -0
- package/dist/contract.d.ts +6 -0
- package/dist/contract.js +155 -0
- package/dist/define.d.ts +97 -0
- package/dist/define.js +1320 -0
- package/dist/dev.d.ts +9 -0
- package/dist/dev.js +15 -0
- package/dist/errors.d.ts +59 -0
- package/dist/errors.js +97 -0
- package/dist/i18n/catalog.d.ts +29 -0
- package/dist/i18n/catalog.js +159 -0
- package/dist/i18n/index.d.ts +2 -0
- package/dist/i18n/index.js +2 -0
- package/dist/i18n/keys.d.ts +10 -0
- package/dist/i18n/keys.js +34 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.js +37 -0
- package/dist/lint.d.ts +73 -0
- package/dist/lint.js +702 -0
- package/dist/observability.d.ts +5 -0
- package/dist/observability.js +39 -0
- package/dist/provider.d.ts +9 -0
- package/dist/provider.js +8 -0
- package/dist/public-schema-field-lint.d.ts +2 -0
- package/dist/public-schema-field-lint.js +158 -0
- package/dist/recipes/gov-api.d.ts +19 -0
- package/dist/recipes/gov-api.js +72 -0
- package/dist/recipes/rest-api.d.ts +21 -0
- package/dist/recipes/rest-api.js +115 -0
- package/dist/runtime/auth-flow.d.ts +14 -0
- package/dist/runtime/auth-flow.js +44 -0
- package/dist/runtime/browser.d.ts +25 -0
- package/dist/runtime/browser.js +1034 -0
- package/dist/runtime/cache.d.ts +10 -0
- package/dist/runtime/cache.js +372 -0
- package/dist/runtime/choice.d.ts +15 -0
- package/dist/runtime/choice.js +435 -0
- package/dist/runtime/credential.d.ts +8 -0
- package/dist/runtime/credential.js +61 -0
- package/dist/runtime/env.d.ts +2 -0
- package/dist/runtime/env.js +10 -0
- package/dist/runtime/executor.d.ts +16 -0
- package/dist/runtime/executor.js +51 -0
- package/dist/runtime/http.d.ts +8 -0
- package/dist/runtime/http.js +706 -0
- package/dist/runtime/insights.d.ts +9 -0
- package/dist/runtime/insights.js +324 -0
- package/dist/runtime/instrumentation.d.ts +8 -0
- package/dist/runtime/instrumentation.js +269 -0
- package/dist/runtime/key-derivation.d.ts +24 -0
- package/dist/runtime/key-derivation.js +73 -0
- package/dist/runtime/keyring.d.ts +25 -0
- package/dist/runtime/keyring.js +93 -0
- package/dist/runtime/namespace.d.ts +9 -0
- package/dist/runtime/namespace.js +19 -0
- package/dist/runtime/otlp.d.ts +39 -0
- package/dist/runtime/otlp.js +103 -0
- package/dist/runtime/perf.d.ts +12 -0
- package/dist/runtime/perf.js +52 -0
- package/dist/runtime/prevalidate.d.ts +12 -0
- package/dist/runtime/prevalidate.js +173 -0
- package/dist/runtime/provider.d.ts +2 -0
- package/dist/runtime/provider.js +11 -0
- package/dist/runtime/proxy-errors.d.ts +21 -0
- package/dist/runtime/proxy-errors.js +83 -0
- package/dist/runtime/proxy-telemetry.d.ts +8 -0
- package/dist/runtime/proxy-telemetry.js +174 -0
- package/dist/runtime/redis.d.ts +17 -0
- package/dist/runtime/redis.js +82 -0
- package/dist/runtime/request-options.d.ts +3 -0
- package/dist/runtime/request-options.js +42 -0
- package/dist/runtime/state.d.ts +17 -0
- package/dist/runtime/state.js +344 -0
- package/dist/runtime/stealth.d.ts +18 -0
- package/dist/runtime/stealth.js +827 -0
- package/dist/runtime/stt.d.ts +22 -0
- package/dist/runtime/stt.js +480 -0
- package/dist/runtime/trace.d.ts +26 -0
- package/dist/runtime/trace.js +142 -0
- package/dist/runtime/waterfall.d.ts +12 -0
- package/dist/runtime/waterfall.js +147 -0
- package/dist/schema.d.ts +74 -0
- package/dist/schema.js +243 -0
- package/dist/serve.d.ts +1 -0
- package/dist/serve.js +1 -0
- package/dist/server/index.d.ts +3 -0
- package/dist/server/index.js +2 -0
- package/dist/server/serve.d.ts +64 -0
- package/dist/server/serve.js +1110 -0
- package/dist/server/types.d.ts +136 -0
- package/dist/server/types.js +86 -0
- package/dist/stealth/profiles.d.ts +4 -0
- package/dist/stealth/profiles.js +259 -0
- package/dist/stream.d.ts +44 -0
- package/dist/stream.js +151 -0
- package/dist/testing/helpers.d.ts +23 -0
- package/dist/testing/helpers.js +95 -0
- package/dist/testing/index.d.ts +2 -0
- package/dist/testing/index.js +2 -0
- package/dist/testing/run.d.ts +34 -0
- package/dist/testing/run.js +303 -0
- package/dist/types.d.ts +1324 -0
- package/dist/types.js +61 -0
- package/dist/utils/date.d.ts +6 -0
- package/dist/utils/date.js +101 -0
- package/dist/utils/parse.d.ts +16 -0
- package/dist/utils/parse.js +51 -0
- package/dist/utils/text.d.ts +4 -0
- package/dist/utils/text.js +14 -0
- package/dist/utils/transform.d.ts +8 -0
- package/dist/utils/transform.js +48 -0
- package/package.json +42 -25
- package/src/ceremonies/index.ts +8 -2
- package/src/choice-token.ts +1 -0
- package/src/cli/commands.ts +8 -5
- package/src/cli/create.ts +28 -0
- package/src/cli/templates/provider/operations/ping.ts.tpl +3 -2
- package/src/cli/templates/provider/schemas/ping.ts.tpl +8 -0
- package/src/config/loader.ts +19 -1
- package/src/contract-json.ts +75 -0
- package/src/contract-serialization.ts +89 -0
- package/src/contract-types.ts +52 -0
- package/src/contract.ts +215 -0
- package/src/define.ts +37 -2
- package/src/errors.ts +15 -0
- package/src/i18n/catalog.ts +156 -0
- package/src/index.ts +22 -1
- package/src/lint.ts +256 -37
- package/src/provider.ts +45 -2
- package/src/runtime/browser.ts +685 -30
- package/src/runtime/cache.ts +35 -89
- package/src/runtime/choice.ts +760 -0
- package/src/runtime/executor.ts +19 -2
- package/src/runtime/redis.ts +116 -0
- package/src/runtime/state.ts +487 -0
- package/src/runtime/stealth.ts +8 -1
- package/src/server/serve.ts +361 -46
- package/src/server/types.ts +2 -0
- package/src/testing/run.ts +16 -3
- package/src/types.ts +209 -6
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
export type JsonPrimitive = string | number | boolean | null;
|
|
2
|
+
export type JsonValue =
|
|
3
|
+
| JsonPrimitive
|
|
4
|
+
| readonly JsonValue[]
|
|
5
|
+
| { readonly [key: string]: JsonValue };
|
|
6
|
+
|
|
7
|
+
export function canonicalJson(value: unknown): string {
|
|
8
|
+
return JSON.stringify(canonicalize(toJsonValue(value) ?? null));
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function toJsonValue(value: unknown): JsonValue | undefined {
|
|
12
|
+
if (
|
|
13
|
+
value === null ||
|
|
14
|
+
typeof value === "string" ||
|
|
15
|
+
typeof value === "boolean"
|
|
16
|
+
) {
|
|
17
|
+
return value;
|
|
18
|
+
}
|
|
19
|
+
if (typeof value === "number") {
|
|
20
|
+
return Number.isFinite(value) ? value : undefined;
|
|
21
|
+
}
|
|
22
|
+
if (Array.isArray(value)) {
|
|
23
|
+
return value.flatMap((item) => {
|
|
24
|
+
const json = toJsonValue(item);
|
|
25
|
+
return json === undefined ? [] : [json];
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
if (!isRecord(value)) return undefined;
|
|
29
|
+
return compactObject(
|
|
30
|
+
Object.fromEntries(
|
|
31
|
+
Object.entries(value).flatMap(([key, item]) => {
|
|
32
|
+
const json = toJsonValue(item);
|
|
33
|
+
return json === undefined ? [] : [[key, json]];
|
|
34
|
+
}),
|
|
35
|
+
),
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function compactObject(
|
|
40
|
+
value: Record<string, JsonValue | undefined>,
|
|
41
|
+
): JsonValue {
|
|
42
|
+
return Object.fromEntries(
|
|
43
|
+
Object.entries(value).filter((entry): entry is [string, JsonValue] => {
|
|
44
|
+
const [, item] = entry;
|
|
45
|
+
return item !== undefined;
|
|
46
|
+
}),
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function copyRecordWithout(
|
|
51
|
+
value: unknown,
|
|
52
|
+
ignoredKeys: ReadonlySet<string>,
|
|
53
|
+
): Record<string, unknown> {
|
|
54
|
+
if (!isRecord(value)) return {};
|
|
55
|
+
return Object.fromEntries(
|
|
56
|
+
Object.entries(value).filter(([key]) => !ignoredKeys.has(key)),
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isRecord(value: unknown): value is Record<string, unknown> {
|
|
61
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function canonicalize(value: JsonValue): JsonValue {
|
|
65
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
66
|
+
if (!isRecord(value)) return value;
|
|
67
|
+
return Object.fromEntries(
|
|
68
|
+
Object.entries(value)
|
|
69
|
+
.sort(([leftKey], [rightKey]) => leftKey.localeCompare(rightKey))
|
|
70
|
+
.flatMap(([key, item]) => {
|
|
71
|
+
const json = toJsonValue(item);
|
|
72
|
+
return json === undefined ? [] : [[key, canonicalize(json)]];
|
|
73
|
+
}),
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { type ZodType, z } from "zod";
|
|
3
|
+
import {
|
|
4
|
+
canonicalJson,
|
|
5
|
+
compactObject,
|
|
6
|
+
isRecord,
|
|
7
|
+
type JsonValue,
|
|
8
|
+
toJsonValue,
|
|
9
|
+
} from "./contract-json";
|
|
10
|
+
import type { SchemaLike } from "./types";
|
|
11
|
+
|
|
12
|
+
export function describeSchema(schema: SchemaLike): JsonValue {
|
|
13
|
+
if (isZodSchema(schema)) {
|
|
14
|
+
const jsonSchema = zodJsonSchema(schema);
|
|
15
|
+
return compactObject({
|
|
16
|
+
kind: "schema",
|
|
17
|
+
vendor: "zod",
|
|
18
|
+
typeName: getSchemaTypeName(schema),
|
|
19
|
+
jsonSchema,
|
|
20
|
+
jsonSchemaHash:
|
|
21
|
+
jsonSchema === undefined
|
|
22
|
+
? undefined
|
|
23
|
+
: digest(canonicalJson(jsonSchema)),
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
const standard = isRecord(schema) ? schema["~standard"] : undefined;
|
|
27
|
+
if (isRecord(standard)) {
|
|
28
|
+
return compactObject({
|
|
29
|
+
kind: "schema",
|
|
30
|
+
standard: "standard-schema-v1",
|
|
31
|
+
vendor: typeof standard.vendor === "string" ? standard.vendor : "unknown",
|
|
32
|
+
version:
|
|
33
|
+
typeof standard.version === "number" ||
|
|
34
|
+
typeof standard.version === "string"
|
|
35
|
+
? standard.version
|
|
36
|
+
: undefined,
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return compactObject({
|
|
40
|
+
kind: "schema",
|
|
41
|
+
vendor: "zod",
|
|
42
|
+
typeName: getSchemaTypeName(schema),
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export function serializeSmsMatcher(
|
|
47
|
+
value: Record<string, unknown>,
|
|
48
|
+
): Record<string, unknown> {
|
|
49
|
+
const code = value.code;
|
|
50
|
+
if (!isRecord(code)) return value;
|
|
51
|
+
const pattern = code.pattern;
|
|
52
|
+
if (!(pattern instanceof RegExp)) return value;
|
|
53
|
+
return {
|
|
54
|
+
...value,
|
|
55
|
+
code: {
|
|
56
|
+
...code,
|
|
57
|
+
pattern: {
|
|
58
|
+
source: pattern.source,
|
|
59
|
+
flags: pattern.flags,
|
|
60
|
+
},
|
|
61
|
+
},
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function digest(value: string): string {
|
|
66
|
+
return createHash("sha256").update(value).digest("hex");
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function isZodSchema(schema: SchemaLike): schema is ZodType {
|
|
70
|
+
return schema instanceof z.ZodType;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function zodJsonSchema(schema: ZodType): JsonValue | undefined {
|
|
74
|
+
try {
|
|
75
|
+
const jsonSchema = z.toJSONSchema(schema);
|
|
76
|
+
return toJsonValue(jsonSchema);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
if (error instanceof Error) return undefined;
|
|
79
|
+
throw error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function getSchemaTypeName(schema: SchemaLike): string | undefined {
|
|
84
|
+
if (!isRecord(schema)) return undefined;
|
|
85
|
+
const def = schema._def;
|
|
86
|
+
if (!isRecord(def)) return undefined;
|
|
87
|
+
const typeName = def.typeName ?? def.type;
|
|
88
|
+
return typeof typeName === "string" ? typeName : undefined;
|
|
89
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import type { JsonValue } from "./contract-json";
|
|
2
|
+
import type { ProviderDefinition } from "./types";
|
|
3
|
+
|
|
4
|
+
export const PROVIDER_CONTRACT_SCHEMA_VERSION = "2026-06-23";
|
|
5
|
+
|
|
6
|
+
export interface ProviderContractSnapshot {
|
|
7
|
+
readonly schemaVersion: typeof PROVIDER_CONTRACT_SCHEMA_VERSION;
|
|
8
|
+
readonly provider: {
|
|
9
|
+
readonly id: string;
|
|
10
|
+
readonly version: string;
|
|
11
|
+
readonly runtime: ProviderDefinition["runtime"];
|
|
12
|
+
};
|
|
13
|
+
readonly allowedHosts?: readonly string[];
|
|
14
|
+
readonly stealth?: JsonValue;
|
|
15
|
+
readonly proxy?: JsonValue;
|
|
16
|
+
readonly stt?: JsonValue;
|
|
17
|
+
readonly browser?: JsonValue;
|
|
18
|
+
readonly auth?: JsonValue;
|
|
19
|
+
readonly reviewed?: JsonValue;
|
|
20
|
+
readonly access?: JsonValue;
|
|
21
|
+
readonly secrets?: JsonValue;
|
|
22
|
+
readonly credential?: JsonValue;
|
|
23
|
+
readonly context?: JsonValue;
|
|
24
|
+
readonly meta: JsonValue;
|
|
25
|
+
readonly healthMonitor?: JsonValue;
|
|
26
|
+
readonly healthJourneys?: readonly JsonValue[];
|
|
27
|
+
readonly operations: readonly ProviderContractOperation[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface ProviderContractOperation {
|
|
31
|
+
readonly id: string;
|
|
32
|
+
readonly descriptionKey?: JsonValue;
|
|
33
|
+
readonly docs?: JsonValue;
|
|
34
|
+
readonly whenToUseKeys?: JsonValue;
|
|
35
|
+
readonly whenNotToUseKeys?: JsonValue;
|
|
36
|
+
readonly derivations?: JsonValue;
|
|
37
|
+
readonly inputExamples?: JsonValue;
|
|
38
|
+
readonly annotations?: JsonValue;
|
|
39
|
+
readonly contract?: JsonValue;
|
|
40
|
+
readonly tags?: JsonValue;
|
|
41
|
+
readonly relatedOperations?: JsonValue;
|
|
42
|
+
readonly toolRouter?: JsonValue;
|
|
43
|
+
readonly observability?: JsonValue;
|
|
44
|
+
readonly transport?: JsonValue;
|
|
45
|
+
readonly inputSchema: JsonValue;
|
|
46
|
+
readonly outputSchema: JsonValue;
|
|
47
|
+
readonly fixtures?: JsonValue;
|
|
48
|
+
readonly upstream?: JsonValue;
|
|
49
|
+
readonly hints?: JsonValue;
|
|
50
|
+
readonly healthCheck?: JsonValue;
|
|
51
|
+
readonly healthCheckUnsupported?: JsonValue;
|
|
52
|
+
}
|
package/src/contract.ts
ADDED
|
@@ -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:
|
|
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., "
|
|
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
|
};
|
package/src/i18n/catalog.ts
CHANGED
|
@@ -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[];
|