@every-app/sdk 0.3.0-rc.1 → 0.3.0-rc.10

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 (163) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1 -1
  3. package/dist/cloudflare/catalogSettings.generated.d.ts +81 -0
  4. package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
  5. package/dist/cloudflare/catalogSettings.generated.js +174 -0
  6. package/dist/cloudflare/index.d.ts +3 -2
  7. package/dist/cloudflare/index.d.ts.map +1 -1
  8. package/dist/cloudflare/index.js +4 -1
  9. package/dist/cloudflare/sentinel.d.ts +60 -2
  10. package/dist/cloudflare/sentinel.d.ts.map +1 -1
  11. package/dist/cloudflare/sentinel.js +37 -2
  12. package/dist/cloudflare/worker.d.ts +357 -63
  13. package/dist/cloudflare/worker.d.ts.map +1 -1
  14. package/dist/cloudflare/worker.js +363 -104
  15. package/dist/dev/index.d.ts +1 -1
  16. package/dist/dev/index.d.ts.map +1 -1
  17. package/dist/dev/index.js +1 -1
  18. package/dist/dev/keypair.d.ts +2 -0
  19. package/dist/dev/keypair.d.ts.map +1 -1
  20. package/dist/dev/keypair.js +2 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.d.ts.map +1 -0
  23. package/dist/index.js +5 -0
  24. package/dist/internal/aiDefault.d.ts +204 -0
  25. package/dist/internal/aiDefault.d.ts.map +1 -0
  26. package/dist/internal/aiDefault.js +334 -0
  27. package/dist/internal/aiProviders.d.ts +7 -2
  28. package/dist/internal/aiProviders.d.ts.map +1 -1
  29. package/dist/internal/aiProviders.js +17 -4
  30. package/dist/internal/appSettings.d.ts +82 -4
  31. package/dist/internal/appSettings.d.ts.map +1 -1
  32. package/dist/internal/appSettings.js +204 -29
  33. package/dist/internal/audit.d.ts +108 -0
  34. package/dist/internal/audit.d.ts.map +1 -0
  35. package/dist/internal/audit.js +235 -0
  36. package/dist/internal/capabilities.d.ts +2268 -438
  37. package/dist/internal/capabilities.d.ts.map +1 -1
  38. package/dist/internal/capabilities.js +128 -539
  39. package/dist/internal/catalog/commerce.d.ts +394 -0
  40. package/dist/internal/catalog/commerce.d.ts.map +1 -0
  41. package/dist/internal/catalog/commerce.js +477 -0
  42. package/dist/internal/catalog/comms.d.ts +375 -0
  43. package/dist/internal/catalog/comms.d.ts.map +1 -0
  44. package/dist/internal/catalog/comms.js +520 -0
  45. package/dist/internal/catalog/index.d.ts +1973 -0
  46. package/dist/internal/catalog/index.d.ts.map +1 -0
  47. package/dist/internal/catalog/index.js +19 -0
  48. package/dist/internal/catalog/inference.d.ts +393 -0
  49. package/dist/internal/catalog/inference.d.ts.map +1 -0
  50. package/dist/internal/catalog/inference.js +460 -0
  51. package/dist/internal/catalog/infra.d.ts +423 -0
  52. package/dist/internal/catalog/infra.d.ts.map +1 -0
  53. package/dist/internal/catalog/infra.js +543 -0
  54. package/dist/internal/catalog/search.d.ts +400 -0
  55. package/dist/internal/catalog/search.d.ts.map +1 -0
  56. package/dist/internal/catalog/search.js +463 -0
  57. package/dist/internal/devProviders.d.ts +2 -2
  58. package/dist/internal/devProviders.d.ts.map +1 -1
  59. package/dist/internal/devProviders.js +19 -1
  60. package/dist/internal/events.d.ts +13 -2
  61. package/dist/internal/events.d.ts.map +1 -1
  62. package/dist/internal/events.js +47 -4
  63. package/dist/internal/gatewaySettings.d.ts +3 -2
  64. package/dist/internal/gatewaySettings.d.ts.map +1 -1
  65. package/dist/internal/gatewaySettings.js +12 -6
  66. package/dist/internal/identity.d.ts +41 -4
  67. package/dist/internal/identity.d.ts.map +1 -1
  68. package/dist/internal/identity.js +48 -10
  69. package/dist/internal/index.d.ts +6 -1
  70. package/dist/internal/index.d.ts.map +1 -1
  71. package/dist/internal/index.js +6 -1
  72. package/dist/internal/members.d.ts +3 -2
  73. package/dist/internal/members.d.ts.map +1 -1
  74. package/dist/internal/oauthClient.d.ts +10 -12
  75. package/dist/internal/oauthClient.d.ts.map +1 -1
  76. package/dist/internal/oauthClient.js +20 -81
  77. package/dist/internal/oauthPresets.d.ts +33 -0
  78. package/dist/internal/oauthPresets.d.ts.map +1 -0
  79. package/dist/internal/oauthPresets.js +1064 -0
  80. package/dist/internal/oauthScopes.d.ts +56 -1
  81. package/dist/internal/oauthScopes.d.ts.map +1 -1
  82. package/dist/internal/oauthScopes.js +98 -4
  83. package/dist/internal/oauthSpec.d.ts +102 -0
  84. package/dist/internal/oauthSpec.d.ts.map +1 -0
  85. package/dist/internal/oauthSpec.js +789 -0
  86. package/dist/internal/protocol.d.ts +64 -1
  87. package/dist/internal/protocol.d.ts.map +1 -1
  88. package/dist/internal/protocol.js +51 -1
  89. package/dist/internal/providerRegistry.d.ts +77 -6
  90. package/dist/internal/providerRegistry.d.ts.map +1 -1
  91. package/dist/internal/providerRegistry.js +178 -8
  92. package/dist/internal/providerSpec.d.ts +18 -7
  93. package/dist/internal/providerSpec.d.ts.map +1 -1
  94. package/dist/internal/providerSpec.js +442 -34
  95. package/dist/internal/signInProviders.d.ts +104 -0
  96. package/dist/internal/signInProviders.d.ts.map +1 -0
  97. package/dist/internal/signInProviders.js +245 -0
  98. package/dist/manifest.d.ts +238 -0
  99. package/dist/manifest.d.ts.map +1 -0
  100. package/dist/manifest.js +30 -0
  101. package/dist/server/ai.d.ts +111 -0
  102. package/dist/server/ai.d.ts.map +1 -0
  103. package/dist/server/ai.js +145 -0
  104. package/dist/server/aiBinding.d.ts +68 -0
  105. package/dist/server/aiBinding.d.ts.map +1 -0
  106. package/dist/server/aiBinding.js +294 -0
  107. package/dist/server/attribution.d.ts +9 -5
  108. package/dist/server/attribution.d.ts.map +1 -1
  109. package/dist/server/attribution.js +12 -4
  110. package/dist/server/authorizeUrl.d.ts +1 -1
  111. package/dist/server/authorizeUrl.d.ts.map +1 -1
  112. package/dist/server/authorizeUrl.js +2 -6
  113. package/dist/server/connectUrl.d.ts +21 -0
  114. package/dist/server/connectUrl.d.ts.map +1 -0
  115. package/dist/server/connectUrl.js +37 -0
  116. package/dist/server/context.d.ts +68 -0
  117. package/dist/server/context.d.ts.map +1 -0
  118. package/dist/server/context.js +117 -0
  119. package/dist/server/emailBinding.d.ts +72 -0
  120. package/dist/server/emailBinding.d.ts.map +1 -0
  121. package/dist/server/emailBinding.js +124 -0
  122. package/dist/server/emit.d.ts +1 -1
  123. package/dist/server/emit.d.ts.map +1 -1
  124. package/dist/server/emit.js +7 -7
  125. package/dist/server/env.d.ts +44 -0
  126. package/dist/server/env.d.ts.map +1 -0
  127. package/dist/server/env.js +1 -0
  128. package/dist/server/everyApp.d.ts +27 -7
  129. package/dist/server/everyApp.d.ts.map +1 -1
  130. package/dist/server/everyApp.js +106 -5
  131. package/dist/server/fromMcpTools.d.ts +89 -0
  132. package/dist/server/fromMcpTools.d.ts.map +1 -0
  133. package/dist/server/fromMcpTools.js +192 -0
  134. package/dist/server/gatewayFetch.d.ts +42 -18
  135. package/dist/server/gatewayFetch.d.ts.map +1 -1
  136. package/dist/server/gatewayFetch.js +138 -19
  137. package/dist/server/gatewayUrl.d.ts +13 -0
  138. package/dist/server/gatewayUrl.d.ts.map +1 -0
  139. package/dist/server/gatewayUrl.js +34 -0
  140. package/dist/server/index.d.ts +15 -4
  141. package/dist/server/index.d.ts.map +1 -1
  142. package/dist/server/index.js +12 -2
  143. package/dist/server/integrationFetch.d.ts +70 -39
  144. package/dist/server/integrationFetch.d.ts.map +1 -1
  145. package/dist/server/integrationFetch.js +244 -82
  146. package/dist/server/listMembers.d.ts +1 -1
  147. package/dist/server/listMembers.d.ts.map +1 -1
  148. package/dist/server/listMembers.js +8 -11
  149. package/dist/server/refusal.d.ts +55 -0
  150. package/dist/server/refusal.d.ts.map +1 -0
  151. package/dist/server/refusal.js +193 -0
  152. package/dist/server/sendEmail.d.ts +1 -1
  153. package/dist/server/sendEmail.d.ts.map +1 -1
  154. package/dist/server/sendEmail.js +7 -7
  155. package/dist/server/toolsEndpoint.d.ts.map +1 -1
  156. package/dist/server/toolsEndpoint.js +11 -5
  157. package/dist/tanstack-start/index.d.ts +9 -0
  158. package/dist/tanstack-start/index.d.ts.map +1 -0
  159. package/dist/tanstack-start/index.js +29 -0
  160. package/dist/testing/index.d.ts +93 -0
  161. package/dist/testing/index.d.ts.map +1 -0
  162. package/dist/testing/index.js +156 -0
  163. package/package.json +35 -14
@@ -6,30 +6,41 @@
6
6
  * JSON into an {@link ApiKeyCapability} or names every problem; nothing
7
7
  * here reads a value: a spec names Gateway settings, never holds one.
8
8
  *
9
- * This slice parses API-key providers (`auth.kind` bearer, header, query
10
- * or basic). An AI provider or an OAuth client is a catalog entry only;
11
- * the parser says so rather than guessing.
9
+ * Three kinds parse: API-key providers (`auth.kind` bearer, header, query
10
+ * or basic), OAuth providers (`auth.kind` oauth2-user, the half in
11
+ * ./oauthSpec.ts: the Gateway runs the flow and holds the tokens,
12
+ * docs/design/2026-10-oauth-providers.md), and OpenAI-compatible model
13
+ * providers (`mode: "ai"`: a `baseUrl`, the key's placement, the models
14
+ * it serves and the default, its inference routes; the AI proxy serves a
15
+ * row exactly as it serves a catalog `ai` entry, inference POSTs alone,
16
+ * `store: false` on chat, the usage tap, so a new model vendor is a
17
+ * registered spec, not a catalog release).
12
18
  *
13
19
  * The rules that make a row safe on a particular gateway (its secret names
14
20
  * are its own, its hosts are public, it shadows no catalog entry) are the
15
21
  * registry's (./providerRegistry.ts), run after this parse.
16
22
  */
17
- import { type ApiKeyCapability } from "./capabilities.js";
23
+ import { type AiProviderCapability, type ApiKeyCapability, type OAuthClientCapability } from "./capabilities.js";
18
24
  /** A provider id: lowercase, a letter first, 2 to 40 characters. */
19
25
  export declare const PROVIDER_ID: RegExp;
20
26
  /** A Gateway setting name a provider may hold: upper case, 3 to 64 characters. */
21
27
  export declare const PROVIDER_SECRET_NAME: RegExp;
22
28
  /** A bare lowercase host with at least one dot: labels of letters, digits and hyphens. */
23
29
  export declare const PROVIDER_HOST: RegExp;
30
+ /** What an `ai` row serves by default: chat, and embeddings when it names an embeddings model. */
31
+ export declare const AI_SPEC_CHAT_ROUTE = "/v1/chat/completions";
32
+ export declare const AI_SPEC_EMBEDDINGS_ROUTE = "/v1/embeddings";
24
33
  export interface ProviderSpecProblem {
25
34
  /** Dotted: `auth.secret`, `hosts.1`, or `(root)`. */
26
35
  path: string;
27
36
  message: string;
28
37
  }
38
+ /** What a row or a preset may be: an API key, an OAuth provider with a `flow`, or an OpenAI-compatible model provider. */
39
+ export type RegistrableCapability = ApiKeyCapability | OAuthClientCapability | AiProviderCapability;
29
40
  export type ParsedProviderSpec = {
30
41
  ok: true;
31
42
  id: string;
32
- capability: ApiKeyCapability;
43
+ capability: RegistrableCapability;
33
44
  } | {
34
45
  ok: false;
35
46
  problems: ProviderSpecProblem[];
@@ -49,7 +60,7 @@ export declare function describeSpecProblems(problems: readonly ProviderSpecProb
49
60
  * defaults the parser fills in spelled out, so `parseProviderSpec` of it
50
61
  * gives the entry back.
51
62
  */
52
- export declare function providerSpecOf(id: string, capability: ApiKeyCapability): Record<string, unknown>;
63
+ export declare function providerSpecOf(id: string, capability: RegistrableCapability): Record<string, unknown>;
53
64
  /** The methods an app with `access: "write"` may use on `capability` besides its read methods. */
54
- export declare function writeMethodsOf(capability: ApiKeyCapability): readonly string[];
65
+ export declare function writeMethodsOf(capability: RegistrableCapability): readonly string[];
55
66
  //# sourceMappingURL=providerSpec.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"providerSpec.d.ts","sourceRoot":"","sources":["../../src/internal/providerSpec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AACH,OAAO,EAIL,KAAK,gBAAgB,EAMtB,MAAM,mBAAmB,CAAC;AAE3B,oEAAoE;AACpE,eAAO,MAAM,WAAW,QAA2B,CAAC;AACpD,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,QAA2B,CAAC;AAC7D,0FAA0F;AAC1F,eAAO,MAAM,aAAa,QACoD,CAAC;AA0D/E,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,gBAAgB,CAAA;CAAE,GACtD;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC;AA+fnD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CAqKpE;AAED,wFAAwF;AACxF,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,GACvC,MAAM,CAIR;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,gBAAgB,GAC3B,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAwCzB;AAED,kGAAkG;AAClG,wBAAgB,cAAc,CAC5B,UAAU,EAAE,gBAAgB,GAC3B,SAAS,MAAM,EAAE,CAEnB"}
1
+ {"version":3,"file":"providerSpec.d.ts","sourceRoot":"","sources":["../../src/internal/providerSpec.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,OAAO,EAGL,KAAK,oBAAoB,EAEzB,KAAK,gBAAgB,EACrB,KAAK,qBAAqB,EAM3B,MAAM,mBAAmB,CAAC;AAG3B,oEAAoE;AACpE,eAAO,MAAM,WAAW,QAA2B,CAAC;AACpD,kFAAkF;AAClF,eAAO,MAAM,oBAAoB,QAA2B,CAAC;AAC7D,0FAA0F;AAC1F,eAAO,MAAM,aAAa,QACoD,CAAC;AA4C/E,kGAAkG;AAClG,eAAO,MAAM,kBAAkB,yBAAyB,CAAC;AACzD,eAAO,MAAM,wBAAwB,mBAAmB,CAAC;AA+EzD,MAAM,WAAW,mBAAmB;IAClC,qDAAqD;IACrD,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,0HAA0H;AAC1H,MAAM,MAAM,qBAAqB,GAC7B,gBAAgB,GAChB,qBAAqB,GACrB,oBAAoB,CAAC;AAEzB,MAAM,MAAM,kBAAkB,GAC1B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,EAAE,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,qBAAqB,CAAA;CAAE,GAC3D;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,QAAQ,EAAE,mBAAmB,EAAE,CAAA;CAAE,CAAC;AA+iBnD;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,OAAO,GAAG,kBAAkB,CA2NpE;AAkOD,wFAAwF;AACxF,wBAAgB,oBAAoB,CAClC,QAAQ,EAAE,SAAS,mBAAmB,EAAE,GACvC,MAAM,CAIR;AAED;;;;;GAKG;AACH,wBAAgB,cAAc,CAC5B,EAAE,EAAE,MAAM,EACV,UAAU,EAAE,qBAAqB,GAChC,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAuGzB;AAED,kGAAkG;AAClG,wBAAgB,cAAc,CAC5B,UAAU,EAAE,qBAAqB,GAChC,SAAS,MAAM,EAAE,CAKnB"}
@@ -6,15 +6,22 @@
6
6
  * JSON into an {@link ApiKeyCapability} or names every problem; nothing
7
7
  * here reads a value: a spec names Gateway settings, never holds one.
8
8
  *
9
- * This slice parses API-key providers (`auth.kind` bearer, header, query
10
- * or basic). An AI provider or an OAuth client is a catalog entry only;
11
- * the parser says so rather than guessing.
9
+ * Three kinds parse: API-key providers (`auth.kind` bearer, header, query
10
+ * or basic), OAuth providers (`auth.kind` oauth2-user, the half in
11
+ * ./oauthSpec.ts: the Gateway runs the flow and holds the tokens,
12
+ * docs/design/2026-10-oauth-providers.md), and OpenAI-compatible model
13
+ * providers (`mode: "ai"`: a `baseUrl`, the key's placement, the models
14
+ * it serves and the default, its inference routes; the AI proxy serves a
15
+ * row exactly as it serves a catalog `ai` entry, inference POSTs alone,
16
+ * `store: false` on chat, the usage tap, so a new model vendor is a
17
+ * registered spec, not a catalog release).
12
18
  *
13
19
  * The rules that make a row safe on a particular gateway (its secret names
14
20
  * are its own, its hosts are public, it shadows no catalog entry) are the
15
21
  * registry's (./providerRegistry.ts), run after this parse.
16
22
  */
17
23
  import { DEFAULT_WRITE_METHODS, authSettingNames, } from "./capabilities.js";
24
+ import { oauthSpecOf, parseOAuthUserSpec } from "./oauthSpec.js";
18
25
  /** A provider id: lowercase, a letter first, 2 to 40 characters. */
19
26
  export const PROVIDER_ID = /^[a-z][a-z0-9-]{1,39}$/;
20
27
  /** A Gateway setting name a provider may hold: upper case, 3 to 64 characters. */
@@ -26,7 +33,12 @@ const SEGMENT = /^[a-z0-9_-]{1,64}$/;
26
33
  const HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
27
34
  const QUERY_PARAM = /^[A-Za-z0-9_.-]{1,64}$/;
28
35
  const SCHEME = /^[A-Za-z][A-Za-z0-9-]{0,31}$/;
29
- const RESPONSE_FIELD = /^[A-Za-z_][A-Za-z0-9_]{0,63}$/;
36
+ /**
37
+ * A JSON field name, or a dotted path to a nested one (`usage.cost`, at
38
+ * most four segments): the capability proxy reads a top-level name off the
39
+ * answer's head, the AI proxy a path into the usage object off its tail.
40
+ */
41
+ const RESPONSE_FIELD = /^[A-Za-z_][A-Za-z0-9_]{0,63}(?:\.[A-Za-z_][A-Za-z0-9_]{0,63}){0,3}$/;
30
42
  const DATE = /^\d{4}-\d{2}-\d{2}$/;
31
43
  const TAG = /^[a-z][a-z0-9-]{0,31}$/;
32
44
  /** Control and format characters: in no text a spec carries. */
@@ -52,6 +64,15 @@ const RESERVED_HEADERS = new Set([
52
64
  ]);
53
65
  const MAX_HOSTS = 20;
54
66
  const MAX_ACCOUNT_WIDE_PATHS = 50;
67
+ /** An inference route an `ai` row names: `/v1/chat/completions`, segments of lowercase letters, digits, `_` and `-`. */
68
+ const AI_ROUTE = /^(?:\/[a-z0-9_-]+)+$/;
69
+ const MAX_AI_ROUTES = 20;
70
+ const MAX_MODELS = 100;
71
+ /** A model name as OpenAI-compatible APIs spell them: `gpt-6-luna`, `meta-llama/llama-4`, `claude-haiku-5-5:beta`. */
72
+ const MODEL_NAME = /^[A-Za-z0-9][A-Za-z0-9._:/@-]{0,199}$/;
73
+ /** What an `ai` row serves by default: chat, and embeddings when it names an embeddings model. */
74
+ export const AI_SPEC_CHAT_ROUTE = "/v1/chat/completions";
75
+ export const AI_SPEC_EMBEDDINGS_ROUTE = "/v1/embeddings";
55
76
  const KNOWN_FIELDS = new Set([
56
77
  "id",
57
78
  "mode",
@@ -75,7 +96,59 @@ const KNOWN_FIELDS = new Set([
75
96
  "example",
76
97
  "verified",
77
98
  "tags",
99
+ // An `oauth2-user` spec's own (./oauthSpec.ts).
100
+ "scopeSets",
101
+ "alwaysRequested",
102
+ "tolerated",
103
+ // An `ai` spec's own.
104
+ "baseUrl",
105
+ "models",
106
+ "defaultModel",
107
+ "embeddingsModel",
108
+ "routes",
109
+ "chatRoute",
110
+ "embeddingsRoute",
78
111
  ]);
112
+ /** Fields only an `ai` spec has. */
113
+ const AI_ONLY_FIELDS = [
114
+ "baseUrl",
115
+ "models",
116
+ "defaultModel",
117
+ "embeddingsModel",
118
+ "routes",
119
+ "chatRoute",
120
+ "embeddingsRoute",
121
+ ];
122
+ /** Fields an `ai` spec never has: the key goes to one base URL, and the AI proxy's rules replace the capability proxy's. */
123
+ const NOT_AI_FIELDS = [
124
+ "hosts",
125
+ "hostSetting",
126
+ "data",
127
+ "access",
128
+ "readMethods",
129
+ "writeMethods",
130
+ "accountWidePaths",
131
+ ...OAUTH_ONLY_FIELDS_LIST(),
132
+ ];
133
+ function OAUTH_ONLY_FIELDS_LIST() {
134
+ return ["scopeSets", "alwaysRequested", "tolerated"];
135
+ }
136
+ /** Fields an API-key spec has that an `oauth2-user` spec does not. */
137
+ const API_KEY_ONLY_FIELDS = [
138
+ "hostSetting",
139
+ "data",
140
+ "secrets",
141
+ "accountWidePaths",
142
+ "smoke",
143
+ "cost",
144
+ "rate",
145
+ "verified",
146
+ ];
147
+ const OAUTH_ONLY_FIELDS = [
148
+ "scopeSets",
149
+ "alwaysRequested",
150
+ "tolerated",
151
+ ];
79
152
  function isRecord(value) {
80
153
  return typeof value === "object" && value !== null && !Array.isArray(value);
81
154
  }
@@ -300,7 +373,7 @@ function auth(value, problems) {
300
373
  default:
301
374
  problems.push({
302
375
  path: "auth.kind",
303
- message: "must be bearer, header, query or basic (an OAuth client or an AI provider is a catalog entry, not a spec this gateway takes)",
376
+ message: "must be bearer, header, query, basic or oauth2-user (an AI provider is a catalog entry, not a spec this gateway takes)",
304
377
  });
305
378
  return undefined;
306
379
  }
@@ -475,11 +548,11 @@ function cost(value, problems) {
475
548
  }
476
549
  const usd = nonNegative(value["usd"], "cost.usd", problems);
477
550
  const note = text(value["note"], "cost.note", problems, { max: 200 });
478
- const responseField = text(value["responseField"], "cost.responseField", problems, { max: 64 });
551
+ const responseField = text(value["responseField"], "cost.responseField", problems, { max: 128 });
479
552
  if (responseField !== undefined && !RESPONSE_FIELD.test(responseField)) {
480
553
  problems.push({
481
554
  path: "cost.responseField",
482
- message: "must be a JSON field name",
555
+ message: "must be a JSON field name, or a dotted path to a nested one",
483
556
  });
484
557
  }
485
558
  if (unit !== "call" && unit !== "result" && unit !== "token")
@@ -532,6 +605,43 @@ function verified(value, problems) {
532
605
  });
533
606
  return undefined;
534
607
  }
608
+ /** `access`, `readMethods` and `writeMethods`, shared by both kinds. */
609
+ function grants(input, problems) {
610
+ const access = stringList(input["access"], "access", problems, {
611
+ max: 2,
612
+ each: (level, at) => {
613
+ if (level !== "read" && level !== "write") {
614
+ problems.push({ path: at, message: "must be read or write" });
615
+ }
616
+ },
617
+ });
618
+ if (access && access.length === 0) {
619
+ problems.push({ path: "access", message: "must list read, write or both" });
620
+ }
621
+ const readMethods = methods(input["readMethods"], "readMethods", problems);
622
+ // A write-only provider (a sender, a GraphQL API) reads with nothing:
623
+ // every call needs the write grant. Anything that offers read lists how.
624
+ if (readMethods &&
625
+ readMethods.length === 0 &&
626
+ (!Array.isArray(access) || access.includes("read"))) {
627
+ problems.push({
628
+ path: "readMethods",
629
+ message: "must list a method unless access is write alone",
630
+ });
631
+ }
632
+ const writeMethods = methods(input["writeMethods"], "writeMethods", problems);
633
+ return { access, readMethods, writeMethods };
634
+ }
635
+ function tagList(value, problems) {
636
+ return stringList(value, "tags", problems, {
637
+ max: 10,
638
+ each: (tag, at) => {
639
+ if (!TAG.test(tag)) {
640
+ problems.push({ path: at, message: "must be a lowercase word" });
641
+ }
642
+ },
643
+ });
644
+ }
535
645
  /**
536
646
  * Parse a provider spec. Every problem is reported, with its path, so a
537
647
  * person fixes the file once. On success the id and the catalog-shaped
@@ -558,10 +668,21 @@ export function parseProviderSpec(input) {
558
668
  message: "must be lowercase letters, digits and hyphens, a letter first, 2 to 40 characters (stripe)",
559
669
  });
560
670
  }
561
- if (input["mode"] !== undefined && input["mode"] !== "api_key") {
671
+ const isOAuth = isRecord(input["auth"]) && input["auth"]["kind"] === "oauth2-user";
672
+ const isAi = input["mode"] === "ai";
673
+ const expectedMode = isOAuth ? "oauth_client" : "api_key";
674
+ if (!isAi && input["mode"] !== undefined && input["mode"] !== expectedMode) {
562
675
  problems.push({
563
676
  path: "mode",
564
- message: "this gateway registers api_key providers; an AI provider or an OAuth client is a catalog entry",
677
+ message: isOAuth
678
+ ? "an oauth2-user spec's mode is oauth_client (or left out)"
679
+ : 'this gateway registers api_key, oauth_client (auth.kind oauth2-user) and ai (mode: "ai", an OpenAI-compatible model provider) providers',
680
+ });
681
+ }
682
+ if (isAi && isOAuth) {
683
+ problems.push({
684
+ path: "auth.kind",
685
+ message: "an ai spec's key is a bearer, header, query or basic key, never oauth2-user",
565
686
  });
566
687
  }
567
688
  const name = text(input["name"], "name", problems, { max: 64 });
@@ -590,6 +711,64 @@ export function parseProviderSpec(input) {
590
711
  }
591
712
  },
592
713
  });
714
+ if (isAi && !isOAuth) {
715
+ return parseAiSpec(input, problems, { id, name, description, docs, api });
716
+ }
717
+ if (isOAuth) {
718
+ const { access, readMethods, writeMethods } = grants(input, problems);
719
+ const hint = text(input["hint"], "hint", problems, { max: 500 });
720
+ const example = text(input["example"], "example", problems, { max: 200 });
721
+ const tags = tagList(input["tags"], problems);
722
+ // An OAuth provider: its token goes to `hosts` alone, so the API-key
723
+ // fields that shape a key's placement have no meaning here.
724
+ for (const field of API_KEY_ONLY_FIELDS) {
725
+ if (input[field] !== undefined) {
726
+ problems.push({
727
+ path: field,
728
+ message: "not a field of an oauth2-user spec",
729
+ });
730
+ }
731
+ }
732
+ if (input["hosts"] === undefined || (hosts && hosts.length === 0)) {
733
+ problems.push({
734
+ path: "hosts",
735
+ message: "required: at least one API host the token is sent to",
736
+ });
737
+ }
738
+ const parsed = typeof id === "string" &&
739
+ name !== undefined &&
740
+ docs !== undefined &&
741
+ hosts !== undefined
742
+ ? parseOAuthUserSpec(input, {
743
+ id,
744
+ name,
745
+ docs,
746
+ hosts,
747
+ ...(description === undefined ? {} : { description }),
748
+ ...(api === undefined ? {} : { reference: api }),
749
+ ...(access === undefined
750
+ ? {}
751
+ : { access: access }),
752
+ ...(readMethods === undefined ? {} : { readMethods }),
753
+ ...(writeMethods === undefined ? {} : { writeMethods }),
754
+ ...(hint === undefined ? {} : { hint }),
755
+ ...(example === undefined ? {} : { example }),
756
+ ...(tags === undefined ? {} : { tags }),
757
+ }, problems)
758
+ : undefined;
759
+ if (problems.length > 0 || !parsed || typeof id !== "string") {
760
+ return { ok: false, problems };
761
+ }
762
+ return { ok: true, id, capability: parsed.capability };
763
+ }
764
+ for (const field of OAUTH_ONLY_FIELDS) {
765
+ if (input[field] !== undefined) {
766
+ problems.push({
767
+ path: field,
768
+ message: "only an oauth2-user spec has it",
769
+ });
770
+ }
771
+ }
593
772
  const hostSetting = input["hostSetting"] === undefined
594
773
  ? undefined
595
774
  : secretName(input["hostSetting"], "hostSetting", problems);
@@ -623,22 +802,7 @@ export function parseProviderSpec(input) {
623
802
  ...(parsedAuth ? authSettingNames(parsedAuth) : []),
624
803
  ...(hostSetting === undefined ? [] : [hostSetting]),
625
804
  ], problems);
626
- const access = stringList(input["access"], "access", problems, {
627
- max: 2,
628
- each: (level, at) => {
629
- if (level !== "read" && level !== "write") {
630
- problems.push({ path: at, message: "must be read or write" });
631
- }
632
- },
633
- });
634
- if (access && access.length === 0) {
635
- problems.push({ path: "access", message: "must list read, write or both" });
636
- }
637
- const readMethods = methods(input["readMethods"], "readMethods", problems);
638
- if (readMethods && readMethods.length === 0) {
639
- problems.push({ path: "readMethods", message: "must list a method" });
640
- }
641
- const writeMethods = methods(input["writeMethods"], "writeMethods", problems);
805
+ const { access, readMethods, writeMethods } = grants(input, problems);
642
806
  const paths = accountWidePaths(input["accountWidePaths"], problems);
643
807
  const hint = text(input["hint"], "hint", problems, { max: 500 });
644
808
  const smokeRequest = smoke(input["smoke"], hosts ?? [], problems);
@@ -646,14 +810,7 @@ export function parseProviderSpec(input) {
646
810
  const parsedRate = rate(input["rate"], problems);
647
811
  const example = text(input["example"], "example", problems, { max: 200 });
648
812
  const parsedVerified = verified(input["verified"], problems);
649
- const tags = stringList(input["tags"], "tags", problems, {
650
- max: 10,
651
- each: (tag, at) => {
652
- if (!TAG.test(tag)) {
653
- problems.push({ path: at, message: "must be a lowercase word" });
654
- }
655
- },
656
- });
813
+ const tags = tagList(input["tags"], problems);
657
814
  if (problems.length > 0 ||
658
815
  typeof id !== "string" ||
659
816
  name === undefined ||
@@ -687,6 +844,197 @@ export function parseProviderSpec(input) {
687
844
  };
688
845
  return { ok: true, id, capability };
689
846
  }
847
+ /**
848
+ * The `ai` half of `parseProviderSpec`: an OpenAI-compatible model
849
+ * provider. `baseUrl` is where the app's path is joined (`https://api.groq.com/openai`:
850
+ * the origin, and a base path the proxy keeps in front of every route);
851
+ * `auth` places the key as an API-key spec does; `models` lists what the
852
+ * provider serves (1 to 100; the proxy refuses any other) with
853
+ * `defaultModel` the one the Gateway's default route fills in and
854
+ * `embeddingsModel` the one for `/api/ai/default/v1/embeddings`; `routes`
855
+ * are the inference paths apps may POST to (default chat, and embeddings
856
+ * with an embeddings model), `chatRoute` and `embeddingsRoute` which of
857
+ * them the default route maps onto. Chat answers are never stored
858
+ * (`store: false` on the chat route). `cost.responseField` is where the
859
+ * answer carries the call's price, read by the usage tap as OpenRouter's
860
+ * `usage.cost` is.
861
+ */
862
+ function parseAiSpec(input, problems, common) {
863
+ for (const field of NOT_AI_FIELDS) {
864
+ if (input[field] !== undefined) {
865
+ problems.push({ path: field, message: "not a field of an ai spec" });
866
+ }
867
+ }
868
+ const base = httpsUrl(input["baseUrl"], "baseUrl", problems);
869
+ let origin;
870
+ let basePath;
871
+ if (input["baseUrl"] === undefined) {
872
+ problems.push({
873
+ path: "baseUrl",
874
+ message: "required: the https URL the app's paths are joined onto (https://api.groq.com/openai)",
875
+ });
876
+ }
877
+ else if (base !== undefined) {
878
+ const url = new URL(base);
879
+ if (url.search || url.hash || url.username || url.password || url.port) {
880
+ problems.push({
881
+ path: "baseUrl",
882
+ message: "must be an https URL with no query, fragment, credentials or port",
883
+ });
884
+ }
885
+ else if (!PROVIDER_HOST.test(url.hostname)) {
886
+ problems.push({
887
+ path: "baseUrl",
888
+ message: "must name a public host with a dot (api.groq.com)",
889
+ });
890
+ }
891
+ else {
892
+ origin = url.origin;
893
+ const path = url.pathname.replace(/\/+$/, "");
894
+ if (path && !AI_ROUTE.test(path)) {
895
+ problems.push({
896
+ path: "baseUrl",
897
+ message: "its path must be segments of lowercase letters, digits, _ and -",
898
+ });
899
+ }
900
+ else if (path) {
901
+ basePath = path;
902
+ }
903
+ }
904
+ }
905
+ const parsedAuth = auth(input["auth"], problems);
906
+ if (input["auth"] === undefined) {
907
+ problems.push({ path: "auth", message: "required" });
908
+ }
909
+ const secrets = secretDetails(input["secrets"], parsedAuth ? authSettingNames(parsedAuth) : [], problems);
910
+ const models = stringList(input["models"], "models", problems, {
911
+ max: MAX_MODELS,
912
+ each: (model, at) => {
913
+ if (!MODEL_NAME.test(model)) {
914
+ problems.push({
915
+ path: at,
916
+ message: "must be a model name (gpt-6-luna, meta-llama/llama-4)",
917
+ });
918
+ }
919
+ },
920
+ });
921
+ if (input["models"] === undefined || (models && models.length === 0)) {
922
+ problems.push({
923
+ path: "models",
924
+ message: "required: the models the provider serves, at least one",
925
+ });
926
+ }
927
+ const defaultModel = text(input["defaultModel"], "defaultModel", problems, {
928
+ max: 200,
929
+ });
930
+ if (input["defaultModel"] === undefined) {
931
+ problems.push({
932
+ path: "defaultModel",
933
+ message: "required: the chat model the Gateway fills in when an app names none; one of models",
934
+ });
935
+ }
936
+ else if (defaultModel !== undefined &&
937
+ models &&
938
+ !models.includes(defaultModel)) {
939
+ problems.push({ path: "defaultModel", message: "must be one of models" });
940
+ }
941
+ const embeddingsModel = text(input["embeddingsModel"], "embeddingsModel", problems, { max: 200 });
942
+ if (embeddingsModel !== undefined &&
943
+ models &&
944
+ !models.includes(embeddingsModel)) {
945
+ problems.push({
946
+ path: "embeddingsModel",
947
+ message: "must be one of models",
948
+ });
949
+ }
950
+ const routeList = (value, path) => stringList(value, path, problems, {
951
+ max: MAX_AI_ROUTES,
952
+ each: (route, at) => {
953
+ if (!AI_ROUTE.test(route)) {
954
+ problems.push({
955
+ path: at,
956
+ message: "must be a path of lowercase segments starting with / (/v1/chat/completions)",
957
+ });
958
+ }
959
+ },
960
+ });
961
+ const chatRoute = text(input["chatRoute"], "chatRoute", problems, { max: 200 }) ??
962
+ AI_SPEC_CHAT_ROUTE;
963
+ const embeddingsRoute = text(input["embeddingsRoute"], "embeddingsRoute", problems, { max: 200 }) ??
964
+ AI_SPEC_EMBEDDINGS_ROUTE;
965
+ for (const [field, route] of [
966
+ ["chatRoute", chatRoute],
967
+ ["embeddingsRoute", embeddingsRoute],
968
+ ]) {
969
+ if (!AI_ROUTE.test(route)) {
970
+ problems.push({
971
+ path: field,
972
+ message: "must be a path of lowercase segments starting with /",
973
+ });
974
+ }
975
+ }
976
+ const routes = routeList(input["routes"], "routes") ?? [
977
+ chatRoute,
978
+ ...(embeddingsModel !== undefined ? [embeddingsRoute] : []),
979
+ ];
980
+ if (routes.length === 0) {
981
+ problems.push({ path: "routes", message: "must list a route" });
982
+ }
983
+ if (!routes.includes(chatRoute)) {
984
+ problems.push({ path: "chatRoute", message: "must be one of routes" });
985
+ }
986
+ if (embeddingsModel !== undefined && !routes.includes(embeddingsRoute)) {
987
+ problems.push({
988
+ path: "embeddingsRoute",
989
+ message: "must be one of routes",
990
+ });
991
+ }
992
+ const hint = text(input["hint"], "hint", problems, { max: 500 });
993
+ const smokeRequest = smoke(input["smoke"], origin ? [new URL(origin).host] : [], problems);
994
+ const parsedCost = cost(input["cost"], problems);
995
+ const parsedRate = rate(input["rate"], problems);
996
+ const example = text(input["example"], "example", problems, { max: 200 });
997
+ const parsedVerified = verified(input["verified"], problems);
998
+ const tags = tagList(input["tags"], problems);
999
+ const { id, name, description, docs, api } = common;
1000
+ if (problems.length > 0 ||
1001
+ typeof id !== "string" ||
1002
+ name === undefined ||
1003
+ docs === undefined ||
1004
+ origin === undefined ||
1005
+ !parsedAuth ||
1006
+ !models ||
1007
+ defaultModel === undefined) {
1008
+ return { ok: false, problems };
1009
+ }
1010
+ const capability = {
1011
+ mode: "ai",
1012
+ label: name,
1013
+ auth: parsedAuth,
1014
+ docs,
1015
+ origin,
1016
+ ...(basePath === undefined ? {} : { basePath }),
1017
+ inferenceRoutes: routes,
1018
+ unstoredRoutes: [chatRoute],
1019
+ shape: "openai",
1020
+ chat: { path: chatRoute, seedModel: defaultModel },
1021
+ ...(embeddingsModel === undefined
1022
+ ? {}
1023
+ : { embeddings: { path: embeddingsRoute, seedModel: embeddingsModel } }),
1024
+ models,
1025
+ ...(description === undefined ? {} : { description }),
1026
+ ...(api === undefined ? {} : { api }),
1027
+ ...(secrets === undefined ? {} : { secrets }),
1028
+ ...(hint === undefined ? {} : { hint }),
1029
+ ...(smokeRequest === undefined ? {} : { smoke: smokeRequest }),
1030
+ ...(parsedCost === undefined ? {} : { cost: parsedCost }),
1031
+ ...(parsedRate === undefined ? {} : { rate: parsedRate }),
1032
+ ...(example === undefined ? {} : { example }),
1033
+ ...(parsedVerified === undefined ? {} : { verified: parsedVerified }),
1034
+ ...(tags === undefined ? {} : { tags }),
1035
+ };
1036
+ return { ok: true, id, capability };
1037
+ }
690
1038
  /** The problems as one line: `auth.secret: must name a Gateway setting; hosts.0: …`. */
691
1039
  export function describeSpecProblems(problems) {
692
1040
  return problems
@@ -700,6 +1048,65 @@ export function describeSpecProblems(problems) {
700
1048
  * gives the entry back.
701
1049
  */
702
1050
  export function providerSpecOf(id, capability) {
1051
+ if (capability.mode === "oauth_client") {
1052
+ const spec = oauthSpecOf(id, capability);
1053
+ if (!spec) {
1054
+ throw new Error(`${id} is a catalog OAuth client without a Gateway-owned flow: it has no spec`);
1055
+ }
1056
+ return spec;
1057
+ }
1058
+ if (capability.mode === "ai") {
1059
+ if (!capability.chat || !capability.models) {
1060
+ throw new Error(`${id} is a catalog AI entry without a model list: it has no spec`);
1061
+ }
1062
+ return {
1063
+ id,
1064
+ mode: "ai",
1065
+ name: capability.label,
1066
+ ...(capability.description === undefined
1067
+ ? {}
1068
+ : { description: capability.description }),
1069
+ docs: capability.docs,
1070
+ ...(capability.api === undefined ? {} : { api: capability.api }),
1071
+ baseUrl: `${capability.origin}${capability.basePath ?? ""}`,
1072
+ auth: { ...capability.auth },
1073
+ ...(capability.secrets === undefined
1074
+ ? {}
1075
+ : { secrets: capability.secrets.map((detail) => ({ ...detail })) }),
1076
+ models: [...capability.models],
1077
+ defaultModel: capability.chat.seedModel,
1078
+ ...(capability.embeddings === undefined
1079
+ ? {}
1080
+ : { embeddingsModel: capability.embeddings.seedModel }),
1081
+ routes: [...capability.inferenceRoutes],
1082
+ chatRoute: capability.chat.path,
1083
+ ...(capability.embeddings === undefined
1084
+ ? {}
1085
+ : { embeddingsRoute: capability.embeddings.path }),
1086
+ ...(capability.hint === undefined ? {} : { hint: capability.hint }),
1087
+ ...(capability.smoke === undefined
1088
+ ? {}
1089
+ : {
1090
+ smoke: {
1091
+ ...capability.smoke,
1092
+ expect: [...capability.smoke.expect],
1093
+ },
1094
+ }),
1095
+ ...(capability.cost === undefined
1096
+ ? {}
1097
+ : { cost: { ...capability.cost } }),
1098
+ ...(capability.rate === undefined
1099
+ ? {}
1100
+ : { rate: { ...capability.rate } }),
1101
+ ...(capability.example === undefined
1102
+ ? {}
1103
+ : { example: capability.example }),
1104
+ ...(capability.verified === undefined
1105
+ ? {}
1106
+ : { verified: capability.verified }),
1107
+ ...(capability.tags === undefined ? {} : { tags: [...capability.tags] }),
1108
+ };
1109
+ }
703
1110
  return {
704
1111
  id,
705
1112
  name: capability.label,
@@ -742,5 +1149,6 @@ export function providerSpecOf(id, capability) {
742
1149
  }
743
1150
  /** The methods an app with `access: "write"` may use on `capability` besides its read methods. */
744
1151
  export function writeMethodsOf(capability) {
745
- return capability.writeMethods ?? DEFAULT_WRITE_METHODS;
1152
+ return ((capability.mode === "ai" ? undefined : capability.writeMethods) ??
1153
+ DEFAULT_WRITE_METHODS);
746
1154
  }