@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
@@ -158,6 +158,10 @@ export function generateLayer2Headers(
158
158
  return headers;
159
159
  }
160
160
 
161
+ const STEALTH_PROFILE_ALIASES: Record<string, string> = {
162
+ "chrome-desktop": "chrome-146",
163
+ };
164
+
161
165
  const STEALTH_PROFILES: Record<string, StealthProfile> = {
162
166
  "chrome-146": createProfile("chrome-146", {
163
167
  platform: "macos",
@@ -169,87 +173,6 @@ const STEALTH_PROFILES: Record<string, StealthProfile> = {
169
173
  h2Settings: CHROMIUM_H2_SETTINGS,
170
174
  headerOrder: CHROMIUM_HEADER_ORDER,
171
175
  }),
172
- "chrome-144": createProfile("chrome-144", {
173
- platform: "macos",
174
- version: "144.0.0.0",
175
- userAgent:
176
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/144.0.0.0 Safari/537.36",
177
- tlsClientIdentifier: "chrome_144",
178
- ja3: CHROMIUM_JA3,
179
- h2Settings: CHROMIUM_H2_SETTINGS,
180
- headerOrder: CHROMIUM_HEADER_ORDER,
181
- }),
182
- "chrome-133": createProfile("chrome-133", {
183
- platform: "macos",
184
- version: "133.0.0.0",
185
- userAgent:
186
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36",
187
- tlsClientIdentifier: "chrome_133",
188
- ja3: CHROMIUM_JA3,
189
- h2Settings: CHROMIUM_H2_SETTINGS,
190
- headerOrder: CHROMIUM_HEADER_ORDER,
191
- }),
192
- "chrome-131": createProfile("chrome-131", {
193
- platform: "macos",
194
- version: "131.0.0.0",
195
- userAgent:
196
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
197
- tlsClientIdentifier: "chrome_131",
198
- ja3: CHROMIUM_JA3,
199
- ja4: "t13d1516h2_8daaf6152771_02713d6af862",
200
- h2Settings: CHROMIUM_H2_SETTINGS,
201
- headerOrder: CHROMIUM_HEADER_ORDER,
202
- }),
203
- "chrome-124": createProfile("chrome-124", {
204
- platform: "macos",
205
- version: "124.0.0.0",
206
- userAgent:
207
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
208
- tlsClientIdentifier: "chrome_124",
209
- ja3: CHROMIUM_JA3,
210
- h2Settings: CHROMIUM_H2_SETTINGS,
211
- headerOrder: CHROMIUM_HEADER_ORDER,
212
- }),
213
- "chrome-120": createProfile("chrome-120", {
214
- platform: "macos",
215
- version: "120.0.0.0",
216
- userAgent:
217
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36",
218
- tlsClientIdentifier: "chrome_120",
219
- ja3: CHROMIUM_JA3,
220
- h2Settings: CHROMIUM_H2_SETTINGS,
221
- headerOrder: CHROMIUM_HEADER_ORDER,
222
- }),
223
- "chrome-146-psk": createProfile("chrome-146-psk", {
224
- platform: "macos",
225
- version: "146.0.0.0",
226
- userAgent:
227
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/146.0.0.0 Safari/537.36",
228
- tlsClientIdentifier: "chrome_146_PSK",
229
- ja3: CHROMIUM_JA3,
230
- h2Settings: CHROMIUM_H2_SETTINGS,
231
- headerOrder: CHROMIUM_HEADER_ORDER,
232
- }),
233
- "chrome-131-psk": createProfile("chrome-131-psk", {
234
- platform: "macos",
235
- version: "131.0.0.0",
236
- userAgent:
237
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
238
- tlsClientIdentifier: "chrome_131_PSK",
239
- ja3: CHROMIUM_JA3,
240
- h2Settings: CHROMIUM_H2_SETTINGS,
241
- headerOrder: CHROMIUM_HEADER_ORDER,
242
- }),
243
- "chrome-130-psk": createProfile("chrome-130-psk", {
244
- platform: "macos",
245
- version: "130.0.0.0",
246
- userAgent:
247
- "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/130.0.0.0 Safari/537.36",
248
- tlsClientIdentifier: "chrome_130_PSK",
249
- ja3: CHROMIUM_JA3,
250
- h2Settings: CHROMIUM_H2_SETTINGS,
251
- headerOrder: CHROMIUM_HEADER_ORDER,
252
- }),
253
176
  "firefox-147": createProfile("firefox-147", {
254
177
  platform: "macos",
255
178
  version: "147.0",
@@ -310,16 +233,6 @@ const STEALTH_PROFILES: Record<string, StealthProfile> = {
310
233
  h2Settings: SAFARI_H2_SETTINGS,
311
234
  headerOrder: SAFARI_HEADER_ORDER,
312
235
  }),
313
- "edge-131": createProfile("edge-131", {
314
- platform: "windows",
315
- version: "131.0.0.0",
316
- userAgent:
317
- "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0",
318
- tlsClientIdentifier: "chrome_131",
319
- ja3: CHROMIUM_JA3,
320
- h2Settings: CHROMIUM_H2_SETTINGS,
321
- headerOrder: CHROMIUM_HEADER_ORDER,
322
- }),
323
236
  "ios-safari-26": createProfile("ios-safari-26", {
324
237
  platform: "ios",
325
238
  version: "26.0",
@@ -373,7 +286,8 @@ const STEALTH_PROFILES: Record<string, StealthProfile> = {
373
286
  };
374
287
 
375
288
  export function getStealthProfile(name: string): StealthProfile {
376
- const profile = STEALTH_PROFILES[name];
289
+ const canonicalName = STEALTH_PROFILE_ALIASES[name] ?? name;
290
+ const profile = STEALTH_PROFILES[canonicalName];
377
291
 
378
292
  if (!profile) {
379
293
  throw new SDKError(`Unknown stealth profile: ${name}`);
@@ -387,5 +301,8 @@ export function getStealthProfile(name: string): StealthProfile {
387
301
  }
388
302
 
389
303
  export function listStealthProfiles(): string[] {
390
- return Object.keys(STEALTH_PROFILES);
304
+ return [
305
+ ...Object.keys(STEALTH_PROFILES),
306
+ ...Object.keys(STEALTH_PROFILE_ALIASES),
307
+ ];
391
308
  }
@@ -1,20 +1,270 @@
1
1
  import { describe, expect, it } from "bun:test";
2
2
 
3
- import type { ProviderDefinition } from "../types";
3
+ import { safeParseSchemaSync } from "../schema";
4
+ import type {
5
+ AuthMode,
6
+ CredentialContext,
7
+ HttpResponse,
8
+ ProviderContext,
9
+ ProviderDefinition,
10
+ } from "../types";
4
11
 
5
12
  const CONNECTOR_ID_REGEX = /^[a-z][a-z0-9]*(-[a-z][a-z0-9]*)+$/;
13
+ const VALID_AUTH_MODES = [
14
+ "none",
15
+ "platform-managed",
16
+ "credentials",
17
+ "oauth2",
18
+ ] as const;
19
+ const UPDATE_SNAPSHOT_ARGS = new Set(["-u", "--update-snapshots"]);
6
20
 
7
- type ProviderManifest = {
8
- id?: unknown;
9
- displayName?: unknown;
10
- category?: unknown;
11
- version?: unknown;
12
- sdkVersion?: unknown;
13
- runtime?: unknown;
14
- auth?: unknown;
15
- stealthProfile?: unknown;
16
- language?: unknown;
17
- };
21
+ export interface StandardTestsManifest {
22
+ id?: string;
23
+ displayName?: string;
24
+ category?: string;
25
+ version?: string;
26
+ runtime?: ProviderDefinition["runtime"];
27
+ sdkVersion?: number;
28
+ auth?: AuthMode;
29
+ language?: string;
30
+ signature?: string;
31
+ signatureUri?: string;
32
+ }
33
+
34
+ export interface StandardTestsOptions {
35
+ /** Validate operation request/response fixtures and JSON raw fixture shape. */
36
+ validateFixture?: boolean;
37
+ /** Write/read __fixtures__/transform.snap.json for handler(raw fixture) output. */
38
+ snapshot?: boolean;
39
+ /** Opt-in integration-only manifest signature assertion. */
40
+ verifyManifest?: boolean;
41
+ /** Opt-in auth mode/operation consistency assertion. */
42
+ validateAuthMode?: boolean;
43
+ /** Override inferred __fixtures__ directory for tests generated outside providers/<id>. */
44
+ fixtureDir?: string;
45
+ }
46
+
47
+ interface FixtureEnvelope {
48
+ request?: unknown;
49
+ response?: unknown;
50
+ }
51
+
52
+ function isFixtureEnvelope(value: unknown): value is FixtureEnvelope {
53
+ return (
54
+ value !== null &&
55
+ typeof value === "object" &&
56
+ !Array.isArray(value) &&
57
+ ("request" in value || "response" in value)
58
+ );
59
+ }
60
+
61
+ function isJsonCompatible(value: unknown): boolean {
62
+ if (
63
+ value === null ||
64
+ typeof value === "string" ||
65
+ typeof value === "number" ||
66
+ typeof value === "boolean"
67
+ ) {
68
+ return Number.isFinite(value) || typeof value !== "number";
69
+ }
70
+
71
+ if (Array.isArray(value)) {
72
+ return value.every(isJsonCompatible);
73
+ }
74
+
75
+ if (typeof value === "object" && value !== null) {
76
+ return Object.values(value).every(isJsonCompatible);
77
+ }
78
+
79
+ return false;
80
+ }
81
+
82
+ function sortJson(value: unknown): unknown {
83
+ if (Array.isArray(value)) {
84
+ return value.map(sortJson);
85
+ }
86
+
87
+ if (value !== null && typeof value === "object") {
88
+ return Object.fromEntries(
89
+ Object.entries(value)
90
+ .sort(([left], [right]) => left.localeCompare(right))
91
+ .map(([key, entry]) => [key, sortJson(entry)]),
92
+ );
93
+ }
94
+
95
+ return value;
96
+ }
97
+
98
+ function stableStringify(value: unknown): string {
99
+ return `${JSON.stringify(sortJson(value), null, 2)}\n`;
100
+ }
101
+
102
+ function shouldUpdateSnapshots(): boolean {
103
+ return process.argv.some((arg) => UPDATE_SNAPSHOT_ARGS.has(arg));
104
+ }
105
+
106
+ function inferFixtureDir(providerId: string): string {
107
+ const stack = new Error().stack ?? "";
108
+ const testFile = stack
109
+ .split("\n")
110
+ .map((line) => line.match(/\(?((?:file:\/\/)?[^():]+\.test\.ts)/)?.[1])
111
+ .find((file) => file !== undefined);
112
+
113
+ if (testFile) {
114
+ const pathname = testFile.startsWith("file://")
115
+ ? new URL(testFile).pathname
116
+ : testFile;
117
+ return `${pathname.replace(/\/[^/]+$/, "")}/../__fixtures__`;
118
+ }
119
+
120
+ return `providers/${providerId}/__fixtures__`;
121
+ }
122
+
123
+ function jsonResponse(data: unknown): HttpResponse {
124
+ return {
125
+ status: 200,
126
+ ok: true,
127
+ headers: {},
128
+ data,
129
+ json: async <_T = unknown>() => JSON.parse(JSON.stringify(data)),
130
+ text: async () => JSON.stringify(data),
131
+ };
132
+ }
133
+
134
+ function unsupported(name: string): never {
135
+ throw new Error(`Standard test snapshot context does not support ${name}`);
136
+ }
137
+
138
+ function createSnapshotContext(rawFixture: unknown): ProviderContext {
139
+ const credential: CredentialContext = {
140
+ mode: "none",
141
+ get: () => undefined,
142
+ getAll: () => ({}),
143
+ getAccessToken: () => undefined,
144
+ getScopes: () => [],
145
+ };
146
+
147
+ return {
148
+ env: { get: () => undefined },
149
+ credential,
150
+ request: { headers: {} },
151
+ http: {
152
+ request: async () => jsonResponse(rawFixture),
153
+ get: async () => jsonResponse(rawFixture),
154
+ post: async () => jsonResponse(rawFixture),
155
+ put: async () => jsonResponse(rawFixture),
156
+ delete: async () => jsonResponse(rawFixture),
157
+ },
158
+ tls: {
159
+ fetch: async () => unsupported("ctx.tls.fetch"),
160
+ createSession: () => unsupported("ctx.tls.createSession"),
161
+ },
162
+ browser: {
163
+ engine: "playwright-stealth",
164
+ newPage: async () => unsupported("ctx.browser.newPage"),
165
+ },
166
+ trace: {
167
+ span: async (_name, fn) => fn(),
168
+ },
169
+ auth: {
170
+ requestField: async (name) =>
171
+ unsupported(`ctx.auth.requestField(${name})`),
172
+ },
173
+ };
174
+ }
175
+
176
+ async function transformSnapshotOutput(
177
+ provider: ProviderDefinition,
178
+ rawFixture: unknown,
179
+ ): Promise<unknown> {
180
+ const entries = Object.entries(provider.operations);
181
+ const context = createSnapshotContext(rawFixture);
182
+ const outputs = await Promise.all(
183
+ entries.map(async ([operationName, operation]) => {
184
+ const request = operation.fixtures?.request ?? {};
185
+ const output = await operation.handler(context, request);
186
+ return [operationName, output] as const;
187
+ }),
188
+ );
189
+
190
+ if (outputs.length === 1) {
191
+ return outputs[0]?.[1];
192
+ }
193
+
194
+ return Object.fromEntries(outputs);
195
+ }
196
+
197
+ function formatJson(value: unknown): string {
198
+ return JSON.stringify(value, null, 2);
199
+ }
200
+
201
+ function formatJsonDiff(current: unknown, expected: unknown): string {
202
+ const currentLines = formatJson(current).split("\n");
203
+ const expectedLines = formatJson(expected).split("\n");
204
+ const lineCount = Math.max(currentLines.length, expectedLines.length);
205
+ const lines = ["JSON diff (- current, + expected):"];
206
+
207
+ for (let index = 0; index < lineCount; index += 1) {
208
+ const currentLine = currentLines[index];
209
+ const expectedLine = expectedLines[index];
210
+
211
+ if (currentLine === expectedLine) {
212
+ if (currentLine !== undefined) {
213
+ lines.push(` ${currentLine}`);
214
+ }
215
+ continue;
216
+ }
217
+
218
+ if (currentLine !== undefined) {
219
+ lines.push(`- ${currentLine}`);
220
+ }
221
+ if (expectedLine !== undefined) {
222
+ lines.push(`+ ${expectedLine}`);
223
+ }
224
+ }
225
+
226
+ return lines.join("\n");
227
+ }
228
+
229
+ function expectSchemaFixture(
230
+ operationName: string,
231
+ fieldName: "request" | "response",
232
+ fixture: unknown,
233
+ result: ReturnType<typeof safeParseSchemaSync>,
234
+ ): void {
235
+ if (result.success) {
236
+ expect(result.success).toBe(true);
237
+ return;
238
+ }
239
+
240
+ throw new Error(
241
+ [
242
+ `Fixture ${operationName}.${fieldName} failed schema validation.`,
243
+ formatJsonDiff(
244
+ { valid: false, value: fixture, error: result.error },
245
+ { valid: true, value: fixture },
246
+ ),
247
+ ].join("\n"),
248
+ );
249
+ }
250
+
251
+ function parseSchemaFixture(
252
+ operationName: string,
253
+ fieldName: "request" | "response",
254
+ schema: ProviderDefinition["operations"][string]["input"],
255
+ fixture: unknown,
256
+ ): void {
257
+ expectSchemaFixture(
258
+ operationName,
259
+ fieldName,
260
+ fixture,
261
+ safeParseSchemaSync(
262
+ schema,
263
+ fixture,
264
+ `operations.${operationName}.fixtures.${fieldName}`,
265
+ ),
266
+ );
267
+ }
18
268
 
19
269
  /**
20
270
  * Run standard SDK tests for a provider in one line.
@@ -22,15 +272,84 @@ type ProviderManifest = {
22
272
  * Usage:
23
273
  * import { myProvider } from "../index";
24
274
  * import { runStandardTests } from "@apifuse/provider-sdk/testing";
25
- * runStandardTests(myProvider);
275
+ * runStandardTests(myProvider, rawFixture, manifest, { snapshot: true });
26
276
  */
27
277
  export function runStandardTests(
28
278
  provider: ProviderDefinition,
29
- _rawFixture?: unknown,
30
- manifest?: ProviderManifest,
279
+ rawFixture?: unknown,
280
+ manifest?: StandardTestsManifest,
281
+ options: StandardTestsOptions = {},
31
282
  ): void {
32
283
  const operations = Object.entries(provider.operations);
33
284
 
285
+ const assertFixtureValidation = (): void => {
286
+ expect(rawFixture).toBeDefined();
287
+ expect(isJsonCompatible(rawFixture)).toBe(true);
288
+
289
+ for (const [operationName, op] of operations) {
290
+ if (op.fixtures?.request !== undefined) {
291
+ parseSchemaFixture(
292
+ operationName,
293
+ "request",
294
+ op.input,
295
+ op.fixtures.request,
296
+ );
297
+ }
298
+
299
+ if (op.fixtures?.response !== undefined) {
300
+ parseSchemaFixture(
301
+ operationName,
302
+ "response",
303
+ op.output,
304
+ op.fixtures.response,
305
+ );
306
+ }
307
+
308
+ if (isFixtureEnvelope(rawFixture)) {
309
+ if (rawFixture.request !== undefined) {
310
+ parseSchemaFixture(
311
+ operationName,
312
+ "request",
313
+ op.input,
314
+ rawFixture.request,
315
+ );
316
+ }
317
+
318
+ if (rawFixture.response !== undefined) {
319
+ parseSchemaFixture(
320
+ operationName,
321
+ "response",
322
+ op.output,
323
+ rawFixture.response,
324
+ );
325
+ }
326
+ }
327
+ }
328
+ };
329
+
330
+ const assertManifestSignature = (): void => {
331
+ expect(manifest).toBeDefined();
332
+ expect(Boolean(manifest?.signature ?? manifest?.signatureUri)).toBe(true);
333
+ };
334
+
335
+ const assertAuthModeContract = (): void => {
336
+ const authMode = provider.auth?.mode ?? "none";
337
+ expect(VALID_AUTH_MODES).toContain(authMode);
338
+
339
+ if (manifest?.auth !== undefined) {
340
+ expect(manifest.auth).toBe(authMode);
341
+ }
342
+
343
+ if (authMode === "credentials" || authMode === "oauth2") {
344
+ expect(provider.auth?.flow).toBeTruthy();
345
+ expect(Object.keys(provider.operations).length).toBeGreaterThan(0);
346
+ expect(provider.credential?.keys.length ?? 0).toBeGreaterThan(0);
347
+ return;
348
+ }
349
+
350
+ expect(provider.credential?.keys ?? []).toHaveLength(0);
351
+ };
352
+
34
353
  describe(`[SDK Standard Tests] ${provider.id}`, () => {
35
354
  it("id follows kebab-case format", () => {
36
355
  expect(CONNECTOR_ID_REGEX.test(provider.id)).toBe(true);
@@ -40,7 +359,7 @@ export function runStandardTests(
40
359
  expect(provider.meta.displayName).toBeTruthy();
41
360
  expect(provider.meta.category).toBeTruthy();
42
361
  expect(provider.version).toBeTruthy();
43
- expect(["standard", "browser"]).toContain(provider.runtime);
362
+ expect(["standard", "shared", "browser"]).toContain(provider.runtime);
44
363
  });
45
364
 
46
365
  it("has at least one operation", () => {
@@ -58,31 +377,71 @@ export function runStandardTests(
58
377
  it("operation schemas can parse fixture data", () => {
59
378
  for (const [operationName, op] of operations) {
60
379
  if (op.fixtures?.request !== undefined && op.input) {
61
- const requestResult = op.input.safeParse(op.fixtures.request);
62
- expect(requestResult.success).toBe(true);
380
+ parseSchemaFixture(
381
+ operationName,
382
+ "request",
383
+ op.input,
384
+ op.fixtures.request,
385
+ );
63
386
  }
64
387
 
65
388
  if (op.fixtures?.response !== undefined && op.output) {
66
- const responseResult = op.output.safeParse(op.fixtures.response);
67
- expect(responseResult.success).toBe(true);
389
+ parseSchemaFixture(
390
+ operationName,
391
+ "response",
392
+ op.output,
393
+ op.fixtures.response,
394
+ );
68
395
  }
69
396
 
70
397
  expect(operationName).toBeTruthy();
71
398
  }
72
399
  });
73
400
 
74
- it("manifest matches provider metadata", () => {
75
- if (!manifest) {
76
- return;
77
- }
78
-
79
- expect(manifest.id).toBe(provider.id);
80
- expect(manifest.displayName).toBe(provider.meta.displayName);
81
- expect(manifest.category).toBe(provider.meta.category);
82
- expect(manifest.version).toBe(provider.version);
83
- expect(manifest.runtime).toBe(provider.runtime);
84
- expect(manifest.sdkVersion).toBe(1);
85
- expect(manifest.language).toBe("typescript");
401
+ it("provider metadata is declared in defineProvider", () => {
402
+ expect(provider.id).toBeTruthy();
403
+ expect(provider.meta.displayName).toBeTruthy();
404
+ expect(provider.meta.category).toBeTruthy();
405
+ expect(provider.version).toBeTruthy();
406
+ expect(VALID_AUTH_MODES).toContain(provider.auth?.mode ?? "none");
86
407
  });
408
+
409
+ if (options.validateFixture) {
410
+ it("validates raw and declared operation fixtures", () => {
411
+ assertFixtureValidation();
412
+ });
413
+ }
414
+
415
+ if (options.verifyManifest) {
416
+ it("verifies manifest signature metadata", () => {
417
+ assertManifestSignature();
418
+ });
419
+ }
420
+
421
+ if (options.validateAuthMode) {
422
+ it("validates auth mode contract", () => {
423
+ assertAuthModeContract();
424
+ });
425
+ }
426
+
427
+ if (options.snapshot) {
428
+ it("matches transform snapshot", async () => {
429
+ expect(rawFixture).toBeDefined();
430
+ const fixtureDir = options.fixtureDir ?? inferFixtureDir(provider.id);
431
+ const snapshotPath = `${fixtureDir}/transform.snap.json`;
432
+ const actual = await transformSnapshotOutput(provider, rawFixture);
433
+ const serialized = stableStringify(actual);
434
+ const snapshotFile = Bun.file(snapshotPath);
435
+
436
+ if (shouldUpdateSnapshots() || !(await snapshotFile.exists())) {
437
+ await Bun.write(snapshotPath, serialized);
438
+ }
439
+
440
+ const expected: unknown = JSON.parse(
441
+ await Bun.file(snapshotPath).text(),
442
+ );
443
+ expect(actual).toEqual(expected);
444
+ });
445
+ }
87
446
  });
88
447
  }