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

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 (63) hide show
  1. package/AUTHORING.md +187 -8
  2. package/CHANGELOG.md +13 -1
  3. package/README.md +40 -18
  4. package/SUBMISSION.md +4 -4
  5. package/bin/apifuse-dev.ts +12 -5
  6. package/bin/apifuse-pack-check.ts +9 -2
  7. package/bin/apifuse-pack-smoke.ts +127 -6
  8. package/bin/apifuse-perf.ts +76 -31
  9. package/bin/apifuse-record.ts +148 -94
  10. package/bin/apifuse-submit-check.ts +243 -7
  11. package/bin/apifuse.ts +1 -1
  12. package/package.json +17 -8
  13. package/src/choice-token.ts +164 -0
  14. package/src/cli/commands.ts +4 -7
  15. package/src/cli/create.ts +180 -51
  16. package/src/cli/templates/provider/.dockerignore.tpl +22 -0
  17. package/src/cli/templates/provider/.gitignore.tpl +22 -0
  18. package/src/cli/templates/provider/README.md.tpl +42 -7
  19. package/src/cli/templates/provider/dev.ts.tpl +1 -1
  20. package/src/cli/templates/provider/domain/README.md.tpl +3 -0
  21. package/src/cli/templates/provider/index.ts.tpl +5 -47
  22. package/src/cli/templates/provider/mappers/README.md.tpl +3 -0
  23. package/src/cli/templates/provider/meta.ts.tpl +7 -0
  24. package/src/cli/templates/provider/operations/index.ts.tpl +5 -0
  25. package/src/cli/templates/provider/operations/ping.ts.tpl +23 -0
  26. package/src/cli/templates/provider/schemas/ping.ts.tpl +16 -0
  27. package/src/cli/templates/provider/start.ts.tpl +1 -1
  28. package/src/cli/templates/provider/upstream/README.md.tpl +3 -0
  29. package/src/config/loader.ts +1206 -9
  30. package/src/define.ts +1620 -106
  31. package/src/errors.ts +12 -0
  32. package/src/i18n/catalog.ts +121 -0
  33. package/src/i18n/index.ts +2 -0
  34. package/src/i18n/keys.ts +64 -0
  35. package/src/index.ts +149 -8
  36. package/src/lint.ts +306 -51
  37. package/src/observability.ts +41 -0
  38. package/src/provider.ts +60 -3
  39. package/src/public-schema-field-lint.ts +237 -0
  40. package/src/runtime/auth-flow.ts +7 -0
  41. package/src/runtime/browser.ts +77 -21
  42. package/src/runtime/cache.ts +582 -0
  43. package/src/runtime/executor.ts +13 -1
  44. package/src/runtime/http.ts +939 -195
  45. package/src/runtime/insights.ts +11 -11
  46. package/src/runtime/instrumentation.ts +12 -4
  47. package/src/runtime/key-derivation.ts +1 -1
  48. package/src/runtime/keyring.ts +4 -3
  49. package/src/runtime/proxy-errors.ts +132 -0
  50. package/src/runtime/proxy-telemetry.ts +253 -0
  51. package/src/runtime/request-options.ts +66 -0
  52. package/src/runtime/state.ts +76 -0
  53. package/src/runtime/stealth.ts +1145 -0
  54. package/src/runtime/stt.ts +629 -0
  55. package/src/runtime/trace.ts +1 -1
  56. package/src/schema.ts +363 -1
  57. package/src/server/serve.ts +816 -58
  58. package/src/server/types.ts +35 -0
  59. package/src/stream.ts +210 -0
  60. package/src/testing/run.ts +17 -4
  61. package/src/types.ts +876 -53
  62. package/src/runtime/tls.ts +0 -434
  63. package/src/types/playwright-stealth.d.ts +0 -9
@@ -1,5 +1,7 @@
1
1
  import { z } from "zod";
2
2
 
3
+ import { HttpRetryPreset } from "../types";
4
+
3
5
  export const ConnectionModeSchema = z.enum([
4
6
  "oauth2",
5
7
  "credentials",
@@ -34,6 +36,39 @@ export const ErrorEnvelopeSchema = z.object({
34
36
 
35
37
  export const OperationSuccessResponseSchema = z.object({
36
38
  data: z.unknown(),
39
+ meta: z
40
+ .object({
41
+ cached: z.boolean().optional(),
42
+ stale: z.boolean().optional(),
43
+ cache: z
44
+ .object({
45
+ hit: z.boolean(),
46
+ stale: z.boolean(),
47
+ keys: z.array(z.string()),
48
+ source: z.enum(["redis", "memory", "loader", "mixed"]).optional(),
49
+ })
50
+ .optional(),
51
+ retry: z
52
+ .object({
53
+ attempts: z.number().int().min(1),
54
+ retries: z.number().int().min(0),
55
+ preset: z
56
+ .enum([
57
+ HttpRetryPreset.Off,
58
+ HttpRetryPreset.TransportTransient,
59
+ HttpRetryPreset.SafeRead,
60
+ HttpRetryPreset.AggressiveRead,
61
+ HttpRetryPreset.RateLimitAware,
62
+ ])
63
+ .optional(),
64
+ transport: z.enum(["native"]),
65
+ lastErrorCode: z.string().optional(),
66
+ lastStatus: z.number().int().optional(),
67
+ })
68
+ .optional(),
69
+ })
70
+ .passthrough()
71
+ .optional(),
37
72
  });
38
73
 
39
74
  export const OperationErrorResponseSchema = z.object({
package/src/stream.ts ADDED
@@ -0,0 +1,210 @@
1
+ import type { SseMessage } from "./types";
2
+
3
+ export interface SseEvent<TData = unknown> {
4
+ event: string;
5
+ data: TData;
6
+ id?: string;
7
+ retry?: number;
8
+ }
9
+
10
+ export interface SseErrorData {
11
+ code: string;
12
+ message: string;
13
+ requestId?: string;
14
+ retryable?: boolean;
15
+ details?: unknown;
16
+ }
17
+
18
+ export const APIFUSE_STREAM_ERROR_EVENT = "apifuse.error";
19
+ export const APIFUSE_STREAM_DONE_EVENT = "apifuse.done";
20
+
21
+ export function event<TData>(
22
+ eventName: string,
23
+ data: TData,
24
+ options: { id?: string; retry?: number } = {},
25
+ ): SseEvent<TData> {
26
+ return {
27
+ event: eventName,
28
+ data,
29
+ ...(options.id ? { id: options.id } : {}),
30
+ ...(options.retry !== undefined ? { retry: options.retry } : {}),
31
+ };
32
+ }
33
+
34
+ export function error(
35
+ code: string,
36
+ message: string,
37
+ options: Omit<SseErrorData, "code" | "message"> & {
38
+ id?: string;
39
+ retry?: number;
40
+ } = {},
41
+ ): SseEvent<SseErrorData> {
42
+ const { id, retry, ...dataOptions } = options;
43
+ return event(
44
+ APIFUSE_STREAM_ERROR_EVENT,
45
+ { code, message, ...dataOptions },
46
+ { ...(id ? { id } : {}), ...(retry !== undefined ? { retry } : {}) },
47
+ );
48
+ }
49
+
50
+ export function done(): SseEvent<Record<string, never>>;
51
+ export function done<TData>(
52
+ data: TData,
53
+ options?: { id?: string; retry?: number },
54
+ ): SseEvent<TData>;
55
+ export function done<TData>(
56
+ data?: TData,
57
+ options: { id?: string; retry?: number } = {},
58
+ ): SseEvent<TData | Record<string, never>> {
59
+ return event(APIFUSE_STREAM_DONE_EVENT, data ?? {}, options);
60
+ }
61
+
62
+ export function encodeSseEvent(input: SseEvent): string {
63
+ const lines: string[] = [];
64
+ if (input.id !== undefined)
65
+ lines.push(`id: ${sseFieldValue(input.id, "id")}`);
66
+ if (input.event) lines.push(`event: ${sseFieldValue(input.event, "event")}`);
67
+ if (input.retry !== undefined) {
68
+ if (!Number.isInteger(input.retry) || input.retry < 0) {
69
+ throw new TypeError("SSE retry must be a non-negative integer.");
70
+ }
71
+ lines.push(`retry: ${input.retry}`);
72
+ }
73
+ const data =
74
+ typeof input.data === "string" ? input.data : JSON.stringify(input.data);
75
+ for (const line of data.split(/\r?\n/)) {
76
+ lines.push(`data: ${line}`);
77
+ }
78
+ return `${lines.join("\n")}\n\n`;
79
+ }
80
+
81
+ function sseFieldValue(value: string, field: "event" | "id"): string {
82
+ if (/[\r\n]/.test(value)) {
83
+ throw new TypeError(`SSE ${field} must not contain CR or LF.`);
84
+ }
85
+ return value;
86
+ }
87
+
88
+ export async function* readableBytes(
89
+ body: ReadableStream<Uint8Array>,
90
+ ): AsyncIterable<Uint8Array> {
91
+ const reader = body.getReader();
92
+ try {
93
+ for (;;) {
94
+ const { value, done } = await reader.read();
95
+ if (done) return;
96
+ if (value) yield value;
97
+ }
98
+ } finally {
99
+ reader.releaseLock();
100
+ }
101
+ }
102
+
103
+ export async function* readableTextChunks(
104
+ body: ReadableStream<Uint8Array>,
105
+ ): AsyncIterable<string> {
106
+ const decoder = new TextDecoder();
107
+ for await (const chunk of readableBytes(body)) {
108
+ yield decoder.decode(chunk, { stream: true });
109
+ }
110
+ const tail = decoder.decode();
111
+ if (tail) yield tail;
112
+ }
113
+
114
+ export async function* readableLines(
115
+ body: ReadableStream<Uint8Array>,
116
+ ): AsyncIterable<string> {
117
+ let buffer = "";
118
+ for await (const chunk of readableTextChunks(body)) {
119
+ buffer += chunk;
120
+ for (;;) {
121
+ const index = buffer.search(/\r?\n/);
122
+ if (index < 0) break;
123
+ const line = buffer.slice(0, index);
124
+ const newlineLength =
125
+ buffer[index] === "\r" && buffer[index + 1] === "\n" ? 2 : 1;
126
+ buffer = buffer.slice(index + newlineLength);
127
+ yield line;
128
+ }
129
+ }
130
+ if (buffer) yield buffer;
131
+ }
132
+
133
+ function createSseMessage(
134
+ eventName: string,
135
+ data: string,
136
+ options: { id?: string; retry?: number },
137
+ ): SseMessage {
138
+ return {
139
+ event: eventName || "message",
140
+ data,
141
+ ...(options.id !== undefined ? { id: options.id } : {}),
142
+ ...(options.retry !== undefined ? { retry: options.retry } : {}),
143
+ json<T = unknown>() {
144
+ return parseJson<T>(data);
145
+ },
146
+ };
147
+ }
148
+
149
+ function parseJson<T>(data: string): T {
150
+ return JSON.parse(data);
151
+ }
152
+
153
+ export async function* parseSseStream(
154
+ body: ReadableStream<Uint8Array>,
155
+ ): AsyncIterable<SseMessage> {
156
+ let eventName = "message";
157
+ let dataLines: string[] = [];
158
+ let id: string | undefined;
159
+ let retry: number | undefined;
160
+
161
+ const dispatch = function* () {
162
+ if (dataLines.length === 0 && id === undefined && retry === undefined) {
163
+ return;
164
+ }
165
+ yield createSseMessage(eventName, dataLines.join("\n"), { id, retry });
166
+ eventName = "message";
167
+ dataLines = [];
168
+ retry = undefined;
169
+ };
170
+
171
+ for await (const line of readableLines(body)) {
172
+ if (line === "") {
173
+ yield* dispatch();
174
+ continue;
175
+ }
176
+ if (line.startsWith(":")) continue;
177
+ const separator = line.indexOf(":");
178
+ const field = separator < 0 ? line : line.slice(0, separator);
179
+ const rawValue = separator < 0 ? "" : line.slice(separator + 1);
180
+ const value = rawValue.startsWith(" ") ? rawValue.slice(1) : rawValue;
181
+ switch (field) {
182
+ case "event":
183
+ eventName = value;
184
+ break;
185
+ case "data":
186
+ dataLines.push(value);
187
+ break;
188
+ case "id":
189
+ id = value;
190
+ break;
191
+ case "retry": {
192
+ const parsed = Number(value);
193
+ if (Number.isInteger(parsed) && parsed >= 0) retry = parsed;
194
+ break;
195
+ }
196
+ }
197
+ }
198
+ yield* dispatch();
199
+ }
200
+
201
+ export const stream = {
202
+ event,
203
+ error,
204
+ done,
205
+ encodeSseEvent,
206
+ parseSseStream,
207
+ readableBytes,
208
+ readableTextChunks,
209
+ readableLines,
210
+ };
@@ -1,5 +1,8 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
+ import { createProviderCache } from "../runtime/cache";
4
+ import { createUnsupportedProviderRuntimeState } from "../runtime/state";
5
+ import { createUnsupportedSttClient } from "../runtime/stt";
3
6
  import { safeParseSchemaSync } from "../schema";
4
7
  import type {
5
8
  AuthMode,
@@ -9,7 +12,10 @@ import type {
9
12
  ProviderDefinition,
10
13
  } from "../types";
11
14
 
12
- const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)+$/;
15
+ // Mirrors CONNECTOR_ID_REGEX in ../define.ts, which defineProvider() enforces.
16
+ // A single lowercase segment (no hyphen) is a valid id, so the trailing group
17
+ // is optional (`*`), matching providers like `kakaomap`, `kstartup`, `triple`.
18
+ const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)*$/;
13
19
  const VALID_AUTH_MODES = [
14
20
  "none",
15
21
  "platform-managed",
@@ -154,10 +160,14 @@ function createSnapshotContext(rawFixture: unknown): ProviderContext {
154
160
  post: async () => jsonResponse(rawFixture),
155
161
  put: async () => jsonResponse(rawFixture),
156
162
  delete: async () => jsonResponse(rawFixture),
163
+ stream: async () => unsupported("ctx.http.stream"),
164
+ sse: async () => unsupported("ctx.http.sse"),
157
165
  },
158
- tls: {
159
- fetch: async () => unsupported("ctx.tls.fetch"),
160
- createSession: () => unsupported("ctx.tls.createSession"),
166
+ cache: createProviderCache({ providerId: "standard-test" }),
167
+ state: createUnsupportedProviderRuntimeState(),
168
+ stealth: {
169
+ fetch: async () => unsupported("ctx.stealth.fetch"),
170
+ createSession: () => unsupported("ctx.stealth.createSession"),
161
171
  },
162
172
  browser: {
163
173
  engine: "playwright-stealth",
@@ -170,6 +180,9 @@ function createSnapshotContext(rawFixture: unknown): ProviderContext {
170
180
  requestField: async (name) =>
171
181
  unsupported(`ctx.auth.requestField(${name})`),
172
182
  },
183
+ stt: createUnsupportedSttClient(
184
+ "Standard test snapshot context does not support ctx.stt.transcribe",
185
+ ),
173
186
  };
174
187
  }
175
188