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

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 (79) hide show
  1. package/AUTHORING.md +93 -0
  2. package/CHANGELOG.md +21 -0
  3. package/README.md +133 -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 +87 -0
  8. package/bin/apifuse-pack-smoke.ts +122 -0
  9. package/bin/apifuse-perf.ts +33 -32
  10. package/bin/apifuse-record.ts +17 -7
  11. package/bin/apifuse-test.ts +6 -4
  12. package/bin/apifuse.ts +36 -35
  13. package/package.json +29 -9
  14. package/src/ceremonies/index.ts +768 -0
  15. package/src/cli/commands.ts +87 -0
  16. package/src/cli/create.ts +845 -0
  17. package/src/cli/templates/provider/Dockerfile.tpl +7 -0
  18. package/src/cli/templates/provider/README.md.tpl +41 -0
  19. package/src/cli/templates/provider/dev.ts.tpl +5 -0
  20. package/src/cli/templates/provider/index.test.ts.tpl +13 -0
  21. package/src/cli/templates/provider/index.ts.tpl +58 -0
  22. package/src/cli/templates/provider/start.ts.tpl +5 -0
  23. package/src/config/loader.ts +61 -1
  24. package/src/define.ts +565 -41
  25. package/src/dev.ts +2 -6
  26. package/src/errors.ts +42 -0
  27. package/src/index.ts +44 -38
  28. package/src/lint.ts +574 -0
  29. package/src/provider.ts +13 -0
  30. package/src/runtime/auth-flow.ts +67 -0
  31. package/src/runtime/credential.ts +95 -0
  32. package/src/runtime/env.ts +13 -0
  33. package/src/runtime/executor.ts +13 -14
  34. package/src/runtime/http.ts +36 -12
  35. package/src/runtime/insights.ts +3 -3
  36. package/src/runtime/key-derivation.ts +122 -0
  37. package/src/runtime/keyring.ts +148 -0
  38. package/src/runtime/namespace.ts +33 -0
  39. package/src/runtime/prevalidate.ts +252 -0
  40. package/src/runtime/tls.ts +41 -17
  41. package/src/runtime/waterfall.ts +0 -1
  42. package/src/schema.ts +77 -0
  43. package/src/serve.ts +1 -664
  44. package/src/server/index.ts +22 -0
  45. package/src/server/serve.ts +624 -0
  46. package/src/server/types.ts +78 -0
  47. package/src/stealth/profiles.ts +10 -93
  48. package/src/testing/run.ts +391 -32
  49. package/src/types.ts +390 -41
  50. package/bin/apifuse-init.ts +0 -387
  51. package/src/__tests__/auth.test.ts +0 -396
  52. package/src/__tests__/browser-auth.test.ts +0 -180
  53. package/src/__tests__/browser.test.ts +0 -632
  54. package/src/__tests__/define.test.ts +0 -225
  55. package/src/__tests__/errors.test.ts +0 -69
  56. package/src/__tests__/executor.test.ts +0 -214
  57. package/src/__tests__/http.test.ts +0 -238
  58. package/src/__tests__/insights.test.ts +0 -210
  59. package/src/__tests__/instrumentation.test.ts +0 -290
  60. package/src/__tests__/otlp.test.ts +0 -141
  61. package/src/__tests__/perf.test.ts +0 -60
  62. package/src/__tests__/providers-yaml.test.ts +0 -135
  63. package/src/__tests__/proxy.test.ts +0 -359
  64. package/src/__tests__/recipes.test.ts +0 -36
  65. package/src/__tests__/serve.test.ts +0 -233
  66. package/src/__tests__/session.test.ts +0 -231
  67. package/src/__tests__/state.test.ts +0 -100
  68. package/src/__tests__/stealth.test.ts +0 -57
  69. package/src/__tests__/testing.test.ts +0 -97
  70. package/src/__tests__/tls.test.ts +0 -345
  71. package/src/__tests__/types.test.ts +0 -142
  72. package/src/__tests__/utils.test.ts +0 -62
  73. package/src/__tests__/waterfall.test.ts +0 -270
  74. package/src/config/providers-yaml.ts +0 -370
  75. package/src/index.test.ts +0 -1
  76. package/src/protocol.ts +0 -183
  77. package/src/runtime/auth.ts +0 -245
  78. package/src/runtime/session.ts +0 -573
  79. 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
- });