@apifuse/provider-sdk 2.1.0-beta.0 → 2.1.0-beta.10

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.
Files changed (213) hide show
  1. package/AUTHORING.md +218 -21
  2. package/CHANGELOG.md +54 -0
  3. package/README.md +147 -10
  4. package/SUBMISSION.md +87 -0
  5. package/bin/apifuse-check.ts +86 -4
  6. package/bin/apifuse-dev.ts +87 -13
  7. package/bin/apifuse-pack-check.ts +120 -0
  8. package/bin/apifuse-pack-smoke.ts +423 -0
  9. package/bin/apifuse-perf.ts +142 -49
  10. package/bin/apifuse-record.ts +182 -104
  11. package/bin/apifuse-submit-check.ts +2538 -0
  12. package/bin/apifuse.ts +1 -1
  13. package/dist/ceremonies/index.d.ts +41 -0
  14. package/dist/ceremonies/index.js +490 -0
  15. package/dist/choice-token.d.ts +24 -0
  16. package/dist/choice-token.js +74 -0
  17. package/dist/cli/commands.d.ts +10 -0
  18. package/dist/cli/commands.js +80 -0
  19. package/dist/cli/create.d.ts +47 -0
  20. package/dist/cli/create.js +762 -0
  21. package/dist/cli/templates/provider/.dockerignore.tpl +22 -0
  22. package/dist/cli/templates/provider/.gitignore.tpl +22 -0
  23. package/dist/cli/templates/provider/Dockerfile.tpl +7 -0
  24. package/dist/cli/templates/provider/README.md.tpl +160 -0
  25. package/dist/cli/templates/provider/dev.ts.tpl +5 -0
  26. package/dist/cli/templates/provider/domain/README.md.tpl +3 -0
  27. package/dist/cli/templates/provider/index.test.ts.tpl +13 -0
  28. package/dist/cli/templates/provider/index.ts.tpl +15 -0
  29. package/dist/cli/templates/provider/mappers/README.md.tpl +3 -0
  30. package/dist/cli/templates/provider/meta.ts.tpl +7 -0
  31. package/dist/cli/templates/provider/operations/index.ts.tpl +5 -0
  32. package/dist/cli/templates/provider/operations/ping.ts.tpl +24 -0
  33. package/dist/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  34. package/dist/cli/templates/provider/start.ts.tpl +5 -0
  35. package/dist/cli/templates/provider/upstream/README.md.tpl +3 -0
  36. package/dist/config/loader.d.ts +107 -0
  37. package/dist/config/loader.js +935 -0
  38. package/dist/contract-json.d.ts +9 -0
  39. package/dist/contract-json.js +51 -0
  40. package/dist/contract-serialization.d.ts +4 -0
  41. package/dist/contract-serialization.js +78 -0
  42. package/dist/contract-types.d.ts +49 -0
  43. package/dist/contract-types.js +1 -0
  44. package/dist/contract.d.ts +6 -0
  45. package/dist/contract.js +155 -0
  46. package/dist/define.d.ts +97 -0
  47. package/dist/define.js +1320 -0
  48. package/dist/dev.d.ts +9 -0
  49. package/dist/dev.js +15 -0
  50. package/dist/errors.d.ts +59 -0
  51. package/dist/errors.js +97 -0
  52. package/dist/i18n/catalog.d.ts +29 -0
  53. package/dist/i18n/catalog.js +159 -0
  54. package/dist/i18n/index.d.ts +2 -0
  55. package/dist/i18n/index.js +2 -0
  56. package/dist/i18n/keys.d.ts +10 -0
  57. package/dist/i18n/keys.js +34 -0
  58. package/dist/index.d.ts +41 -0
  59. package/dist/index.js +37 -0
  60. package/dist/lint.d.ts +73 -0
  61. package/dist/lint.js +702 -0
  62. package/dist/observability.d.ts +5 -0
  63. package/dist/observability.js +39 -0
  64. package/dist/provider.d.ts +9 -0
  65. package/dist/provider.js +8 -0
  66. package/dist/public-schema-field-lint.d.ts +2 -0
  67. package/dist/public-schema-field-lint.js +158 -0
  68. package/dist/recipes/gov-api.d.ts +19 -0
  69. package/dist/recipes/gov-api.js +72 -0
  70. package/dist/recipes/rest-api.d.ts +21 -0
  71. package/dist/recipes/rest-api.js +115 -0
  72. package/dist/runtime/auth-flow.d.ts +14 -0
  73. package/dist/runtime/auth-flow.js +44 -0
  74. package/dist/runtime/browser.d.ts +25 -0
  75. package/dist/runtime/browser.js +1034 -0
  76. package/dist/runtime/cache.d.ts +10 -0
  77. package/dist/runtime/cache.js +372 -0
  78. package/dist/runtime/choice.d.ts +15 -0
  79. package/dist/runtime/choice.js +435 -0
  80. package/dist/runtime/credential.d.ts +8 -0
  81. package/dist/runtime/credential.js +61 -0
  82. package/dist/runtime/env.d.ts +2 -0
  83. package/dist/runtime/env.js +10 -0
  84. package/dist/runtime/executor.d.ts +16 -0
  85. package/dist/runtime/executor.js +51 -0
  86. package/dist/runtime/http.d.ts +8 -0
  87. package/dist/runtime/http.js +706 -0
  88. package/dist/runtime/insights.d.ts +9 -0
  89. package/dist/runtime/insights.js +324 -0
  90. package/dist/runtime/instrumentation.d.ts +8 -0
  91. package/dist/runtime/instrumentation.js +269 -0
  92. package/dist/runtime/key-derivation.d.ts +24 -0
  93. package/dist/runtime/key-derivation.js +73 -0
  94. package/dist/runtime/keyring.d.ts +25 -0
  95. package/dist/runtime/keyring.js +93 -0
  96. package/dist/runtime/namespace.d.ts +9 -0
  97. package/dist/runtime/namespace.js +19 -0
  98. package/dist/runtime/otlp.d.ts +39 -0
  99. package/dist/runtime/otlp.js +103 -0
  100. package/dist/runtime/perf.d.ts +12 -0
  101. package/dist/runtime/perf.js +52 -0
  102. package/dist/runtime/prevalidate.d.ts +12 -0
  103. package/dist/runtime/prevalidate.js +173 -0
  104. package/dist/runtime/provider.d.ts +2 -0
  105. package/dist/runtime/provider.js +11 -0
  106. package/dist/runtime/proxy-errors.d.ts +21 -0
  107. package/dist/runtime/proxy-errors.js +83 -0
  108. package/dist/runtime/proxy-telemetry.d.ts +8 -0
  109. package/dist/runtime/proxy-telemetry.js +174 -0
  110. package/dist/runtime/redis.d.ts +17 -0
  111. package/dist/runtime/redis.js +82 -0
  112. package/dist/runtime/request-options.d.ts +3 -0
  113. package/dist/runtime/request-options.js +42 -0
  114. package/dist/runtime/state.d.ts +17 -0
  115. package/dist/runtime/state.js +344 -0
  116. package/dist/runtime/stealth.d.ts +18 -0
  117. package/dist/runtime/stealth.js +834 -0
  118. package/dist/runtime/stt.d.ts +22 -0
  119. package/dist/runtime/stt.js +480 -0
  120. package/dist/runtime/trace.d.ts +26 -0
  121. package/dist/runtime/trace.js +142 -0
  122. package/dist/runtime/waterfall.d.ts +12 -0
  123. package/dist/runtime/waterfall.js +147 -0
  124. package/dist/schema.d.ts +74 -0
  125. package/dist/schema.js +243 -0
  126. package/dist/serve.d.ts +1 -0
  127. package/dist/serve.js +1 -0
  128. package/dist/server/index.d.ts +3 -0
  129. package/dist/server/index.js +2 -0
  130. package/dist/server/serve.d.ts +64 -0
  131. package/dist/server/serve.js +1110 -0
  132. package/dist/server/types.d.ts +136 -0
  133. package/dist/server/types.js +86 -0
  134. package/dist/stealth/profiles.d.ts +4 -0
  135. package/dist/stealth/profiles.js +259 -0
  136. package/dist/stream.d.ts +44 -0
  137. package/dist/stream.js +151 -0
  138. package/dist/testing/helpers.d.ts +23 -0
  139. package/dist/testing/helpers.js +95 -0
  140. package/dist/testing/index.d.ts +2 -0
  141. package/dist/testing/index.js +2 -0
  142. package/dist/testing/run.d.ts +34 -0
  143. package/dist/testing/run.js +303 -0
  144. package/dist/types.d.ts +1326 -0
  145. package/dist/types.js +61 -0
  146. package/dist/utils/date.d.ts +6 -0
  147. package/dist/utils/date.js +101 -0
  148. package/dist/utils/parse.d.ts +16 -0
  149. package/dist/utils/parse.js +51 -0
  150. package/dist/utils/text.d.ts +4 -0
  151. package/dist/utils/text.js +14 -0
  152. package/dist/utils/transform.d.ts +8 -0
  153. package/dist/utils/transform.js +48 -0
  154. package/package.json +57 -29
  155. package/src/ceremonies/index.ts +30 -3
  156. package/src/choice-token.ts +165 -0
  157. package/src/cli/commands.ts +34 -11
  158. package/src/cli/create.ts +214 -52
  159. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  160. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  161. package/src/cli/templates/provider/README.md.tpl +134 -2
  162. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  163. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  164. package/src/cli/templates/provider/index.ts.tpl +5 -44
  165. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  166. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  167. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  168. package/src/cli/templates/provider/operations/ping.ts.tpl +24 -0
  169. package/src/cli/templates/provider/schemas/ping.ts.tpl +24 -0
  170. package/src/cli/templates/provider/start.ts.tpl +1 -1
  171. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  172. package/src/config/loader.ts +1282 -7
  173. package/src/contract-json.ts +75 -0
  174. package/src/contract-serialization.ts +89 -0
  175. package/src/contract-types.ts +52 -0
  176. package/src/contract.ts +215 -0
  177. package/src/define.ts +1726 -48
  178. package/src/errors.ts +27 -0
  179. package/src/i18n/catalog.ts +277 -0
  180. package/src/i18n/index.ts +2 -0
  181. package/src/i18n/keys.ts +64 -0
  182. package/src/index.ts +174 -15
  183. package/src/lint.ts +547 -73
  184. package/src/observability.ts +41 -0
  185. package/src/provider.ts +104 -5
  186. package/src/public-schema-field-lint.ts +237 -0
  187. package/src/runtime/auth-flow.ts +7 -0
  188. package/src/runtime/browser.ts +762 -51
  189. package/src/runtime/cache.ts +528 -0
  190. package/src/runtime/choice.ts +760 -0
  191. package/src/runtime/executor.ts +32 -3
  192. package/src/runtime/http.ts +945 -185
  193. package/src/runtime/insights.ts +11 -11
  194. package/src/runtime/instrumentation.ts +12 -4
  195. package/src/runtime/key-derivation.ts +1 -1
  196. package/src/runtime/keyring.ts +4 -3
  197. package/src/runtime/proxy-errors.ts +132 -0
  198. package/src/runtime/proxy-telemetry.ts +253 -0
  199. package/src/runtime/redis.ts +116 -0
  200. package/src/runtime/request-options.ts +66 -0
  201. package/src/runtime/state.ts +563 -0
  202. package/src/runtime/stealth.ts +1159 -0
  203. package/src/runtime/stt.ts +629 -0
  204. package/src/runtime/trace.ts +1 -1
  205. package/src/schema.ts +363 -1
  206. package/src/server/serve.ts +1172 -76
  207. package/src/server/types.ts +37 -0
  208. package/src/stream.ts +210 -0
  209. package/src/testing/run.ts +31 -5
  210. package/src/types.ts +1118 -44
  211. package/src/composite.ts +0 -43
  212. package/src/runtime/tls.ts +0 -425
  213. package/src/types/playwright-stealth.d.ts +0 -9
@@ -0,0 +1,22 @@
1
+ import type { ProviderSttConfig, SttContext, SttTranscribeRequest, SttVerificationCodeOptions, VerificationCodeExtractionResult } from "../types";
2
+ export declare const APIFUSE__STT__BACKEND_ENV = "APIFUSE__STT__BACKEND";
3
+ export declare const APIFUSE__STT__MODEL_ENV = "APIFUSE__STT__MODEL";
4
+ export declare const CLOUDFLARE_ACCOUNT_ID_ENV = "APIFUSE__CLOUDFLARE__ACCOUNT_ID";
5
+ export declare const APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV = "APIFUSE__STT__CLOUDFLARE_API_TOKEN";
6
+ export declare const CLOUDFLARE_WORKERS_AI_STT_BACKEND = "cloudflare-workers-ai";
7
+ export declare const DEFAULT_CLOUDFLARE_WORKERS_AI_STT_MODEL = "@cf/openai/whisper-large-v3-turbo";
8
+ export declare const DEFAULT_STT_MAX_AUDIO_BYTES: number;
9
+ export declare const DEFAULT_STT_TIMEOUT_MS = 30000;
10
+ type EnvLike = Record<string, string | undefined>;
11
+ type CloudflareWorkersAiSttClientOptions = {
12
+ accountId: string;
13
+ apiToken: string;
14
+ model?: string;
15
+ fetch?: typeof fetch;
16
+ };
17
+ export declare function createUnsupportedSttClient(reason?: string): SttContext;
18
+ export declare function createSttClientFromEnv(config: ProviderSttConfig | undefined, env?: EnvLike): SttContext;
19
+ export declare function resolveSttPrompt(request: SttTranscribeRequest): string | undefined;
20
+ export declare function createCloudflareWorkersAiSttClient(options: CloudflareWorkersAiSttClientOptions): SttContext;
21
+ export declare function extractVerificationCode(text: string, options?: SttVerificationCodeOptions): VerificationCodeExtractionResult;
22
+ export {};
@@ -0,0 +1,480 @@
1
+ import { ProviderError, TransportError, ValidationError } from "../errors";
2
+ export const APIFUSE__STT__BACKEND_ENV = "APIFUSE__STT__BACKEND";
3
+ export const APIFUSE__STT__MODEL_ENV = "APIFUSE__STT__MODEL";
4
+ export const CLOUDFLARE_ACCOUNT_ID_ENV = "APIFUSE__CLOUDFLARE__ACCOUNT_ID";
5
+ export const APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV = "APIFUSE__STT__CLOUDFLARE_API_TOKEN";
6
+ export const CLOUDFLARE_WORKERS_AI_STT_BACKEND = "cloudflare-workers-ai";
7
+ export const DEFAULT_CLOUDFLARE_WORKERS_AI_STT_MODEL = "@cf/openai/whisper-large-v3-turbo";
8
+ export const DEFAULT_STT_MAX_AUDIO_BYTES = 10 * 1024 * 1024;
9
+ export const DEFAULT_STT_TIMEOUT_MS = 30_000;
10
+ const BASE64_AUDIO_PATTERN = /^[A-Za-z0-9+/]+={0,2}$/;
11
+ const DEFAULT_OTP_HINT = "Transcribe verification codes using Arabic numerals only. Preserve leading zeros and spacing.";
12
+ function providerError(message, options) {
13
+ return new ProviderError(message, options);
14
+ }
15
+ function createErrorSttClient(options) {
16
+ return {
17
+ async transcribe() {
18
+ throw providerError(options.message, {
19
+ code: options.code,
20
+ fix: options.fix,
21
+ });
22
+ },
23
+ extractVerificationCode,
24
+ };
25
+ }
26
+ export function createUnsupportedSttClient(reason) {
27
+ return createErrorSttClient({
28
+ code: "STT_UNAVAILABLE",
29
+ message: reason ?? "STT runtime is not configured",
30
+ fix: `Configure ${APIFUSE__STT__BACKEND_ENV} and the matching backend credentials, or provide a test SttContext override.`,
31
+ });
32
+ }
33
+ function normalizedEnvValue(env, key) {
34
+ const value = env[key]?.trim();
35
+ return value ? value : undefined;
36
+ }
37
+ export function createSttClientFromEnv(config, env = process.env) {
38
+ if (!config) {
39
+ return createUnsupportedSttClient("Provider does not declare STT capability");
40
+ }
41
+ const backend = normalizedEnvValue(env, APIFUSE__STT__BACKEND_ENV);
42
+ if (!backend) {
43
+ return createUnsupportedSttClient(config.mode === "required"
44
+ ? `STT is required by this provider but ${APIFUSE__STT__BACKEND_ENV} is not configured`
45
+ : undefined);
46
+ }
47
+ if (backend !== CLOUDFLARE_WORKERS_AI_STT_BACKEND) {
48
+ return createErrorSttClient({
49
+ code: "UNSUPPORTED_STT_BACKEND",
50
+ message: `Unsupported STT backend "${backend}"`,
51
+ fix: `Use ${APIFUSE__STT__BACKEND_ENV}=${CLOUDFLARE_WORKERS_AI_STT_BACKEND} or provide a custom SttContext override.`,
52
+ });
53
+ }
54
+ const accountId = env.APIFUSE__CLOUDFLARE__ACCOUNT_ID?.trim();
55
+ const apiToken = env.APIFUSE__STT__CLOUDFLARE_API_TOKEN?.trim();
56
+ if (!accountId || !apiToken) {
57
+ return createUnsupportedSttClient(`STT backend ${backend} requires ${CLOUDFLARE_ACCOUNT_ID_ENV} and ${APIFUSE__STT__CLOUDFLARE_API_TOKEN_ENV}`);
58
+ }
59
+ return createCloudflareWorkersAiSttClient({
60
+ accountId,
61
+ apiToken,
62
+ model: normalizedEnvValue(env, APIFUSE__STT__MODEL_ENV) ??
63
+ DEFAULT_CLOUDFLARE_WORKERS_AI_STT_MODEL,
64
+ });
65
+ }
66
+ function base64ByteLength(data) {
67
+ const normalized = data.trim();
68
+ const padding = normalized.endsWith("==")
69
+ ? 2
70
+ : normalized.endsWith("=")
71
+ ? 1
72
+ : 0;
73
+ return Math.floor((normalized.length * 3) / 4) - padding;
74
+ }
75
+ function assertBase64Audio(audio, maxAudioBytes) {
76
+ if (audio.kind !== "base64") {
77
+ throw new ValidationError("Unsupported STT audio input kind", {
78
+ code: "UNSUPPORTED_STT_OPTION",
79
+ fix: 'Use audio: { kind: "base64", data, mediaType } for STT v1.',
80
+ });
81
+ }
82
+ const data = audio.data.trim();
83
+ if (data.length === 0 ||
84
+ data.length % 4 !== 0 ||
85
+ !BASE64_AUDIO_PATTERN.test(data)) {
86
+ throw new ValidationError("STT audio.data must be a base64-encoded string", {
87
+ code: "INVALID_STT_AUDIO",
88
+ });
89
+ }
90
+ const bytes = base64ByteLength(data);
91
+ const maxBytes = maxAudioBytes ?? DEFAULT_STT_MAX_AUDIO_BYTES;
92
+ if (bytes > maxBytes) {
93
+ throw new ValidationError(`STT audio exceeds maxAudioBytes (${bytes} > ${maxBytes})`, { code: "STT_AUDIO_TOO_LARGE" });
94
+ }
95
+ return bytes;
96
+ }
97
+ export function resolveSttPrompt(request) {
98
+ const policy = effectivePromptPolicy(request);
99
+ if (policy === "none")
100
+ return undefined;
101
+ if (policy === "default-hint")
102
+ return DEFAULT_OTP_HINT;
103
+ return request.initialPrompt;
104
+ }
105
+ function effectivePromptPolicy(request) {
106
+ if (request.promptPolicy)
107
+ return request.promptPolicy;
108
+ return request.mode === "otp" ? "default-hint" : "none";
109
+ }
110
+ function warnOrThrowUnsupportedOption(request, message) {
111
+ const policy = request.unsupportedOptionPolicy ?? "warn";
112
+ if (policy === "error") {
113
+ throw new ProviderError(message, { code: "UNSUPPORTED_STT_OPTION" });
114
+ }
115
+ return { code: "UNSUPPORTED_STT_OPTION", message };
116
+ }
117
+ function normalizeCloudflareLanguage(language) {
118
+ return language?.split("-")[0]?.toLowerCase();
119
+ }
120
+ function isTimeoutLikeError(error) {
121
+ return (error instanceof Error &&
122
+ (error.name === "AbortError" ||
123
+ error.name === "TimeoutError" ||
124
+ /\b(timed out|timeout|deadline exceeded)\b/i.test(error.message)));
125
+ }
126
+ function toSttTransportError(error) {
127
+ if (error instanceof TransportError)
128
+ return error;
129
+ if (isTimeoutLikeError(error)) {
130
+ return new TransportError("STT upstream request timed out", {
131
+ code: "transport_timeout",
132
+ status: 0,
133
+ cause: error,
134
+ });
135
+ }
136
+ return new TransportError("STT upstream network request failed", {
137
+ code: "transport_network_error",
138
+ status: 0,
139
+ cause: error instanceof Error ? error : undefined,
140
+ });
141
+ }
142
+ function createTimeoutController(signalTimeoutMs) {
143
+ const controller = new AbortController();
144
+ const timeout = setTimeout(() => controller.abort(), signalTimeoutMs);
145
+ timeout.unref?.();
146
+ return { controller, clear: () => clearTimeout(timeout) };
147
+ }
148
+ function toCloudflareInput(request) {
149
+ const prompt = resolveSttPrompt(request);
150
+ const input = {
151
+ audio: request.audio.data.trim(),
152
+ task: "transcribe",
153
+ };
154
+ const language = normalizeCloudflareLanguage(request.language);
155
+ if (language)
156
+ input.language = language;
157
+ if (prompt)
158
+ input.initial_prompt = prompt;
159
+ return input;
160
+ }
161
+ function unknownRecord(value) {
162
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
163
+ return undefined;
164
+ }
165
+ return Object.fromEntries(Object.entries(value));
166
+ }
167
+ function parseSegments(value) {
168
+ if (!Array.isArray(value))
169
+ return undefined;
170
+ const segments = [];
171
+ for (const item of value) {
172
+ const segment = unknownRecord(item);
173
+ if (!segment || typeof segment.text !== "string")
174
+ continue;
175
+ segments.push({
176
+ text: segment.text,
177
+ startMs: typeof segment.start === "number"
178
+ ? Math.round(segment.start * 1000)
179
+ : typeof segment.startMs === "number"
180
+ ? segment.startMs
181
+ : undefined,
182
+ endMs: typeof segment.end === "number"
183
+ ? Math.round(segment.end * 1000)
184
+ : typeof segment.endMs === "number"
185
+ ? segment.endMs
186
+ : undefined,
187
+ confidence: typeof segment.confidence === "number" ? segment.confidence : undefined,
188
+ });
189
+ }
190
+ return segments.length > 0 ? segments : undefined;
191
+ }
192
+ function toSttTranscript(payload, audioBytes) {
193
+ const envelope = unknownRecord(payload);
194
+ const result = unknownRecord(envelope?.result) ?? envelope;
195
+ const info = unknownRecord(result?.transcription_info);
196
+ const text = (typeof result?.text === "string" ? result.text : undefined) ??
197
+ (typeof info?.text === "string" ? info.text : undefined);
198
+ if (!text) {
199
+ throw new TransportError("STT upstream response did not include transcript text", {
200
+ code: "STT_UPSTREAM_FAILED",
201
+ status: 502,
202
+ });
203
+ }
204
+ const durationMs = typeof result?.durationMs === "number"
205
+ ? result.durationMs
206
+ : typeof info?.duration === "number"
207
+ ? Math.round(info.duration * 1000)
208
+ : undefined;
209
+ return {
210
+ text,
211
+ language: typeof result?.language === "string"
212
+ ? result.language
213
+ : typeof info?.language === "string"
214
+ ? info.language
215
+ : undefined,
216
+ durationMs,
217
+ segments: parseSegments(result?.segments),
218
+ usage: {
219
+ audioBytes,
220
+ ...(durationMs ? { audioDurationMs: durationMs } : {}),
221
+ },
222
+ };
223
+ }
224
+ export function createCloudflareWorkersAiSttClient(options) {
225
+ const model = options.model ?? DEFAULT_CLOUDFLARE_WORKERS_AI_STT_MODEL;
226
+ const runFetch = options.fetch ?? fetch;
227
+ return {
228
+ async transcribe(request) {
229
+ const warnings = [];
230
+ if (request.initialPrompt &&
231
+ effectivePromptPolicy(request) !== "custom-hint") {
232
+ const warning = warnOrThrowUnsupportedOption(request, "initialPrompt is honored only when promptPolicy is custom-hint");
233
+ if (warning)
234
+ warnings.push(warning);
235
+ }
236
+ const audioBytes = assertBase64Audio(request.audio, request.maxAudioBytes);
237
+ const timeout = createTimeoutController(request.timeoutMs ?? DEFAULT_STT_TIMEOUT_MS);
238
+ try {
239
+ let response;
240
+ try {
241
+ response = await runFetch(`https://api.cloudflare.com/client/v4/accounts/${encodeURIComponent(options.accountId)}/ai/run/${model}`, {
242
+ method: "POST",
243
+ headers: {
244
+ Authorization: `Bearer ${options.apiToken}`,
245
+ "Content-Type": "application/json",
246
+ },
247
+ body: JSON.stringify(toCloudflareInput(request)),
248
+ signal: timeout.controller.signal,
249
+ });
250
+ }
251
+ catch (error) {
252
+ throw toSttTransportError(error);
253
+ }
254
+ const payload = await response.json().catch(() => undefined);
255
+ if (!response.ok) {
256
+ throw new TransportError("STT upstream request failed", {
257
+ code: "STT_UPSTREAM_FAILED",
258
+ status: response.status,
259
+ upstreamStatus: response.status,
260
+ });
261
+ }
262
+ const envelope = unknownRecord(payload);
263
+ if (envelope?.success === false) {
264
+ throw new TransportError("STT upstream returned an error", {
265
+ code: "STT_UPSTREAM_FAILED",
266
+ status: 502,
267
+ });
268
+ }
269
+ const transcript = toSttTranscript(payload, audioBytes);
270
+ const withWarnings = warnings.length > 0
271
+ ? {
272
+ ...transcript,
273
+ warnings: [...(transcript.warnings ?? []), ...warnings],
274
+ }
275
+ : transcript;
276
+ if (request.mode === "otp" || request.verificationCode) {
277
+ return {
278
+ ...withWarnings,
279
+ verificationCode: extractVerificationCode(withWarnings.text, request.verificationCode),
280
+ };
281
+ }
282
+ return withWarnings;
283
+ }
284
+ finally {
285
+ timeout.clear();
286
+ }
287
+ },
288
+ extractVerificationCode,
289
+ };
290
+ }
291
+ const EN_DIGITS = {
292
+ zero: "0",
293
+ oh: "0",
294
+ o: "0",
295
+ one: "1",
296
+ two: "2",
297
+ three: "3",
298
+ four: "4",
299
+ five: "5",
300
+ six: "6",
301
+ seven: "7",
302
+ eight: "8",
303
+ nine: "9",
304
+ };
305
+ const KO_DIGITS = {
306
+ 공: "0",
307
+ 영: "0",
308
+ 일: "1",
309
+ 이: "2",
310
+ 삼: "3",
311
+ 사: "4",
312
+ 오: "5",
313
+ 육: "6",
314
+ 륙: "6",
315
+ 칠: "7",
316
+ 팔: "8",
317
+ 구: "9",
318
+ };
319
+ function lengthSet(codeLengths) {
320
+ if (codeLengths === undefined)
321
+ return new Set([4, 5, 6, 7, 8]);
322
+ if (typeof codeLengths === "number")
323
+ return new Set([validCodeLength(codeLengths)]);
324
+ if (Array.isArray(codeLengths)) {
325
+ const values = codeLengths.map((length) => validCodeLength(length));
326
+ return new Set(values);
327
+ }
328
+ if (!("min" in codeLengths) || !("max" in codeLengths)) {
329
+ throw new ValidationError("STT verification code range is malformed", {
330
+ code: "INVALID_STT_VERIFICATION_CODE_OPTIONS",
331
+ });
332
+ }
333
+ const min = validCodeLength(codeLengths.min);
334
+ const max = validCodeLength(codeLengths.max);
335
+ if (min > max) {
336
+ throw new ValidationError("STT verification code range min exceeds max", {
337
+ code: "INVALID_STT_VERIFICATION_CODE_OPTIONS",
338
+ });
339
+ }
340
+ if (max - min > 16) {
341
+ throw new ValidationError("STT verification code range is too large", {
342
+ code: "INVALID_STT_VERIFICATION_CODE_OPTIONS",
343
+ });
344
+ }
345
+ const values = new Set();
346
+ for (let length = min; length <= max; length += 1) {
347
+ values.add(length);
348
+ }
349
+ return values;
350
+ }
351
+ function validCodeLength(value) {
352
+ if (!Number.isInteger(value) || value < 1 || value > 32) {
353
+ throw new ValidationError("STT verification code length must be an integer between 1 and 32", { code: "INVALID_STT_VERIFICATION_CODE_OPTIONS" });
354
+ }
355
+ return value;
356
+ }
357
+ function sourceForGroup(tokens) {
358
+ const sources = new Set(tokens.map((token) => token.source));
359
+ return sources.size === 1 ? (tokens[0]?.source ?? "digits") : "mixed";
360
+ }
361
+ function emitWordToken(token, startIndex) {
362
+ const lower = token.toLowerCase();
363
+ const english = EN_DIGITS[lower];
364
+ if (english) {
365
+ return [
366
+ {
367
+ text: token,
368
+ digits: english,
369
+ source: "spoken_words",
370
+ startIndex,
371
+ endIndex: startIndex + token.length,
372
+ },
373
+ ];
374
+ }
375
+ const korean = KO_DIGITS[token];
376
+ if (korean) {
377
+ return [
378
+ {
379
+ text: token,
380
+ digits: korean,
381
+ source: "spoken_words",
382
+ startIndex,
383
+ endIndex: startIndex + token.length,
384
+ },
385
+ ];
386
+ }
387
+ const chars = [...token];
388
+ if (chars.length > 1 && chars.every((char) => KO_DIGITS[char])) {
389
+ let offset = startIndex;
390
+ return chars.map((char) => {
391
+ const item = {
392
+ text: char,
393
+ digits: KO_DIGITS[char] ?? "",
394
+ source: "spoken_words",
395
+ startIndex: offset,
396
+ endIndex: offset + char.length,
397
+ };
398
+ offset += char.length;
399
+ return item;
400
+ });
401
+ }
402
+ return [];
403
+ }
404
+ function tokenizeDigits(text) {
405
+ const tokens = [];
406
+ for (const match of text.matchAll(/[0-9]+|[A-Za-z]+|[가-힣]+/gu)) {
407
+ const value = match[0];
408
+ const index = match.index ?? 0;
409
+ if (/^[0-9]+$/.test(value)) {
410
+ tokens.push({
411
+ text: value,
412
+ digits: value,
413
+ source: "digits",
414
+ startIndex: index,
415
+ endIndex: index + value.length,
416
+ });
417
+ continue;
418
+ }
419
+ tokens.push(...emitWordToken(value, index));
420
+ }
421
+ return tokens.sort((a, b) => a.startIndex - b.startIndex);
422
+ }
423
+ function isAdjacent(left, right, text) {
424
+ const between = text.slice(left.endIndex, right.startIndex);
425
+ return /^[\s,.:;\-_/]*$/u.test(between);
426
+ }
427
+ function candidatesFromTokens(text, allowedLengths) {
428
+ const tokens = tokenizeDigits(text);
429
+ const candidates = [];
430
+ let group = [];
431
+ const flush = () => {
432
+ if (group.length === 0)
433
+ return;
434
+ const code = group.map((token) => token.digits).join("");
435
+ if (allowedLengths.has(code.length)) {
436
+ candidates.push({
437
+ code,
438
+ source: sourceForGroup(group),
439
+ startIndex: group[0]?.startIndex,
440
+ endIndex: group[group.length - 1]?.endIndex,
441
+ });
442
+ }
443
+ group = [];
444
+ };
445
+ for (const token of tokens) {
446
+ const previous = group[group.length - 1];
447
+ if (previous && !isAdjacent(previous, token, text)) {
448
+ flush();
449
+ }
450
+ group.push(token);
451
+ }
452
+ flush();
453
+ return candidates;
454
+ }
455
+ export function extractVerificationCode(text, options = {}) {
456
+ const allowedLengths = lengthSet(options.codeLengths);
457
+ const candidatesByCode = new Map();
458
+ for (const candidate of candidatesFromTokens(text, allowedLengths)) {
459
+ if (!candidatesByCode.has(candidate.code)) {
460
+ candidatesByCode.set(candidate.code, candidate);
461
+ }
462
+ }
463
+ const candidates = [...candidatesByCode.values()];
464
+ if (candidates.length === 0) {
465
+ throw new ProviderError("No verification code candidate found in transcript", {
466
+ code: "NO_CODE_FOUND",
467
+ });
468
+ }
469
+ if (candidates.length > 1) {
470
+ throw new ProviderError("Multiple verification code candidates found", {
471
+ code: "AMBIGUOUS_CODE",
472
+ });
473
+ }
474
+ const [candidate] = candidates;
475
+ return {
476
+ code: candidate.code,
477
+ candidates,
478
+ normalizedText: text.normalize("NFKC"),
479
+ };
480
+ }
@@ -0,0 +1,26 @@
1
+ import type { TraceContext as BaseTraceContext, TraceAttributeValue, TraceConfig, TraceSpan } from "../types";
2
+ import { type OTLPExportOptions } from "./otlp";
3
+ export type SpanAttributeValue = TraceAttributeValue;
4
+ export type Span = TraceSpan;
5
+ export interface TraceContext extends BaseTraceContext {
6
+ getSpans(): Span[];
7
+ }
8
+ export interface CreateTraceContextOptions {
9
+ maxSpans?: number;
10
+ onSpan?: (span: Span) => void;
11
+ exportOptions?: OTLPExportOptions;
12
+ resourceAttributes?: Record<string, string>;
13
+ }
14
+ type SpanHookOptions<T> = {
15
+ attributes?: Record<string, unknown>;
16
+ onSuccess?: (value: T) => Record<string, unknown> | undefined;
17
+ onError?: (error: unknown) => Record<string, unknown> | undefined;
18
+ };
19
+ export interface TraceRecorder {
20
+ runSpan<T>(name: string, fn: () => Promise<T> | T, options?: SpanHookOptions<T>): Promise<T>;
21
+ }
22
+ export declare const TRACE_RECORDER: unique symbol;
23
+ export declare function resolveTraceContextOptions(config?: TraceConfig): CreateTraceContextOptions;
24
+ export declare function getTraceRecorder(trace: BaseTraceContext): TraceRecorder | null;
25
+ export declare function createTraceContext(options?: CreateTraceContextOptions): TraceContext;
26
+ export {};
@@ -0,0 +1,142 @@
1
+ import { AsyncLocalStorage } from "node:async_hooks";
2
+ import { exportSpansOTLP } from "./otlp";
3
+ export const TRACE_RECORDER = Symbol.for("@apifuse/provider-sdk/runtime/trace-recorder");
4
+ function buildOTLPExportOptions(config) {
5
+ if (config?.exporter !== "otlp") {
6
+ return undefined;
7
+ }
8
+ const endpoint = config.otlp?.endpoint ?? config.endpoint;
9
+ if (!endpoint) {
10
+ return undefined;
11
+ }
12
+ return {
13
+ endpoint,
14
+ headers: config.otlp?.headers,
15
+ timeout: config.otlp?.timeout,
16
+ };
17
+ }
18
+ export function resolveTraceContextOptions(config) {
19
+ return {
20
+ maxSpans: config?.maxSpans,
21
+ onSpan: config?.onSpan,
22
+ exportOptions: buildOTLPExportOptions(config),
23
+ };
24
+ }
25
+ function normalizeAttributeValue(value) {
26
+ if (typeof value === "string" ||
27
+ typeof value === "number" ||
28
+ typeof value === "boolean") {
29
+ return value;
30
+ }
31
+ if (value === null || value === undefined) {
32
+ return undefined;
33
+ }
34
+ return String(value);
35
+ }
36
+ function normalizeAttributes(attributes) {
37
+ if (!attributes) {
38
+ return {};
39
+ }
40
+ const normalizedEntries = Object.entries(attributes)
41
+ .map(([key, value]) => [key, normalizeAttributeValue(value)])
42
+ .filter((entry) => entry[1] !== undefined);
43
+ return Object.fromEntries(normalizedEntries);
44
+ }
45
+ function insertCompletedSpan(completed, entry, maxSpans) {
46
+ const insertAt = completed.findIndex((existingEntry) => existingEntry.sequence > entry.sequence);
47
+ if (insertAt === -1) {
48
+ completed.push(entry);
49
+ }
50
+ else {
51
+ completed.splice(insertAt, 0, entry);
52
+ }
53
+ if (completed.length > maxSpans) {
54
+ completed.splice(0, completed.length - maxSpans);
55
+ }
56
+ }
57
+ export function getTraceRecorder(trace) {
58
+ return trace[TRACE_RECORDER] ?? null;
59
+ }
60
+ export function createTraceContext(options = {}) {
61
+ const maxSpans = options.maxSpans ?? 1000;
62
+ const completed = [];
63
+ const activeSpanStorage = new AsyncLocalStorage();
64
+ let sequence = 0;
65
+ let traceContext;
66
+ const scheduleExport = () => {
67
+ if (!traceContext._exportOptions) {
68
+ return;
69
+ }
70
+ const spans = completed.map((entry) => ({ ...entry.span }));
71
+ setImmediate(() => {
72
+ void exportSpansOTLP(spans, traceContext._exportOptions, traceContext._resourceAttributes);
73
+ });
74
+ };
75
+ const recorder = {
76
+ async runSpan(name, fn, spanOptions = {}) {
77
+ const pendingSpan = {
78
+ id: crypto.randomUUID(),
79
+ name,
80
+ startedAt: Date.now(),
81
+ parentId: activeSpanStorage.getStore()?.id,
82
+ sequence: sequence++,
83
+ attributes: normalizeAttributes(spanOptions.attributes),
84
+ };
85
+ const finalize = (status, extraAttributes, error) => {
86
+ const endedAt = Date.now();
87
+ const duration = endedAt - pendingSpan.startedAt;
88
+ const attributes = {
89
+ ...pendingSpan.attributes,
90
+ ...normalizeAttributes(extraAttributes),
91
+ };
92
+ if (attributes.duration_ms === undefined) {
93
+ attributes.duration_ms = duration;
94
+ }
95
+ const span = {
96
+ id: pendingSpan.id,
97
+ name: pendingSpan.name,
98
+ startedAt: pendingSpan.startedAt,
99
+ endedAt,
100
+ duration_ms: duration,
101
+ status,
102
+ attributes,
103
+ ...(error ? { error } : {}),
104
+ ...(pendingSpan.parentId ? { parentId: pendingSpan.parentId } : {}),
105
+ };
106
+ insertCompletedSpan(completed, { sequence: pendingSpan.sequence, span }, maxSpans);
107
+ options.onSpan?.(span);
108
+ if (!pendingSpan.parentId) {
109
+ scheduleExport();
110
+ }
111
+ };
112
+ return activeSpanStorage.run(pendingSpan, async () => {
113
+ try {
114
+ const value = await fn();
115
+ const successAttributes = spanOptions.onSuccess?.(value);
116
+ finalize("ok", successAttributes ?? undefined);
117
+ return value;
118
+ }
119
+ catch (error) {
120
+ const errorMessage = error instanceof Error ? error.message : String(error);
121
+ const errorAttributes = spanOptions.onError?.(error);
122
+ finalize("error", errorAttributes ?? undefined, errorMessage);
123
+ throw error;
124
+ }
125
+ });
126
+ },
127
+ };
128
+ traceContext = {
129
+ span(name, fn) {
130
+ return recorder.runSpan(name, fn);
131
+ },
132
+ getSpans() {
133
+ return completed.map((entry) => ({ ...entry.span }));
134
+ },
135
+ ...(options.exportOptions ? { _exportOptions: options.exportOptions } : {}),
136
+ ...(options.resourceAttributes
137
+ ? { _resourceAttributes: { ...options.resourceAttributes } }
138
+ : {}),
139
+ [TRACE_RECORDER]: recorder,
140
+ };
141
+ return traceContext;
142
+ }
@@ -0,0 +1,12 @@
1
+ import type { Span } from "./trace";
2
+ export type WaterfallRequest = {
3
+ method: string;
4
+ path: string;
5
+ status: number;
6
+ totalMs: number;
7
+ };
8
+ export type WaterfallOptions = {
9
+ slowThresholdMs?: number;
10
+ maxBarWidth?: number;
11
+ };
12
+ export declare function renderWaterfall(spans: Span[], request: WaterfallRequest, options?: WaterfallOptions): string;