@apifuse/provider-sdk 2.2.0-beta.35 → 2.2.0-beta.37

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 (44) hide show
  1. package/AUTHORING.md +22 -8
  2. package/CHANGELOG.md +8 -0
  3. package/README.md +20 -8
  4. package/bin/apifuse-dev.ts +1 -1
  5. package/bin/apifuse-pack-types.ts +6 -5
  6. package/bin/apifuse-record.ts +1 -1
  7. package/bin/apifuse-submit-check.ts +23 -10
  8. package/dist/ceremonies/index.d.ts +8 -0
  9. package/dist/ceremonies/index.js +32 -25
  10. package/dist/cli/templates/provider/Dockerfile.tpl +1 -1
  11. package/dist/cli/templates/provider/index.ts.tpl +6 -3
  12. package/dist/cli/templates/provider/operations/ping.ts.tpl +2 -1
  13. package/dist/define.d.ts +46 -25
  14. package/dist/define.js +381 -9
  15. package/dist/index.d.ts +2 -2
  16. package/dist/provider.d.ts +2 -1
  17. package/dist/runtime/browser.js +19 -11
  18. package/dist/runtime/choice.js +24 -7
  19. package/dist/runtime/resolver-public.d.ts +1 -1
  20. package/dist/runtime/resolver-public.js +1 -1
  21. package/dist/runtime/resolver-vendors/browser.js +57 -14
  22. package/dist/runtime/resolver-vendors/types.d.ts +9 -1
  23. package/dist/runtime/resolver-vendors/types.js +15 -0
  24. package/dist/runtime/resolver.d.ts +1 -0
  25. package/dist/runtime/resolver.js +13 -7
  26. package/dist/server/serve-implementation.js +25 -0
  27. package/dist/types.d.ts +25 -17
  28. package/package.json +6 -1
  29. package/src/ceremonies/index.ts +45 -31
  30. package/src/cli/templates/provider/Dockerfile.tpl +1 -1
  31. package/src/cli/templates/provider/index.ts.tpl +6 -3
  32. package/src/cli/templates/provider/operations/ping.ts.tpl +2 -1
  33. package/src/define.ts +462 -59
  34. package/src/index.ts +5 -2
  35. package/src/provider.ts +6 -1
  36. package/src/runtime/browser.ts +34 -11
  37. package/src/runtime/choice.ts +25 -8
  38. package/src/runtime/resolver-public.ts +2 -0
  39. package/src/runtime/resolver-vendors/browser.ts +69 -11
  40. package/src/runtime/resolver-vendors/types.ts +21 -0
  41. package/src/runtime/resolver.ts +17 -5
  42. package/src/server/serve-implementation.ts +39 -1
  43. package/src/testing/run.ts +3 -3
  44. package/src/types.ts +41 -17
@@ -1,9 +1,9 @@
1
- import { defineProvider } from "@apifuse/provider-sdk/provider";
1
+ import { defineProvider, type ProviderContextOf } from "@apifuse/provider-sdk/provider";
2
2
 
3
3
  import { providerMeta } from "./meta";
4
4
  import { operations } from "./operations";
5
5
 
6
- export default defineProvider({
6
+ const buildProvider = defineProvider({
7
7
  id: "{{PROVIDER_ID}}",
8
8
  version: "1.0.0",
9
9
  runtime: "{{RUNTIME}}"{{BROWSER_BLOCK}},
@@ -11,5 +11,8 @@ export default defineProvider({
11
11
  reviewed: "community",
12
12
  {{SECRETS_BLOCK}}{{CREDENTIAL_BLOCK}}auth: {{AUTH_BLOCK}},
13
13
  meta: providerMeta,
14
- operations: operations,
15
14
  });
15
+
16
+ export type ProviderContext = ProviderContextOf<typeof buildProvider>;
17
+
18
+ export default buildProvider({ operations });
@@ -1,8 +1,9 @@
1
1
  import { defineOperation } from "@apifuse/provider-sdk/provider";
2
+ import type { ProviderContext } from "../index";
2
3
 
3
4
  import { pingInputSchema, pingOutputSchema } from "../schemas/ping";
4
5
 
5
- export const pingOperation = defineOperation({
6
+ export const pingOperation = defineOperation<ProviderContext>()({
6
7
  descriptionKey: "operations.ping.description",
7
8
  input: pingInputSchema,
8
9
  output: pingOutputSchema,
package/src/define.ts CHANGED
@@ -31,6 +31,8 @@ import type {
31
31
  ProviderAccessConfig,
32
32
  ProviderChallengeKind,
33
33
  ProviderDefinition,
34
+ ProviderContext,
35
+ ProviderContextFor,
34
36
  ProviderOcrConfig,
35
37
  ProviderDeploymentOverrides,
36
38
  ProviderHealthMonitorConfig,
@@ -197,71 +199,383 @@ function parsePositiveMsDuration(value: string): number | undefined {
197
199
  return parsed;
198
200
  }
199
201
 
200
- type ProviderOperation = OperationDefinition<SchemaLike, SchemaLike>;
201
- type OperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
202
- OperationDefinition<TInput, TOutput>,
203
- "handler"
204
- > & {
202
+ type ProviderOperation = OperationDefinition<any, any, any>;
203
+ type OperationConfig<
204
+ TInput extends SchemaLike,
205
+ TOutput extends SchemaLike,
206
+ TContext = ProviderContext,
207
+ > = Omit<OperationDefinition<TInput, TOutput, TContext>, "handler"> & {
205
208
  handler(
206
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
209
+ ctx: TContext,
207
210
  input: InferSchemaOutput<TInput>,
208
211
  ):
209
212
  | OperationHandlerResult<InferSchemaOutput<TOutput>>
210
213
  | Promise<OperationHandlerResult<InferSchemaOutput<TOutput>>>;
211
214
  };
212
- type OperationMapConfig<TOperations extends Record<string, ProviderOperation>> = {
215
+ type OperationMapConfig<
216
+ TOperations extends Record<string, ProviderOperation>,
217
+ TContext = ProviderContext,
218
+ > = {
213
219
  [K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer TInput, infer TOutput>
214
- ? OperationConfig<TInput, TOutput> | OperationDefinition<TInput, TOutput>
220
+ ? OperationConfig<TInput, TOutput, TContext> | OperationDefinition<TInput, TOutput, TContext>
215
221
  : never;
216
222
  };
217
- type StreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> =
218
- | SseOperationConfig<TInput, TOutput>
219
- | HttpStreamOperationConfig<TInput, TOutput>
220
- | WebSocketOperationConfig<TInput, TOutput>;
221
- type SseOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
222
- OperationConfig<TInput, TOutput>,
223
- "handler" | "transport"
224
- > & {
223
+ type StreamOperationConfig<
224
+ TInput extends SchemaLike,
225
+ TOutput extends SchemaLike,
226
+ TContext = ProviderContext,
227
+ > =
228
+ | SseOperationConfig<TInput, TOutput, TContext>
229
+ | HttpStreamOperationConfig<TInput, TOutput, TContext>
230
+ | WebSocketOperationConfig<TInput, TOutput, TContext>;
231
+ type SseOperationConfig<
232
+ TInput extends SchemaLike,
233
+ TOutput extends SchemaLike,
234
+ TContext = ProviderContext,
235
+ > = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
225
236
  transport: OperationSseTransport;
226
237
  handler(
227
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
238
+ ctx: TContext,
228
239
  input: InferSchemaOutput<TInput>,
229
240
  ): AsyncIterable<ProviderStreamEvent> | Promise<AsyncIterable<ProviderStreamEvent>>;
230
241
  };
231
- type HttpStreamOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
232
- OperationConfig<TInput, TOutput>,
233
- "handler" | "transport"
234
- > & {
242
+ type HttpStreamOperationConfig<
243
+ TInput extends SchemaLike,
244
+ TOutput extends SchemaLike,
245
+ TContext = ProviderContext,
246
+ > = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
235
247
  transport: OperationHttpStreamTransport;
236
248
  handler(
237
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
249
+ ctx: TContext,
238
250
  input: InferSchemaOutput<TInput>,
239
251
  ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
240
252
  };
241
- type WebSocketOperationConfig<TInput extends SchemaLike, TOutput extends SchemaLike> = Omit<
242
- OperationConfig<TInput, TOutput>,
243
- "handler" | "transport"
244
- > & {
253
+ type WebSocketOperationConfig<
254
+ TInput extends SchemaLike,
255
+ TOutput extends SchemaLike,
256
+ TContext = ProviderContext,
257
+ > = Omit<OperationConfig<TInput, TOutput, TContext>, "handler" | "transport"> & {
245
258
  transport: OperationWebSocketTransport;
246
259
  handler(
247
- ctx: Parameters<OperationDefinition<TInput, TOutput>["handler"]>[0],
260
+ ctx: TContext,
248
261
  input: InferSchemaOutput<TInput>,
249
262
  ): Response | ReadableStream<Uint8Array> | Promise<Response | ReadableStream<Uint8Array>>;
250
263
  };
251
264
 
252
- type AuthStartNoInputGuard<TConfig> = TConfig extends {
265
+ type AuthStartHandlerNoInputGuard<TStart> = TStart extends (...args: infer TArgs) => unknown
266
+ ? TArgs["length"] extends 0 | 1
267
+ ? unknown
268
+ : {
269
+ "auth start handlers must not declare input parameters; return a form turn from start and receive user input in continue": never;
270
+ }
271
+ : unknown;
272
+
273
+ export type AuthStartNoInputGuard<TConfig> = TConfig extends {
253
274
  auth?: { flow?: { start: infer TStart } };
254
275
  }
255
- ? TStart extends (...args: infer TArgs) => unknown
256
- ? TArgs extends [unknown]
257
- ? unknown
258
- : {
259
- "auth start handlers must not declare input parameters; return a form turn from start and receive user input in continue": never;
276
+ ? AuthStartHandlerNoInputGuard<TStart>
277
+ : TConfig extends { start: infer TStart }
278
+ ? AuthStartHandlerNoInputGuard<TStart>
279
+ : unknown;
280
+
281
+ function splitAuthStartParameters(parameters: string): string[] | undefined {
282
+ const parts: string[] = [];
283
+ let start = 0;
284
+ let round = 0;
285
+ let square = 0;
286
+ let curly = 0;
287
+ let quote: "'" | '"' | "`" | undefined;
288
+ let escaped = false;
289
+ let lineComment = false;
290
+ let blockComment = false;
291
+ const templateDepths = [0];
292
+ templateDepths.length = 0;
293
+
294
+ for (let index = 0; index < parameters.length; index++) {
295
+ const character = parameters[index];
296
+ const nextCharacter = parameters[index + 1];
297
+ if (lineComment) {
298
+ if (character === "\n" || character === "\r") lineComment = false;
299
+ else continue;
300
+ }
301
+ if (blockComment) {
302
+ if (character === "*" && nextCharacter === "/") {
303
+ blockComment = false;
304
+ index++;
305
+ }
306
+ continue;
307
+ }
308
+ if (quote) {
309
+ if (escaped) {
310
+ escaped = false;
311
+ } else if (character === "\\") {
312
+ escaped = true;
313
+ } else if (quote === "`" && character === "$" && nextCharacter === "{") {
314
+ curly++;
315
+ templateDepths.push(curly);
316
+ quote = undefined;
317
+ index++;
318
+ } else if (character === quote) {
319
+ quote = undefined;
320
+ }
321
+ continue;
322
+ }
323
+ if (character === "'" || character === '"' || character === "`") {
324
+ quote = character;
325
+ continue;
326
+ }
327
+ if (character === "/" && nextCharacter === "/") {
328
+ lineComment = true;
329
+ index++;
330
+ continue;
331
+ }
332
+ if (character === "/" && nextCharacter === "*") {
333
+ blockComment = true;
334
+ index++;
335
+ continue;
336
+ }
337
+ if (character === "/") return undefined;
338
+ // Annex B HTML-like comments are not lexed here; give up rather than
339
+ // risk misreading the parameter list.
340
+ if (character === "<" && parameters.startsWith("!--", index + 1)) return undefined;
341
+ if (character === "-" && parameters.startsWith("->", index + 1)) return undefined;
342
+ if (character === "(") round++;
343
+ else if (character === ")") round--;
344
+ else if (character === "[") square++;
345
+ else if (character === "]") square--;
346
+ else if (character === "{") curly++;
347
+ else if (character === "}") {
348
+ if (templateDepths.at(-1) === curly) {
349
+ templateDepths.pop();
350
+ curly--;
351
+ quote = "`";
352
+ } else curly--;
353
+ } else if (character === "," && round === 0 && square === 0 && curly === 0) {
354
+ parts.push(parameters.slice(start, index));
355
+ start = index + 1;
356
+ }
357
+ if (round < 0 || square < 0 || curly < 0) return undefined;
358
+ }
359
+
360
+ if (
361
+ quote ||
362
+ blockComment ||
363
+ templateDepths.length > 0 ||
364
+ round !== 0 ||
365
+ square !== 0 ||
366
+ curly !== 0
367
+ )
368
+ return undefined;
369
+ parts.push(parameters.slice(start));
370
+ return parts;
371
+ }
372
+
373
+ function authStartParameterList(source: string): string | undefined {
374
+ let index = 0;
375
+ while (index < source.length && /\s/.test(source[index] ?? "")) index++;
376
+ if (index >= source.length) return undefined;
377
+
378
+ let openIndex = -1;
379
+ let parenthesizedArrow = false;
380
+ let asyncMethodOrArrow = false;
381
+ const skipTrivia = () => {
382
+ while (index < source.length) {
383
+ if (/\s/.test(source[index] ?? "")) {
384
+ index++;
385
+ continue;
386
+ }
387
+ if (source[index] === "/" && source[index + 1] === "/") {
388
+ index += 2;
389
+ while (index < source.length && source[index] !== "\n" && source[index] !== "\r") index++;
390
+ continue;
391
+ }
392
+ if (source[index] === "/" && source[index + 1] === "*") {
393
+ const end = source.indexOf("*/", index + 2);
394
+ if (end < 0) {
395
+ index = source.length;
396
+ return;
260
397
  }
261
- : unknown
262
- : unknown;
398
+ index = end + 2;
399
+ continue;
400
+ }
401
+ return;
402
+ }
403
+ };
404
+
405
+ const initial = source.slice(index);
406
+ const isAsync = initial.startsWith("async") && !/[\w$]/.test(initial[5] ?? "");
407
+ if (isAsync) {
408
+ index += 5;
409
+ skipTrivia();
410
+ }
411
+
412
+ const afterAsync = source.slice(index);
413
+ const isFunction = afterAsync.startsWith("function") && !/[\w$]/.test(afterAsync[8] ?? "");
414
+ if (isFunction) {
415
+ index += 8;
416
+ skipTrivia();
417
+ if (source[index] === "*") {
418
+ index++;
419
+ skipTrivia();
420
+ }
421
+ } else if (source[index] === "*") {
422
+ index++;
423
+ skipTrivia();
424
+ }
425
+
426
+ if (source[index] === "(") {
427
+ openIndex = index;
428
+ parenthesizedArrow = !isFunction;
429
+ asyncMethodOrArrow = isAsync && !isFunction;
430
+ } else if (isFunction) {
431
+ if (!/[A-Za-z_$]/.test(source[index] ?? "")) return undefined;
432
+ index++;
433
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? "")) index++;
434
+ skipTrivia();
435
+ if (source[index] !== "(") return undefined;
436
+ openIndex = index;
437
+ } else {
438
+ const identifierStart = index;
439
+ if (!/[A-Za-z_$]/.test(source[index] ?? "")) return undefined;
440
+ index++;
441
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? "")) index++;
442
+ const firstIdentifier = source.slice(identifierStart, index);
443
+ skipTrivia();
444
+ if (source[index] === "=" && source[index + 1] === ">") return undefined;
445
+ if (source[index] !== "(") {
446
+ if (firstIdentifier !== "get" && firstIdentifier !== "set") return undefined;
447
+ if (!/[A-Za-z_$]/.test(source[index] ?? "")) return undefined;
448
+ index++;
449
+ while (index < source.length && /[A-Za-z0-9_$]/.test(source[index] ?? "")) index++;
450
+ skipTrivia();
451
+ }
452
+ if (source[index] !== "(") return undefined;
453
+ openIndex = index;
454
+ }
455
+ if (openIndex < 0) return undefined;
456
+
457
+ let depth = 1;
458
+ let square = 0;
459
+ let curly = 0;
460
+ let quote: "'" | '"' | "`" | undefined;
461
+ let escaped = false;
462
+ let lineComment = false;
463
+ let blockComment = false;
464
+ const templateDepths = [0];
465
+ templateDepths.length = 0;
466
+ for (index = openIndex + 1; index < source.length; index++) {
467
+ const character = source[index];
468
+ const nextCharacter = source[index + 1];
469
+ if (lineComment) {
470
+ if (character === "\n" || character === "\r") lineComment = false;
471
+ else continue;
472
+ }
473
+ if (blockComment) {
474
+ if (character === "*" && nextCharacter === "/") {
475
+ blockComment = false;
476
+ index++;
477
+ }
478
+ continue;
479
+ }
480
+ if (quote) {
481
+ if (escaped) escaped = false;
482
+ else if (character === "\\") escaped = true;
483
+ else if (quote === "`" && character === "$" && nextCharacter === "{") {
484
+ curly++;
485
+ templateDepths.push(curly);
486
+ quote = undefined;
487
+ index++;
488
+ } else if (character === quote) quote = undefined;
489
+ continue;
490
+ }
491
+ if (character === "'" || character === '"' || character === "`") {
492
+ quote = character;
493
+ continue;
494
+ }
495
+ if (character === "/" && nextCharacter === "/") {
496
+ lineComment = true;
497
+ index++;
498
+ continue;
499
+ }
500
+ if (character === "/" && nextCharacter === "*") {
501
+ blockComment = true;
502
+ index++;
503
+ continue;
504
+ }
505
+ if (character === "/") return undefined;
506
+ // Annex B HTML-like comments are not lexed here; give up rather than
507
+ // risk misreading the parameter list.
508
+ if (character === "<" && source.startsWith("!--", index + 1)) return undefined;
509
+ if (character === "-" && source.startsWith("->", index + 1)) return undefined;
510
+ if (character === "(") depth++;
511
+ else if (character === ")") {
512
+ depth--;
513
+ if (depth === 0 && square === 0 && curly === 0) {
514
+ const closeIndex = index;
515
+ if (parenthesizedArrow) {
516
+ index++;
517
+ skipTrivia();
518
+ const hasArrow = source[index] === "=" && source[index + 1] === ">";
519
+ if (!hasArrow && (!asyncMethodOrArrow || source[index] !== "{")) return undefined;
520
+ }
521
+ return source.slice(openIndex + 1, closeIndex);
522
+ }
523
+ if (depth < 0) return undefined;
524
+ } else if (character === "[") square++;
525
+ else if (character === "]") {
526
+ square--;
527
+ if (square < 0) return undefined;
528
+ } else if (character === "{") curly++;
529
+ else if (character === "}") {
530
+ if (templateDepths.at(-1) === curly) {
531
+ templateDepths.pop();
532
+ curly--;
533
+ quote = "`";
534
+ } else curly--;
535
+ if (curly < 0) return undefined;
536
+ }
537
+ }
538
+ return undefined;
539
+ }
540
+
541
+ /**
542
+ * Conservative defense in depth for defaulted second parameters, which
543
+ * JavaScript intentionally omits from Function.length. Ambiguous source is
544
+ * ignored so this check can never reject a valid provider on weak evidence.
545
+ */
546
+ function authStartHasHiddenInput(start: unknown): boolean {
547
+ let source: string;
548
+ try {
549
+ source = Function.prototype.toString.call(start);
550
+ } catch {
551
+ return false;
552
+ }
553
+ if (
554
+ !source ||
555
+ source.includes("[native code]") ||
556
+ /^\s*(?:async\s+)?function\s+bound\b/.test(source)
557
+ )
558
+ return false;
559
+
560
+ const parameters = authStartParameterList(source);
561
+ if (!parameters) return false;
562
+ const parts = splitAuthStartParameters(parameters);
563
+ if (!parts || parts.length < 2) return false;
564
+
565
+ // Require ordinary, readable source formatting. This intentionally fails
566
+ // open for minified output and for transpilers that rewrite defaults.
567
+ const commaIndex = parameters.indexOf(",");
568
+ if (commaIndex < 0 || !/\s/.test(parameters[commaIndex + 1] ?? "")) return false;
569
+ const first = parts[0].trim();
570
+ const second = parts[1].trim();
571
+ const identifier = /^[_$A-Za-z][_$A-Za-z0-9]*/;
572
+ const firstName = first.match(identifier)?.[0];
573
+ const secondName = second.match(identifier)?.[0];
574
+ if (!firstName || !secondName || firstName.length < 3 || secondName.length < 3) return false;
575
+ return /\s=\s/.test(second);
576
+ }
263
577
 
264
- export interface ProviderConfig<TOperations extends Record<string, ProviderOperation>> {
578
+ export interface ProviderDeclaration {
265
579
  id: string;
266
580
  version: string;
267
581
  runtime: "standard" | "shared" | "browser";
@@ -272,6 +586,8 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
272
586
  * resolves omitted fields against the runtime deployment profiles.
273
587
  */
274
588
  deployment?: ProviderDeploymentOverrides;
589
+ /** Declares that provider operations use the SDK HTTP client. */
590
+ http?: true;
275
591
  allowedHosts?: string[];
276
592
  native?: NativeProviderConfig;
277
593
  stealth?: {
@@ -284,11 +600,21 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
284
600
  resolver?: ProviderResolverConfig;
285
601
  browser?: { engine: BrowserEngine };
286
602
  auth?: AuthConfig;
603
+ /** Declares that provider operations issue and consume SDK choice tokens. */
604
+ choice?: true;
287
605
  reviewed?: ProviderReviewed;
288
606
  access?: ProviderAccessConfig;
289
607
  secrets?: ProviderSecretDeclaration[];
608
+ /** Declares that provider operations read SDK-managed environment values. */
609
+ env?: true;
290
610
  credential?: CredentialDeclaration;
291
611
  context?: ContextDeclaration;
612
+ /** Declares that provider operations use SDK-managed persistent state. */
613
+ state?: true;
614
+ /** Declares that provider operations use the SDK provider cache. */
615
+ cache?: true;
616
+ /** Declares that provider operations access runtime-resolvable files. */
617
+ files?: true;
292
618
  meta: {
293
619
  displayName: string;
294
620
  displayNameKey?: string;
@@ -310,25 +636,35 @@ export interface ProviderConfig<TOperations extends Record<string, ProviderOpera
310
636
  publicSchemaFieldNames?: "normalized";
311
637
  };
312
638
  };
313
- operations: OperationMapConfig<TOperations>;
314
639
  healthMonitor?: ProviderHealthMonitorConfig;
315
640
  /** New name for `healthMonitor` (transitional alias); declaring both is a ValidationError. */
316
641
  healthProbe?: ProviderHealthMonitorConfig;
317
642
  healthJourneys?: readonly HealthJourneyDefinition[];
318
643
  }
319
644
 
320
- /** Define one provider operation with schema-driven handler inference. */
321
- export function defineOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
322
- operation: OperationConfig<TInput, TOutput>,
323
- ): OperationDefinition<TInput, TOutput> {
324
- return operation;
645
+ interface ProviderConfig<
646
+ TOperations extends Record<string, ProviderOperation>,
647
+ TContext = ProviderContext,
648
+ > extends ProviderDeclaration {
649
+ operations: OperationMapConfig<TOperations, TContext>;
650
+ }
651
+
652
+ /** Define one factored provider operation with schema-driven handler inference. */
653
+ export function defineOperation<TContext>() {
654
+ return function operation<TInput extends SchemaLike, TOutput extends SchemaLike>(
655
+ config: OperationConfig<TInput, TOutput, TContext>,
656
+ ): OperationDefinition<TInput, TOutput, TContext> {
657
+ return config;
658
+ };
325
659
  }
326
660
 
327
- /** Define a non-JSON provider operation with explicit transport metadata. */
328
- export function defineStreamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
329
- operation: StreamOperationConfig<TInput, TOutput>,
330
- ): OperationDefinition<TInput, TOutput> {
331
- return operation;
661
+ /** Define a factored non-JSON operation with explicit transport metadata. */
662
+ export function defineStreamOperation<TContext>() {
663
+ return function streamOperation<TInput extends SchemaLike, TOutput extends SchemaLike>(
664
+ config: StreamOperationConfig<TInput, TOutput, TContext>,
665
+ ): OperationDefinition<TInput, TOutput, TContext> {
666
+ return config;
667
+ };
332
668
  }
333
669
 
334
670
  function assertObjectConfig(value: unknown): asserts value is Record<string, unknown> {
@@ -479,6 +815,14 @@ function validateProviderShape(config: unknown): void {
479
815
  assertRequiredField(config, "operations", String(config.id));
480
816
  if (typeof config.runtime === "string")
481
817
  assertLiteralField(config.runtime, "runtime", VALID_RUNTIMES, String(config.id));
818
+ if (config.native !== undefined && config.runtime === "browser") {
819
+ throw new ValidationError(
820
+ `Provider "${String(config.id)}" cannot declare capability "native" with runtime "browser"`,
821
+ {
822
+ fix: 'Use runtime: "standard" or runtime: "shared", or remove the native declaration.',
823
+ },
824
+ );
825
+ }
482
826
  const auth = config.auth;
483
827
  if (auth && typeof auth === "object" && "mode" in auth && typeof auth.mode === "string")
484
828
  assertLiteralField(auth.mode, "auth.mode", VALID_AUTH_MODES, String(config.id));
@@ -510,6 +854,24 @@ function validateProviderShape(config: unknown): void {
510
854
  },
511
855
  );
512
856
  }
857
+ if (
858
+ auth &&
859
+ typeof auth === "object" &&
860
+ "flow" in auth &&
861
+ auth.flow &&
862
+ typeof auth.flow === "object" &&
863
+ "start" in auth.flow &&
864
+ typeof auth.flow.start === "function" &&
865
+ auth.flow.start.length <= 1 &&
866
+ authStartHasHiddenInput(auth.flow.start)
867
+ ) {
868
+ throw new ProviderError(
869
+ `Provider "${String(config.id)}" auth.flow.start must not declare an input parameter`,
870
+ {
871
+ fix: "Return a form turn from start(ctx), then receive user input in continue(ctx, input).",
872
+ },
873
+ );
874
+ }
513
875
  const access = config.access;
514
876
  if (access !== undefined) {
515
877
  if (!access || typeof access !== "object" || Array.isArray(access)) {
@@ -768,12 +1130,14 @@ function validateProviderResolver(config: { id: string; resolver?: ProviderResol
768
1130
  "resolver",
769
1131
  config.id,
770
1132
  );
771
- validateResolverLiteralArray(
772
- resolver.vendors,
773
- "resolver.vendors",
774
- VALID_PROVIDER_RESOLVER_VENDORS,
775
- config.id,
776
- );
1133
+ if (resolver.vendors !== undefined) {
1134
+ validateResolverLiteralArray(
1135
+ resolver.vendors,
1136
+ "resolver.vendors",
1137
+ VALID_PROVIDER_RESOLVER_VENDORS,
1138
+ config.id,
1139
+ );
1140
+ }
777
1141
  validateResolverLiteralArray(
778
1142
  resolver.kinds,
779
1143
  "resolver.kinds",
@@ -2464,12 +2828,48 @@ function validateProviderDeployment(providerId: string, deployment: unknown): vo
2464
2828
  });
2465
2829
  }
2466
2830
 
2467
- export function defineProvider<
2831
+ /** The second authoring phase for a declaration established by defineProvider. */
2832
+ export type ProviderBuilder<TDeclaration extends ProviderDeclaration> = <
2833
+ TOperations extends Record<string, ProviderOperation>,
2834
+ >(
2835
+ implementation: {
2836
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2837
+ },
2838
+ ) => ProviderDefinition & {
2839
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2840
+ };
2841
+
2842
+ /** Extract the declaration-derived operation context from a provider builder. */
2843
+ export type ProviderContextOf<TBuilder> = TBuilder extends ProviderBuilder<infer TDeclaration>
2844
+ ? ProviderContextFor<TDeclaration>
2845
+ : never;
2846
+
2847
+ /** Establish a provider declaration before its operations are contextually typed. */
2848
+ export function defineProvider<const TDeclaration extends ProviderDeclaration>(
2849
+ declaration: TDeclaration &
2850
+ Record<Exclude<keyof TDeclaration, keyof ProviderDeclaration>, never> &
2851
+ AuthStartNoInputGuard<TDeclaration>,
2852
+ ): ProviderBuilder<TDeclaration> {
2853
+ const buildProvider = <TOperations extends Record<string, ProviderOperation>>(
2854
+ implementation: {
2855
+ operations: OperationMapConfig<TOperations, ProviderContextFor<TDeclaration>>;
2856
+ },
2857
+ ) =>
2858
+ finalizeProvider({
2859
+ ...declaration,
2860
+ ...implementation,
2861
+ } as ProviderConfig<TOperations, ProviderContextFor<TDeclaration>>);
2862
+ return buildProvider as ProviderBuilder<TDeclaration>;
2863
+ }
2864
+
2865
+ function finalizeProvider<
2468
2866
  TOperations extends Record<string, ProviderOperation>,
2469
- TConfig extends ProviderConfig<TOperations>,
2867
+ TContext,
2470
2868
  >(
2471
- config: TConfig & AuthStartNoInputGuard<TConfig>,
2472
- ): ProviderDefinition & { operations: OperationMapConfig<TOperations> } {
2869
+ config: ProviderConfig<TOperations, TContext>,
2870
+ ): ProviderDefinition & {
2871
+ operations: OperationMapConfig<TOperations, TContext>;
2872
+ } {
2473
2873
  validateProviderShape(config);
2474
2874
  const operations = resolveOperationFixtureRequests(config.operations);
2475
2875
  if (!CONNECTOR_ID_REGEX.test(config.id))
@@ -2530,7 +2930,9 @@ export function defineProvider<
2530
2930
  `Provider "${config.id}" cannot define browser config unless runtime is "browser"`,
2531
2931
  { fix: 'Set runtime: "browser" or remove the browser config' },
2532
2932
  );
2533
- const provider: ProviderDefinition & { operations: OperationMapConfig<TOperations> } = {
2933
+ const provider: ProviderDefinition & {
2934
+ operations: OperationMapConfig<TOperations, TContext>;
2935
+ } = {
2534
2936
  id: config.id,
2535
2937
  version: config.version,
2536
2938
  runtime: config.runtime,
@@ -2552,7 +2954,8 @@ export function defineProvider<
2552
2954
  credential: config.credential,
2553
2955
  context: config.context,
2554
2956
  meta: config.meta,
2555
- operations,
2957
+ operations: operations as ProviderDefinition["operations"] &
2958
+ OperationMapConfig<TOperations, TContext>,
2556
2959
  // Transitional healthMonitor → healthProbe alias: mirror whichever field
2557
2960
  // was declared onto both so old and new consumers keep working.
2558
2961
  healthMonitor: config.healthMonitor ?? config.healthProbe,