@apifuse/provider-sdk 2.0.0-beta.1 → 2.1.0-beta.0

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 (78) hide show
  1. package/AUTHORING.md +102 -0
  2. package/CHANGELOG.md +14 -0
  3. package/README.md +100 -28
  4. package/bin/apifuse-check.ts +78 -71
  5. package/bin/apifuse-create.ts +12 -0
  6. package/bin/apifuse-dev.ts +24 -61
  7. package/bin/apifuse-pack-check.ts +47 -0
  8. package/bin/apifuse-perf.ts +33 -32
  9. package/bin/apifuse-record.ts +17 -7
  10. package/bin/apifuse-test.ts +6 -4
  11. package/bin/apifuse.ts +36 -35
  12. package/package.json +28 -9
  13. package/src/ceremonies/index.ts +747 -0
  14. package/src/cli/commands.ts +87 -0
  15. package/src/cli/create.ts +845 -0
  16. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  17. package/src/cli/templates/provider/README.md.tpl +28 -0
  18. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  19. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  20. package/src/cli/templates/provider/index.ts.tpl +54 -0
  21. package/src/cli/templates/provider/start.ts.tpl +5 -0
  22. package/src/composite.ts +43 -0
  23. package/src/define.ts +527 -41
  24. package/src/dev.ts +2 -6
  25. package/src/errors.ts +42 -0
  26. package/src/index.ts +50 -38
  27. package/src/lint.ts +574 -0
  28. package/src/provider.ts +14 -0
  29. package/src/runtime/auth-flow.ts +67 -0
  30. package/src/runtime/credential.ts +95 -0
  31. package/src/runtime/env.ts +13 -0
  32. package/src/runtime/executor.ts +13 -14
  33. package/src/runtime/http.ts +10 -2
  34. package/src/runtime/insights.ts +3 -3
  35. package/src/runtime/key-derivation.ts +122 -0
  36. package/src/runtime/keyring.ts +148 -0
  37. package/src/runtime/namespace.ts +33 -0
  38. package/src/runtime/prevalidate.ts +252 -0
  39. package/src/runtime/tls.ts +20 -5
  40. package/src/runtime/waterfall.ts +0 -1
  41. package/src/schema.ts +77 -0
  42. package/src/serve.ts +1 -664
  43. package/src/server/index.ts +22 -0
  44. package/src/server/serve.ts +610 -0
  45. package/src/server/types.ts +78 -0
  46. package/src/stealth/profiles.ts +10 -93
  47. package/src/testing/run.ts +391 -32
  48. package/src/types.ts +364 -41
  49. package/bin/apifuse-init.ts +0 -387
  50. package/src/__tests__/auth.test.ts +0 -396
  51. package/src/__tests__/browser-auth.test.ts +0 -180
  52. package/src/__tests__/browser.test.ts +0 -632
  53. package/src/__tests__/define.test.ts +0 -225
  54. package/src/__tests__/errors.test.ts +0 -69
  55. package/src/__tests__/executor.test.ts +0 -214
  56. package/src/__tests__/http.test.ts +0 -238
  57. package/src/__tests__/insights.test.ts +0 -210
  58. package/src/__tests__/instrumentation.test.ts +0 -290
  59. package/src/__tests__/otlp.test.ts +0 -141
  60. package/src/__tests__/perf.test.ts +0 -60
  61. package/src/__tests__/providers-yaml.test.ts +0 -135
  62. package/src/__tests__/proxy.test.ts +0 -359
  63. package/src/__tests__/recipes.test.ts +0 -36
  64. package/src/__tests__/serve.test.ts +0 -233
  65. package/src/__tests__/session.test.ts +0 -231
  66. package/src/__tests__/state.test.ts +0 -100
  67. package/src/__tests__/stealth.test.ts +0 -57
  68. package/src/__tests__/testing.test.ts +0 -97
  69. package/src/__tests__/tls.test.ts +0 -345
  70. package/src/__tests__/types.test.ts +0 -142
  71. package/src/__tests__/utils.test.ts +0 -62
  72. package/src/__tests__/waterfall.test.ts +0 -270
  73. package/src/config/providers-yaml.ts +0 -370
  74. package/src/index.test.ts +0 -1
  75. package/src/protocol.ts +0 -183
  76. package/src/runtime/auth.ts +0 -245
  77. package/src/runtime/session.ts +0 -573
  78. package/src/runtime/state.ts +0 -124
@@ -1,231 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import {
4
- createSessionStore,
5
- createSqliteSessionStore,
6
- type SessionFetch,
7
- SupabaseSessionStore,
8
- } from "../runtime/session";
9
-
10
- type StoredSecretRecord = {
11
- credential_id: string;
12
- encrypted_blob: string;
13
- encryption_key_version: number;
14
- iv: string;
15
- tag: string;
16
- };
17
-
18
- function createStore() {
19
- return createSqliteSessionStore({
20
- databasePath: ":memory:",
21
- namespace: "test-suite",
22
- });
23
- }
24
-
25
- function createMockSupabaseFetch(seed?: StoredSecretRecord) {
26
- let storedRecord = seed ?? null;
27
-
28
- const fetchImpl: SessionFetch = async (input, init) => {
29
- const requestUrl = new URL(String(input));
30
- const method = init?.method ?? "GET";
31
-
32
- if (requestUrl.pathname !== "/rest/v1/credential_secrets") {
33
- return new Response("not found", { status: 404 });
34
- }
35
-
36
- if (method === "GET") {
37
- const credentialId = requestUrl.searchParams
38
- .get("credential_id")
39
- ?.replace("eq.", "");
40
- const result =
41
- storedRecord && storedRecord.credential_id === credentialId
42
- ? [
43
- {
44
- encrypted_blob: storedRecord.encrypted_blob,
45
- encryption_key_version: storedRecord.encryption_key_version,
46
- iv: storedRecord.iv,
47
- tag: storedRecord.tag,
48
- },
49
- ]
50
- : [];
51
-
52
- return Response.json(result);
53
- }
54
-
55
- if (method === "POST") {
56
- const body = JSON.parse(String(init?.body)) as StoredSecretRecord[];
57
- storedRecord = body[0] ?? null;
58
- return new Response(null, { status: 201 });
59
- }
60
-
61
- if (method === "DELETE") {
62
- const credentialId = requestUrl.searchParams
63
- .get("credential_id")
64
- ?.replace("eq.", "");
65
- if (storedRecord?.credential_id === credentialId) {
66
- storedRecord = null;
67
- }
68
- return new Response(null, { status: 204 });
69
- }
70
-
71
- return new Response("method not allowed", { status: 405 });
72
- };
73
-
74
- return {
75
- fetchImpl,
76
- getStoredRecord: () => storedRecord,
77
- };
78
- }
79
-
80
- function createSupabaseStore(seed?: StoredSecretRecord) {
81
- const mock = createMockSupabaseFetch(seed);
82
- return {
83
- ...mock,
84
- store: new SupabaseSessionStore({
85
- credentialId: "conn_123",
86
- encryptionKey: "12345678901234567890123456789012",
87
- fetch: mock.fetchImpl,
88
- supabaseKey: "service-role-key",
89
- supabaseUrl: "https://example.supabase.co",
90
- }),
91
- };
92
- }
93
-
94
- describe("createSessionStore", () => {
95
- it("round-trips stored values", async () => {
96
- const store = createStore();
97
- const session = "accessToken=token-123;refreshToken=refresh-456";
98
-
99
- await store.set("auth", session);
100
-
101
- expect(await store.get("auth")).toEqual(session);
102
- });
103
-
104
- it("returns null for missing keys", async () => {
105
- const store = createStore();
106
-
107
- expect(await store.get("missing")).toBeNull();
108
- });
109
-
110
- it("returns null for expired keys", async () => {
111
- const store = createStore();
112
-
113
- await store.set("expired", "token=stale", "0ms");
114
-
115
- expect(await store.get("expired")).toBeNull();
116
- });
117
-
118
- it("deletes keys", async () => {
119
- const store = createStore();
120
-
121
- await store.set("auth", "token=live");
122
- await store.delete("auth");
123
-
124
- expect(await store.get("auth")).toBeNull();
125
- });
126
-
127
- it("upserts existing keys", async () => {
128
- const store = createStore();
129
-
130
- await store.set("auth", "token=first");
131
- await store.set("auth", "token=second");
132
-
133
- expect(await store.get("auth")).toBe("token=second");
134
- });
135
-
136
- it("stores expires_at based on ttl parsing", async () => {
137
- const store = createStore();
138
- const startedAt = Date.now();
139
-
140
- await store.set("auth", "token=ttl", "15m");
141
-
142
- const record = store.__unsafeInspect("auth");
143
-
144
- expect(record).not.toBeNull();
145
- expect(record?.expiresAt).not.toBeNull();
146
- expect(record?.expiresAt ?? 0).toBeGreaterThanOrEqual(
147
- startedAt + 15 * 60 * 1000 - 2_000,
148
- );
149
- expect(record?.expiresAt ?? 0).toBeLessThanOrEqual(
150
- startedAt + 15 * 60 * 1000 + 2_000,
151
- );
152
- });
153
-
154
- it("creates a sqlite store via factory", async () => {
155
- const store = createSessionStore({
156
- backend: "sqlite",
157
- dbPath: ":memory:",
158
- namespace: "factory-test",
159
- });
160
-
161
- await store.set("auth", "token=factory");
162
- expect(await store.get("auth")).toBe("token=factory");
163
- });
164
- });
165
-
166
- describe("SupabaseSessionStore", () => {
167
- it("encrypts and round-trips stored values", async () => {
168
- const { getStoredRecord, store } = createSupabaseStore();
169
-
170
- await store.set("accessToken", "token-123");
171
- await store.set("refreshToken", "refresh-456");
172
-
173
- expect(await store.get("accessToken")).toBe("token-123");
174
- expect(await store.get("refreshToken")).toBe("refresh-456");
175
-
176
- const record = getStoredRecord();
177
- expect(record).not.toBeNull();
178
- expect(record?.encrypted_blob).not.toContain("token-123");
179
- expect(record?.iv).toBeTruthy();
180
- expect(record?.tag).toBeTruthy();
181
- expect(record?.encryption_key_version).toBe(1);
182
- });
183
-
184
- it("returns null for missing keys", async () => {
185
- const { store } = createSupabaseStore();
186
-
187
- expect(await store.get("missing")).toBeNull();
188
- });
189
-
190
- it("deletes individual keys and removes the row when empty", async () => {
191
- const { getStoredRecord, store } = createSupabaseStore();
192
-
193
- await store.set("accessToken", "token-123");
194
- await store.set("refreshToken", "refresh-456");
195
- await store.delete("accessToken");
196
-
197
- expect(await store.get("accessToken")).toBeNull();
198
- expect(await store.get("refreshToken")).toBe("refresh-456");
199
- expect(getStoredRecord()).not.toBeNull();
200
-
201
- await store.delete("refreshToken");
202
- expect(getStoredRecord()).toBeNull();
203
- });
204
-
205
- it("creates a supabase store via factory", async () => {
206
- const mock = createMockSupabaseFetch();
207
- const store = createSessionStore({
208
- backend: "supabase",
209
- credentialId: "conn_123",
210
- encryptionKey: "12345678901234567890123456789012",
211
- fetch: mock.fetchImpl,
212
- supabaseKey: "service-role-key",
213
- supabaseUrl: "https://example.supabase.co",
214
- }) as SupabaseSessionStore;
215
-
216
- await store.set("accessToken", "factory-token");
217
- expect(await store.get("accessToken")).toBe("factory-token");
218
- });
219
-
220
- it("loads the full session envelope", async () => {
221
- const { store } = createSupabaseStore();
222
-
223
- await store.set("accessToken", "token-123");
224
- await store.set("refreshToken", "refresh-456");
225
-
226
- expect(await store.loadAll()).toEqual({
227
- accessToken: "token-123",
228
- refreshToken: "refresh-456",
229
- });
230
- });
231
- });
@@ -1,100 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { createStateContext } from "../runtime/state";
4
-
5
- function mutateSegment(value: string): string {
6
- if (value.length === 0) {
7
- return "x";
8
- }
9
-
10
- const lastCharacter = value.at(-1);
11
- const replacement = lastCharacter === "a" ? "b" : "a";
12
-
13
- return `${value.slice(0, -1)}${replacement}`;
14
- }
15
-
16
- describe("createStateContext", () => {
17
- it("round-trips sealed values", async () => {
18
- const state = createStateContext();
19
- const data = {
20
- bookingId: "booking-123",
21
- flags: ["priority", "vip"],
22
- };
23
-
24
- const token = await state.seal(data);
25
-
26
- await expect(state.unseal(token)).resolves.toEqual(data);
27
- });
28
-
29
- it("returns null for expired tokens", async () => {
30
- const state = createStateContext();
31
- const token = await state.seal({ retry: true }, { ttl: "1ms" });
32
-
33
- await new Promise((resolve) => setTimeout(resolve, 5));
34
-
35
- await expect(state.unseal(token)).resolves.toBeNull();
36
- });
37
-
38
- it("returns null for tampered hmac values", async () => {
39
- const state = createStateContext();
40
- const [payload, signature] = (await state.seal({ secure: true })).split(
41
- ".",
42
- );
43
-
44
- await expect(
45
- state.unseal(`${payload}.${mutateSegment(signature ?? "")}`),
46
- ).resolves.toBeNull();
47
- });
48
-
49
- it("returns null for tampered payload values", async () => {
50
- const state = createStateContext();
51
- const [payload, signature] = (await state.seal({ secure: true })).split(
52
- ".",
53
- );
54
-
55
- await expect(
56
- state.unseal(`${mutateSegment(payload ?? "")}.${signature}`),
57
- ).resolves.toBeNull();
58
- });
59
-
60
- it("returns null for malformed tokens without throwing", async () => {
61
- const state = createStateContext();
62
-
63
- const resultPromise = state.unseal("garbage");
64
- expect(resultPromise).toBeInstanceOf(Promise);
65
- await expect(resultPromise).resolves.toBeNull();
66
- });
67
-
68
- it("supports custom secrets", async () => {
69
- const writer = createStateContext("custom-secret");
70
- const reader = createStateContext("custom-secret");
71
-
72
- const token = await writer.seal({ secretScoped: true });
73
-
74
- await expect(reader.unseal(token)).resolves.toEqual({ secretScoped: true });
75
- });
76
-
77
- it("round-trips null payloads", async () => {
78
- const state = createStateContext();
79
-
80
- await expect(state.unseal(await state.seal(null))).resolves.toBeNull();
81
- });
82
-
83
- it("round-trips nested objects", async () => {
84
- const state = createStateContext();
85
- const data = {
86
- checkout: {
87
- items: [
88
- { id: "item-1", quantity: 2 },
89
- { id: "item-2", quantity: 1 },
90
- ],
91
- totals: {
92
- currency: "KRW",
93
- value: 42_000,
94
- },
95
- },
96
- };
97
-
98
- await expect(state.unseal(await state.seal(data))).resolves.toEqual(data);
99
- });
100
- });
@@ -1,57 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
-
3
- import { SDKError } from "../errors";
4
- import { getStealthProfile, listStealthProfiles } from "../stealth/profiles";
5
-
6
- describe("stealth profiles", () => {
7
- it("returns the chrome-131 profile", () => {
8
- const profile = getStealthProfile("chrome-131");
9
-
10
- expect(profile.platform).toBe("macos");
11
- expect(profile.tlsClientIdentifier).toBe("chrome_131");
12
- expect(profile.ja4).toBe("t13d1516h2_8daaf6152771_02713d6af862");
13
- });
14
-
15
- it("returns the chrome-146 profile without unverified ja4", () => {
16
- const profile = getStealthProfile("chrome-146");
17
-
18
- expect(profile.platform).toBe("macos");
19
- expect(profile.tlsClientIdentifier).toBe("chrome_146");
20
- expect(profile.ja4).toBeUndefined();
21
- });
22
-
23
- it("returns the firefox-147 profile", () => {
24
- const profile = getStealthProfile("firefox-147");
25
-
26
- expect(profile.platform).toBe("macos");
27
- expect(profile.tlsClientIdentifier).toBe("firefox_147");
28
- });
29
-
30
- it("returns the ios-safari-26 profile", () => {
31
- const profile = getStealthProfile("ios-safari-26");
32
-
33
- expect(profile.platform).toBe("ios");
34
- expect(profile.tlsClientIdentifier).toBe("safari_ios_26_0");
35
- });
36
-
37
- it("throws SDKError for unknown profiles", () => {
38
- expect(() => getStealthProfile("unknown-profile")).toThrow(SDKError);
39
- expect(() => getStealthProfile("unknown-profile")).toThrow(
40
- "Unknown stealth profile: unknown-profile",
41
- );
42
- });
43
-
44
- it("lists 20+ bundled profiles", () => {
45
- const profiles = listStealthProfiles();
46
-
47
- expect(profiles.length).toBeGreaterThanOrEqual(20);
48
- expect(profiles).toEqual(
49
- expect.arrayContaining([
50
- "chrome-131",
51
- "chrome-146",
52
- "firefox-147",
53
- "ios-safari-26",
54
- ]),
55
- );
56
- });
57
- });
@@ -1,97 +0,0 @@
1
- import { describe, expect, it } from "bun:test";
2
- import { z } from "zod";
3
-
4
- import { defineProvider } from "../define";
5
- import * as sdk from "../index";
6
- import {
7
- describeTransform,
8
- runStandardTests,
9
- snapshotTransform,
10
- toMatchShape,
11
- } from "../testing";
12
-
13
- const testProvider = defineProvider({
14
- id: "test-provider",
15
- version: "1.0.0",
16
- runtime: "standard",
17
- meta: {
18
- displayName: "Test Provider",
19
- category: "test",
20
- },
21
- operations: {
22
- search: {
23
- input: z.object({ q: z.string() }),
24
- output: z.object({ result: z.string() }),
25
- handler: async (_ctx, input: unknown) => {
26
- const { q } = z.object({ q: z.string() }).parse(input);
27
-
28
- return { result: `found: ${q}` };
29
- },
30
- fixtures: {
31
- request: { q: "hello" },
32
- response: { result: "found: hello" },
33
- },
34
- },
35
- },
36
- });
37
-
38
- runStandardTests(testProvider);
39
-
40
- describeTransform("double-value", { value: 2 }, { doubled: 4 }, (raw) => ({
41
- doubled: raw.value * 2,
42
- }));
43
-
44
- describe("toMatchShape", () => {
45
- it("passes when all shape keys match", () => {
46
- expect(() => {
47
- toMatchShape(
48
- { name: "Alice", age: 30, extra: "ignored" },
49
- { name: "Alice", age: 30 },
50
- );
51
- }).not.toThrow();
52
- });
53
-
54
- it("supports nested type descriptors", () => {
55
- expect(() => {
56
- toMatchShape(
57
- {
58
- name: "Alice",
59
- age: 30,
60
- tags: ["pro"],
61
- metadata: { source: "fixture" },
62
- },
63
- {
64
- name: "string",
65
- age: "number",
66
- tags: "array",
67
- metadata: { source: "string" },
68
- },
69
- );
70
- }).not.toThrow();
71
- });
72
-
73
- it("fails when shape key does not match", () => {
74
- expect(() => {
75
- toMatchShape({ name: "Bob" }, { name: "Alice" });
76
- }).toThrow();
77
- });
78
-
79
- it("fails when type descriptor does not match", () => {
80
- expect(() => {
81
- toMatchShape({ name: 123 }, { name: "string" });
82
- }).toThrow();
83
- });
84
- });
85
-
86
- describe("testing exports", () => {
87
- it("re-exports testing helpers from package root", () => {
88
- expect(typeof sdk.runStandardTests).toBe("function");
89
- expect("describeTransform" in sdk).toBe(false);
90
- expect(typeof sdk.toMatchShape).toBe("function");
91
- expect(typeof sdk.snapshotTransform).toBe("function");
92
- });
93
-
94
- it("exposes snapshotTransform from testing entrypoint", () => {
95
- expect(typeof snapshotTransform).toBe("function");
96
- });
97
- });