@every-app/sdk 0.2.0 → 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.
- package/LICENSE +21 -0
- package/README.md +609 -3
- package/dist/cloudflare/catalogSettings.generated.d.ts +81 -0
- package/dist/cloudflare/catalogSettings.generated.d.ts.map +1 -0
- package/dist/cloudflare/catalogSettings.generated.js +174 -0
- package/dist/cloudflare/index.d.ts +23 -0
- package/dist/cloudflare/index.d.ts.map +1 -0
- package/dist/cloudflare/index.js +24 -0
- package/dist/cloudflare/sentinel.d.ts +127 -0
- package/dist/cloudflare/sentinel.d.ts.map +1 -0
- package/dist/cloudflare/sentinel.js +85 -0
- package/dist/cloudflare/worker.d.ts +805 -0
- package/dist/cloudflare/worker.d.ts.map +1 -0
- package/dist/cloudflare/worker.js +997 -0
- package/dist/dev/index.d.ts +8 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +7 -0
- package/dist/dev/keypair.d.ts +26 -0
- package/dist/dev/keypair.d.ts.map +1 -0
- package/dist/dev/keypair.js +62 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/internal/aiDefault.d.ts +204 -0
- package/dist/internal/aiDefault.d.ts.map +1 -0
- package/dist/internal/aiDefault.js +334 -0
- package/dist/internal/aiProviders.d.ts +72 -0
- package/dist/internal/aiProviders.d.ts.map +1 -0
- package/dist/internal/aiProviders.js +233 -0
- package/dist/internal/appSettings.d.ts +315 -0
- package/dist/internal/appSettings.d.ts.map +1 -0
- package/dist/internal/appSettings.js +549 -0
- package/dist/internal/audit.d.ts +108 -0
- package/dist/internal/audit.d.ts.map +1 -0
- package/dist/internal/audit.js +235 -0
- package/dist/internal/capabilities.d.ts +3174 -0
- package/dist/internal/capabilities.d.ts.map +1 -0
- package/dist/internal/capabilities.js +1110 -0
- package/dist/internal/catalog/commerce.d.ts +394 -0
- package/dist/internal/catalog/commerce.d.ts.map +1 -0
- package/dist/internal/catalog/commerce.js +477 -0
- package/dist/internal/catalog/comms.d.ts +375 -0
- package/dist/internal/catalog/comms.d.ts.map +1 -0
- package/dist/internal/catalog/comms.js +520 -0
- package/dist/internal/catalog/index.d.ts +1973 -0
- package/dist/internal/catalog/index.d.ts.map +1 -0
- package/dist/internal/catalog/index.js +19 -0
- package/dist/internal/catalog/inference.d.ts +393 -0
- package/dist/internal/catalog/inference.d.ts.map +1 -0
- package/dist/internal/catalog/inference.js +460 -0
- package/dist/internal/catalog/infra.d.ts +423 -0
- package/dist/internal/catalog/infra.d.ts.map +1 -0
- package/dist/internal/catalog/infra.js +543 -0
- package/dist/internal/catalog/search.d.ts +400 -0
- package/dist/internal/catalog/search.d.ts.map +1 -0
- package/dist/internal/catalog/search.js +463 -0
- package/dist/internal/devProviders.d.ts +49 -0
- package/dist/internal/devProviders.d.ts.map +1 -0
- package/dist/internal/devProviders.js +80 -0
- package/dist/internal/email.d.ts +280 -0
- package/dist/internal/email.d.ts.map +1 -0
- package/dist/internal/email.js +613 -0
- package/dist/internal/events.d.ts +193 -0
- package/dist/internal/events.d.ts.map +1 -0
- package/dist/internal/events.js +367 -0
- package/dist/internal/gatewaySettings.d.ts +83 -0
- package/dist/internal/gatewaySettings.d.ts.map +1 -0
- package/dist/internal/gatewaySettings.js +215 -0
- package/dist/internal/identity.d.ts +57 -5
- package/dist/internal/identity.d.ts.map +1 -1
- package/dist/internal/identity.js +99 -22
- package/dist/internal/index.d.ts +22 -1
- package/dist/internal/index.d.ts.map +1 -1
- package/dist/internal/index.js +22 -1
- package/dist/internal/members.d.ts +36 -0
- package/dist/internal/members.d.ts.map +1 -0
- package/dist/internal/members.js +7 -0
- package/dist/internal/oauthClient.d.ts +61 -0
- package/dist/internal/oauthClient.d.ts.map +1 -0
- package/dist/internal/oauthClient.js +133 -0
- package/dist/internal/oauthPresets.d.ts +33 -0
- package/dist/internal/oauthPresets.d.ts.map +1 -0
- package/dist/internal/oauthPresets.js +1064 -0
- package/dist/internal/oauthScopes.d.ts +172 -0
- package/dist/internal/oauthScopes.d.ts.map +1 -0
- package/dist/internal/oauthScopes.js +510 -0
- package/dist/internal/oauthSpec.d.ts +102 -0
- package/dist/internal/oauthSpec.d.ts.map +1 -0
- package/dist/internal/oauthSpec.js +789 -0
- package/dist/internal/organizationName.d.ts +18 -0
- package/dist/internal/organizationName.d.ts.map +1 -0
- package/dist/internal/organizationName.js +24 -0
- package/dist/internal/protocol.d.ts +150 -3
- package/dist/internal/protocol.d.ts.map +1 -1
- package/dist/internal/protocol.js +163 -2
- package/dist/internal/providerApps.d.ts +75 -0
- package/dist/internal/providerApps.d.ts.map +1 -0
- package/dist/internal/providerApps.js +59 -0
- package/dist/internal/providerRegistry.d.ts +218 -0
- package/dist/internal/providerRegistry.d.ts.map +1 -0
- package/dist/internal/providerRegistry.js +432 -0
- package/dist/internal/providerSpec.d.ts +66 -0
- package/dist/internal/providerSpec.d.ts.map +1 -0
- package/dist/internal/providerSpec.js +1154 -0
- package/dist/internal/remoteMcp.d.ts +188 -0
- package/dist/internal/remoteMcp.d.ts.map +1 -0
- package/dist/internal/remoteMcp.js +543 -0
- package/dist/internal/signInProviders.d.ts +104 -0
- package/dist/internal/signInProviders.d.ts.map +1 -0
- package/dist/internal/signInProviders.js +245 -0
- package/dist/internal/tools.d.ts +115 -0
- package/dist/internal/tools.d.ts.map +1 -0
- package/dist/internal/tools.js +80 -0
- package/dist/manifest.d.ts +238 -0
- package/dist/manifest.d.ts.map +1 -0
- package/dist/manifest.js +30 -0
- package/dist/server/ai.d.ts +111 -0
- package/dist/server/ai.d.ts.map +1 -0
- package/dist/server/ai.js +145 -0
- package/dist/server/aiBinding.d.ts +68 -0
- package/dist/server/aiBinding.d.ts.map +1 -0
- package/dist/server/aiBinding.js +294 -0
- package/dist/server/attribution.d.ts +43 -0
- package/dist/server/attribution.d.ts.map +1 -0
- package/dist/server/attribution.js +75 -0
- package/dist/server/authorizeUrl.d.ts +43 -0
- package/dist/server/authorizeUrl.d.ts.map +1 -0
- package/dist/server/authorizeUrl.js +106 -0
- package/dist/server/connectUrl.d.ts +21 -0
- package/dist/server/connectUrl.d.ts.map +1 -0
- package/dist/server/connectUrl.js +37 -0
- package/dist/server/context.d.ts +68 -0
- package/dist/server/context.d.ts.map +1 -0
- package/dist/server/context.js +117 -0
- package/dist/server/emailBinding.d.ts +72 -0
- package/dist/server/emailBinding.d.ts.map +1 -0
- package/dist/server/emailBinding.js +124 -0
- package/dist/server/emit.d.ts +63 -0
- package/dist/server/emit.d.ts.map +1 -0
- package/dist/server/emit.js +101 -0
- package/dist/server/env.d.ts +44 -0
- package/dist/server/env.d.ts.map +1 -0
- package/dist/server/env.js +1 -0
- package/dist/server/everyApp.d.ts +65 -19
- package/dist/server/everyApp.d.ts.map +1 -1
- package/dist/server/everyApp.js +195 -30
- package/dist/server/fromMcpTools.d.ts +89 -0
- package/dist/server/fromMcpTools.d.ts.map +1 -0
- package/dist/server/fromMcpTools.js +192 -0
- package/dist/server/gatewayFetch.d.ts +54 -14
- package/dist/server/gatewayFetch.d.ts.map +1 -1
- package/dist/server/gatewayFetch.js +201 -37
- package/dist/server/gatewayUrl.d.ts +13 -0
- package/dist/server/gatewayUrl.d.ts.map +1 -0
- package/dist/server/gatewayUrl.js +34 -0
- package/dist/server/index.d.ts +23 -3
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +19 -2
- package/dist/server/integrationFetch.d.ts +162 -0
- package/dist/server/integrationFetch.d.ts.map +1 -0
- package/dist/server/integrationFetch.js +688 -0
- package/dist/server/listMembers.d.ts +32 -0
- package/dist/server/listMembers.d.ts.map +1 -0
- package/dist/server/listMembers.js +71 -0
- package/dist/server/nodeServer.d.ts +48 -0
- package/dist/server/nodeServer.d.ts.map +1 -0
- package/dist/server/nodeServer.js +45 -0
- package/dist/server/refusal.d.ts +55 -0
- package/dist/server/refusal.d.ts.map +1 -0
- package/dist/server/refusal.js +193 -0
- package/dist/server/sendEmail.d.ts +39 -0
- package/dist/server/sendEmail.d.ts.map +1 -0
- package/dist/server/sendEmail.js +114 -0
- package/dist/server/tools.d.ts +184 -0
- package/dist/server/tools.d.ts.map +1 -0
- package/dist/server/tools.js +347 -0
- package/dist/server/toolsEndpoint.d.ts +32 -0
- package/dist/server/toolsEndpoint.d.ts.map +1 -0
- package/dist/server/toolsEndpoint.js +237 -0
- package/dist/tanstack-start/index.d.ts +9 -0
- package/dist/tanstack-start/index.d.ts.map +1 -0
- package/dist/tanstack-start/index.js +29 -0
- package/dist/testing/index.d.ts +93 -0
- package/dist/testing/index.d.ts.map +1 -0
- package/dist/testing/index.js +156 -0
- package/package.json +42 -8
- package/dist/server/mcp.d.ts +0 -66
- package/dist/server/mcp.d.ts.map +0 -1
- package/dist/server/mcp.js +0 -147
|
@@ -0,0 +1,1154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A provider spec: the shape `everyapp provider add` takes as JSON (and
|
|
3
|
+
* `everyapp_provider_add` as a tool argument), and the shape every
|
|
4
|
+
* built-in catalog entry has in TypeScript (./capabilities.ts), so a
|
|
5
|
+
* catalog entry is nothing a row couldn't be. `parseProviderSpec` turns
|
|
6
|
+
* JSON into an {@link ApiKeyCapability} or names every problem; nothing
|
|
7
|
+
* here reads a value: a spec names Gateway settings, never holds one.
|
|
8
|
+
*
|
|
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).
|
|
18
|
+
*
|
|
19
|
+
* The rules that make a row safe on a particular gateway (its secret names
|
|
20
|
+
* are its own, its hosts are public, it shadows no catalog entry) are the
|
|
21
|
+
* registry's (./providerRegistry.ts), run after this parse.
|
|
22
|
+
*/
|
|
23
|
+
import { DEFAULT_WRITE_METHODS, authSettingNames, } from "./capabilities.js";
|
|
24
|
+
import { oauthSpecOf, parseOAuthUserSpec } from "./oauthSpec.js";
|
|
25
|
+
/** A provider id: lowercase, a letter first, 2 to 40 characters. */
|
|
26
|
+
export const PROVIDER_ID = /^[a-z][a-z0-9-]{1,39}$/;
|
|
27
|
+
/** A Gateway setting name a provider may hold: upper case, 3 to 64 characters. */
|
|
28
|
+
export const PROVIDER_SECRET_NAME = /^[A-Z][A-Z0-9_]{2,63}$/;
|
|
29
|
+
/** A bare lowercase host with at least one dot: labels of letters, digits and hyphens. */
|
|
30
|
+
export const PROVIDER_HOST = /^(?=.{1,253}$)(?!-)[a-z0-9-]{1,63}(?<!-)(?:\.(?!-)[a-z0-9-]{1,63}(?<!-))+$/;
|
|
31
|
+
const METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE"];
|
|
32
|
+
const SEGMENT = /^[a-z0-9_-]{1,64}$/;
|
|
33
|
+
const HEADER_NAME = /^[A-Za-z0-9-]{1,64}$/;
|
|
34
|
+
const QUERY_PARAM = /^[A-Za-z0-9_.-]{1,64}$/;
|
|
35
|
+
const SCHEME = /^[A-Za-z][A-Za-z0-9-]{0,31}$/;
|
|
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}$/;
|
|
42
|
+
const DATE = /^\d{4}-\d{2}-\d{2}$/;
|
|
43
|
+
const TAG = /^[a-z][a-z0-9-]{0,31}$/;
|
|
44
|
+
/** Control and format characters: in no text a spec carries. */
|
|
45
|
+
const UNPRINTABLE = /[\p{Cc}\p{Cf}]/u;
|
|
46
|
+
/** Headers the proxy sets or strips itself: a spec can't put the key in one. */
|
|
47
|
+
const RESERVED_HEADERS = new Set([
|
|
48
|
+
"host",
|
|
49
|
+
"cookie",
|
|
50
|
+
"content-length",
|
|
51
|
+
"content-type",
|
|
52
|
+
"transfer-encoding",
|
|
53
|
+
"connection",
|
|
54
|
+
"upgrade",
|
|
55
|
+
"te",
|
|
56
|
+
"trailer",
|
|
57
|
+
"keep-alive",
|
|
58
|
+
"proxy-authorization",
|
|
59
|
+
"proxy-authenticate",
|
|
60
|
+
"forwarded",
|
|
61
|
+
"x-forwarded-for",
|
|
62
|
+
"x-forwarded-host",
|
|
63
|
+
"x-forwarded-proto",
|
|
64
|
+
]);
|
|
65
|
+
const MAX_HOSTS = 20;
|
|
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";
|
|
76
|
+
const KNOWN_FIELDS = new Set([
|
|
77
|
+
"id",
|
|
78
|
+
"mode",
|
|
79
|
+
"name",
|
|
80
|
+
"description",
|
|
81
|
+
"docs",
|
|
82
|
+
"api",
|
|
83
|
+
"hosts",
|
|
84
|
+
"hostSetting",
|
|
85
|
+
"data",
|
|
86
|
+
"auth",
|
|
87
|
+
"secrets",
|
|
88
|
+
"access",
|
|
89
|
+
"readMethods",
|
|
90
|
+
"writeMethods",
|
|
91
|
+
"accountWidePaths",
|
|
92
|
+
"hint",
|
|
93
|
+
"smoke",
|
|
94
|
+
"cost",
|
|
95
|
+
"rate",
|
|
96
|
+
"example",
|
|
97
|
+
"verified",
|
|
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",
|
|
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
|
+
];
|
|
152
|
+
function isRecord(value) {
|
|
153
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
154
|
+
}
|
|
155
|
+
function text(value, path, problems, { max, min = 1 }) {
|
|
156
|
+
if (value === undefined)
|
|
157
|
+
return undefined;
|
|
158
|
+
if (typeof value !== "string") {
|
|
159
|
+
problems.push({ path, message: "must be a string" });
|
|
160
|
+
return undefined;
|
|
161
|
+
}
|
|
162
|
+
if (value.length < min || value.length > max) {
|
|
163
|
+
problems.push({
|
|
164
|
+
path,
|
|
165
|
+
message: `must be ${min} to ${max} characters`,
|
|
166
|
+
});
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
if (UNPRINTABLE.test(value)) {
|
|
170
|
+
problems.push({ path, message: "holds a control character" });
|
|
171
|
+
return undefined;
|
|
172
|
+
}
|
|
173
|
+
return value;
|
|
174
|
+
}
|
|
175
|
+
function httpsUrl(value, path, problems) {
|
|
176
|
+
const url = text(value, path, problems, { max: 500 });
|
|
177
|
+
if (url === undefined)
|
|
178
|
+
return undefined;
|
|
179
|
+
let parsed;
|
|
180
|
+
try {
|
|
181
|
+
parsed = new URL(url);
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
problems.push({ path, message: "must be an https URL" });
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
if (parsed.protocol !== "https:") {
|
|
188
|
+
problems.push({ path, message: "must be an https URL" });
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
return url;
|
|
192
|
+
}
|
|
193
|
+
function stringList(value, path, problems, { max, each }) {
|
|
194
|
+
if (value === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
if (!Array.isArray(value)) {
|
|
197
|
+
problems.push({ path, message: "must be a list" });
|
|
198
|
+
return undefined;
|
|
199
|
+
}
|
|
200
|
+
if (value.length > max) {
|
|
201
|
+
problems.push({ path, message: `at most ${max} entries` });
|
|
202
|
+
return undefined;
|
|
203
|
+
}
|
|
204
|
+
const out = [];
|
|
205
|
+
value.forEach((item, index) => {
|
|
206
|
+
if (typeof item !== "string") {
|
|
207
|
+
problems.push({ path: `${path}.${index}`, message: "must be a string" });
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
each(item, `${path}.${index}`);
|
|
211
|
+
out.push(item);
|
|
212
|
+
});
|
|
213
|
+
if (new Set(out).size !== out.length) {
|
|
214
|
+
problems.push({ path, message: "lists an entry twice" });
|
|
215
|
+
}
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
function methods(value, path, problems) {
|
|
219
|
+
return stringList(value, path, problems, {
|
|
220
|
+
max: METHODS.length,
|
|
221
|
+
each: (method, at) => {
|
|
222
|
+
if (!METHODS.includes(method)) {
|
|
223
|
+
problems.push({
|
|
224
|
+
path: at,
|
|
225
|
+
message: `must be one of ${METHODS.join(", ")}`,
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
},
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
function secretName(value, path, problems) {
|
|
232
|
+
if (typeof value !== "string" || !PROVIDER_SECRET_NAME.test(value)) {
|
|
233
|
+
problems.push({
|
|
234
|
+
path,
|
|
235
|
+
message: "must name a Gateway setting: upper case letters, digits and underscores, a letter first, 3 to 64 characters (STRIPE_API_KEY)",
|
|
236
|
+
});
|
|
237
|
+
return undefined;
|
|
238
|
+
}
|
|
239
|
+
return value;
|
|
240
|
+
}
|
|
241
|
+
function auth(value, problems) {
|
|
242
|
+
if (!isRecord(value)) {
|
|
243
|
+
problems.push({
|
|
244
|
+
path: "auth",
|
|
245
|
+
message: "must be an object with a kind: bearer, header, query or basic",
|
|
246
|
+
});
|
|
247
|
+
return undefined;
|
|
248
|
+
}
|
|
249
|
+
const kind = value["kind"];
|
|
250
|
+
const secret = secretName(value["secret"], "auth.secret", problems);
|
|
251
|
+
const known = (fields) => {
|
|
252
|
+
for (const field of Object.keys(value)) {
|
|
253
|
+
if (!fields.includes(field)) {
|
|
254
|
+
problems.push({
|
|
255
|
+
path: `auth.${field}`,
|
|
256
|
+
message: `not a field of auth.kind ${String(kind)}`,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
};
|
|
261
|
+
switch (kind) {
|
|
262
|
+
case "bearer": {
|
|
263
|
+
known(["kind", "secret", "scheme"]);
|
|
264
|
+
const scheme = text(value["scheme"], "auth.scheme", problems, {
|
|
265
|
+
max: 32,
|
|
266
|
+
});
|
|
267
|
+
if (scheme !== undefined && !SCHEME.test(scheme)) {
|
|
268
|
+
problems.push({
|
|
269
|
+
path: "auth.scheme",
|
|
270
|
+
message: "must be a token such as Bearer, Token or Api-Key",
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
if (!secret)
|
|
274
|
+
return undefined;
|
|
275
|
+
return { kind, secret, ...(scheme === undefined ? {} : { scheme }) };
|
|
276
|
+
}
|
|
277
|
+
case "header": {
|
|
278
|
+
known(["kind", "secret", "header", "prefix"]);
|
|
279
|
+
const header = text(value["header"], "auth.header", problems, {
|
|
280
|
+
max: 64,
|
|
281
|
+
});
|
|
282
|
+
if (header !== undefined) {
|
|
283
|
+
if (!HEADER_NAME.test(header)) {
|
|
284
|
+
problems.push({
|
|
285
|
+
path: "auth.header",
|
|
286
|
+
message: "must be a header name (letters, digits, hyphens)",
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
else if (RESERVED_HEADERS.has(header.toLowerCase()) ||
|
|
290
|
+
header.toLowerCase().startsWith("x-everyapp-")) {
|
|
291
|
+
problems.push({
|
|
292
|
+
path: "auth.header",
|
|
293
|
+
message: `${header} is a header the Gateway sets or strips itself`,
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
const prefix = text(value["prefix"], "auth.prefix", problems, {
|
|
298
|
+
max: 32,
|
|
299
|
+
min: 0,
|
|
300
|
+
});
|
|
301
|
+
if (!secret || header === undefined)
|
|
302
|
+
return undefined;
|
|
303
|
+
return {
|
|
304
|
+
kind,
|
|
305
|
+
secret,
|
|
306
|
+
header,
|
|
307
|
+
...(prefix === undefined ? {} : { prefix }),
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
case "query": {
|
|
311
|
+
known(["kind", "secret", "param"]);
|
|
312
|
+
const param = text(value["param"], "auth.param", problems, { max: 64 });
|
|
313
|
+
if (param !== undefined && !QUERY_PARAM.test(param)) {
|
|
314
|
+
problems.push({
|
|
315
|
+
path: "auth.param",
|
|
316
|
+
message: "must be a query parameter name",
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
if (!secret || param === undefined)
|
|
320
|
+
return undefined;
|
|
321
|
+
return { kind, secret, param };
|
|
322
|
+
}
|
|
323
|
+
case "basic": {
|
|
324
|
+
known(["kind", "secret", "as", "user"]);
|
|
325
|
+
const as = value["as"];
|
|
326
|
+
if (as !== "password" && as !== "username" && as !== "pair") {
|
|
327
|
+
problems.push({
|
|
328
|
+
path: "auth.as",
|
|
329
|
+
message: 'must say where the key goes: "password" (beside user), "username" (with an empty password), or "pair" (the key is login:password)',
|
|
330
|
+
});
|
|
331
|
+
return undefined;
|
|
332
|
+
}
|
|
333
|
+
if (as !== "password") {
|
|
334
|
+
if (value["user"] !== undefined) {
|
|
335
|
+
problems.push({
|
|
336
|
+
path: "auth.user",
|
|
337
|
+
message: `not a field of auth.as ${as}`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
if (!secret)
|
|
341
|
+
return undefined;
|
|
342
|
+
return { kind, secret, as };
|
|
343
|
+
}
|
|
344
|
+
const raw = value["user"];
|
|
345
|
+
let user;
|
|
346
|
+
if (typeof raw === "string") {
|
|
347
|
+
const literal = text(raw, "auth.user", problems, { max: 128 });
|
|
348
|
+
if (literal !== undefined && literal.includes(":")) {
|
|
349
|
+
problems.push({ path: "auth.user", message: "may not hold a colon" });
|
|
350
|
+
}
|
|
351
|
+
user = literal;
|
|
352
|
+
}
|
|
353
|
+
else if (isRecord(raw) && Object.keys(raw).join() === "secret") {
|
|
354
|
+
const named = secretName(raw["secret"], "auth.user.secret", problems);
|
|
355
|
+
if (named !== undefined && named === secret) {
|
|
356
|
+
problems.push({
|
|
357
|
+
path: "auth.user.secret",
|
|
358
|
+
message: "must be another setting than auth.secret",
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
user = named === undefined ? undefined : { secret: named };
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
problems.push({
|
|
365
|
+
path: "auth.user",
|
|
366
|
+
message: "required with as: password: a string, or { secret: NAME }",
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
if (!secret || user === undefined)
|
|
370
|
+
return undefined;
|
|
371
|
+
return { kind, secret, as, user };
|
|
372
|
+
}
|
|
373
|
+
default:
|
|
374
|
+
problems.push({
|
|
375
|
+
path: "auth.kind",
|
|
376
|
+
message: "must be bearer, header, query, basic or oauth2-user (an AI provider is a catalog entry, not a spec this gateway takes)",
|
|
377
|
+
});
|
|
378
|
+
return undefined;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function secretDetails(value, names, problems) {
|
|
382
|
+
if (value === undefined)
|
|
383
|
+
return undefined;
|
|
384
|
+
if (!Array.isArray(value)) {
|
|
385
|
+
problems.push({ path: "secrets", message: "must be a list" });
|
|
386
|
+
return undefined;
|
|
387
|
+
}
|
|
388
|
+
const out = [];
|
|
389
|
+
value.forEach((item, index) => {
|
|
390
|
+
const at = `secrets.${index}`;
|
|
391
|
+
if (!isRecord(item)) {
|
|
392
|
+
problems.push({ path: at, message: "must be an object" });
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
for (const field of Object.keys(item)) {
|
|
396
|
+
if (!["name", "hint"].includes(field)) {
|
|
397
|
+
problems.push({ path: `${at}.${field}`, message: "unknown field" });
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
const name = secretName(item["name"], `${at}.name`, problems);
|
|
401
|
+
if (name !== undefined && !names.includes(name)) {
|
|
402
|
+
problems.push({
|
|
403
|
+
path: `${at}.name`,
|
|
404
|
+
message: `auth names no setting ${name}`,
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
const hint = text(item["hint"], `${at}.hint`, problems, { max: 300 });
|
|
408
|
+
if (name === undefined)
|
|
409
|
+
return;
|
|
410
|
+
out.push({ name, ...(hint === undefined ? {} : { hint }) });
|
|
411
|
+
});
|
|
412
|
+
if (new Set(out.map((detail) => detail.name)).size !== out.length) {
|
|
413
|
+
problems.push({ path: "secrets", message: "names a setting twice" });
|
|
414
|
+
}
|
|
415
|
+
return out;
|
|
416
|
+
}
|
|
417
|
+
function accountWidePaths(value, problems) {
|
|
418
|
+
if (value === undefined)
|
|
419
|
+
return undefined;
|
|
420
|
+
if (!Array.isArray(value)) {
|
|
421
|
+
problems.push({
|
|
422
|
+
path: "accountWidePaths",
|
|
423
|
+
message: 'must be a list of segment runs: [["v1", "account"]]',
|
|
424
|
+
});
|
|
425
|
+
return undefined;
|
|
426
|
+
}
|
|
427
|
+
if (value.length > MAX_ACCOUNT_WIDE_PATHS) {
|
|
428
|
+
problems.push({
|
|
429
|
+
path: "accountWidePaths",
|
|
430
|
+
message: `at most ${MAX_ACCOUNT_WIDE_PATHS} entries`,
|
|
431
|
+
});
|
|
432
|
+
return undefined;
|
|
433
|
+
}
|
|
434
|
+
const out = [];
|
|
435
|
+
value.forEach((run, index) => {
|
|
436
|
+
const at = `accountWidePaths.${index}`;
|
|
437
|
+
if (!Array.isArray(run) || run.length === 0) {
|
|
438
|
+
problems.push({
|
|
439
|
+
path: at,
|
|
440
|
+
message: "must be a non-empty list of segments",
|
|
441
|
+
});
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
const segments = [];
|
|
445
|
+
run.forEach((segment, offset) => {
|
|
446
|
+
if (typeof segment !== "string" || !SEGMENT.test(segment)) {
|
|
447
|
+
problems.push({
|
|
448
|
+
path: `${at}.${offset}`,
|
|
449
|
+
message: "must be a lowercase path segment (letters, digits, _ and -)",
|
|
450
|
+
});
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
segments.push(segment);
|
|
454
|
+
});
|
|
455
|
+
if (segments.length === run.length)
|
|
456
|
+
out.push(segments);
|
|
457
|
+
});
|
|
458
|
+
return out;
|
|
459
|
+
}
|
|
460
|
+
function smoke(value, hosts, problems) {
|
|
461
|
+
if (value === undefined)
|
|
462
|
+
return undefined;
|
|
463
|
+
if (!isRecord(value)) {
|
|
464
|
+
problems.push({ path: "smoke", message: "must be an object" });
|
|
465
|
+
return undefined;
|
|
466
|
+
}
|
|
467
|
+
for (const field of Object.keys(value)) {
|
|
468
|
+
if (!["method", "host", "path", "expect", "free", "note"].includes(field)) {
|
|
469
|
+
problems.push({ path: `smoke.${field}`, message: "unknown field" });
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
const method = value["method"];
|
|
473
|
+
if (method !== "GET" && method !== "HEAD" && method !== "POST") {
|
|
474
|
+
problems.push({
|
|
475
|
+
path: "smoke.method",
|
|
476
|
+
message: "must be GET, HEAD or POST",
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
const host = text(value["host"], "smoke.host", problems, { max: 253 });
|
|
480
|
+
if (host !== undefined && !hosts.includes(host)) {
|
|
481
|
+
problems.push({ path: "smoke.host", message: "must be one of hosts" });
|
|
482
|
+
}
|
|
483
|
+
const path = text(value["path"], "smoke.path", problems, { max: 500 });
|
|
484
|
+
if (path !== undefined && (!path.startsWith("/") || path.startsWith("//"))) {
|
|
485
|
+
problems.push({ path: "smoke.path", message: "must start with one /" });
|
|
486
|
+
}
|
|
487
|
+
const expect = value["expect"];
|
|
488
|
+
const statuses = Array.isArray(expect) &&
|
|
489
|
+
expect.length > 0 &&
|
|
490
|
+
expect.every((status) => Number.isInteger(status) &&
|
|
491
|
+
status >= 100 &&
|
|
492
|
+
status <= 599)
|
|
493
|
+
? expect
|
|
494
|
+
: undefined;
|
|
495
|
+
if (!statuses) {
|
|
496
|
+
problems.push({
|
|
497
|
+
path: "smoke.expect",
|
|
498
|
+
message: "must list the HTTP statuses that mean the key works",
|
|
499
|
+
});
|
|
500
|
+
}
|
|
501
|
+
const free = value["free"];
|
|
502
|
+
if (typeof free !== "boolean") {
|
|
503
|
+
problems.push({ path: "smoke.free", message: "must be true or false" });
|
|
504
|
+
}
|
|
505
|
+
const note = text(value["note"], "smoke.note", problems, { max: 200 });
|
|
506
|
+
if ((method !== "GET" && method !== "HEAD" && method !== "POST") ||
|
|
507
|
+
path === undefined ||
|
|
508
|
+
!statuses ||
|
|
509
|
+
typeof free !== "boolean") {
|
|
510
|
+
return undefined;
|
|
511
|
+
}
|
|
512
|
+
return {
|
|
513
|
+
method,
|
|
514
|
+
...(host === undefined ? {} : { host }),
|
|
515
|
+
path,
|
|
516
|
+
expect: statuses,
|
|
517
|
+
free,
|
|
518
|
+
...(note === undefined ? {} : { note }),
|
|
519
|
+
};
|
|
520
|
+
}
|
|
521
|
+
function nonNegative(value, path, problems) {
|
|
522
|
+
if (value === undefined)
|
|
523
|
+
return undefined;
|
|
524
|
+
if (typeof value !== "number" || !Number.isFinite(value) || value < 0) {
|
|
525
|
+
problems.push({ path, message: "must be a number, zero or more" });
|
|
526
|
+
return undefined;
|
|
527
|
+
}
|
|
528
|
+
return value;
|
|
529
|
+
}
|
|
530
|
+
function cost(value, problems) {
|
|
531
|
+
if (value === undefined)
|
|
532
|
+
return undefined;
|
|
533
|
+
if (!isRecord(value)) {
|
|
534
|
+
problems.push({ path: "cost", message: "must be an object" });
|
|
535
|
+
return undefined;
|
|
536
|
+
}
|
|
537
|
+
for (const field of Object.keys(value)) {
|
|
538
|
+
if (!["unit", "usd", "note", "responseField"].includes(field)) {
|
|
539
|
+
problems.push({ path: `cost.${field}`, message: "unknown field" });
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
const unit = value["unit"];
|
|
543
|
+
if (unit !== "call" && unit !== "result" && unit !== "token") {
|
|
544
|
+
problems.push({
|
|
545
|
+
path: "cost.unit",
|
|
546
|
+
message: "must be call, result or token",
|
|
547
|
+
});
|
|
548
|
+
}
|
|
549
|
+
const usd = nonNegative(value["usd"], "cost.usd", problems);
|
|
550
|
+
const note = text(value["note"], "cost.note", problems, { max: 200 });
|
|
551
|
+
const responseField = text(value["responseField"], "cost.responseField", problems, { max: 128 });
|
|
552
|
+
if (responseField !== undefined && !RESPONSE_FIELD.test(responseField)) {
|
|
553
|
+
problems.push({
|
|
554
|
+
path: "cost.responseField",
|
|
555
|
+
message: "must be a JSON field name, or a dotted path to a nested one",
|
|
556
|
+
});
|
|
557
|
+
}
|
|
558
|
+
if (unit !== "call" && unit !== "result" && unit !== "token")
|
|
559
|
+
return undefined;
|
|
560
|
+
return {
|
|
561
|
+
unit,
|
|
562
|
+
...(usd === undefined ? {} : { usd }),
|
|
563
|
+
...(note === undefined ? {} : { note }),
|
|
564
|
+
...(responseField === undefined ? {} : { responseField }),
|
|
565
|
+
};
|
|
566
|
+
}
|
|
567
|
+
function rate(value, problems) {
|
|
568
|
+
if (value === undefined)
|
|
569
|
+
return undefined;
|
|
570
|
+
if (!isRecord(value)) {
|
|
571
|
+
problems.push({ path: "rate", message: "must be an object" });
|
|
572
|
+
return undefined;
|
|
573
|
+
}
|
|
574
|
+
for (const field of Object.keys(value)) {
|
|
575
|
+
if (!["perSecond", "perMinute", "perDay", "note"].includes(field)) {
|
|
576
|
+
problems.push({ path: `rate.${field}`, message: "unknown field" });
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const perSecond = nonNegative(value["perSecond"], "rate.perSecond", problems);
|
|
580
|
+
const perMinute = nonNegative(value["perMinute"], "rate.perMinute", problems);
|
|
581
|
+
const perDay = nonNegative(value["perDay"], "rate.perDay", problems);
|
|
582
|
+
const note = text(value["note"], "rate.note", problems, { max: 200 });
|
|
583
|
+
return {
|
|
584
|
+
...(perSecond === undefined ? {} : { perSecond }),
|
|
585
|
+
...(perMinute === undefined ? {} : { perMinute }),
|
|
586
|
+
...(perDay === undefined ? {} : { perDay }),
|
|
587
|
+
...(note === undefined ? {} : { note }),
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function verified(value, problems) {
|
|
591
|
+
if (value === undefined)
|
|
592
|
+
return undefined;
|
|
593
|
+
if (value === false)
|
|
594
|
+
return false;
|
|
595
|
+
if (isRecord(value) &&
|
|
596
|
+
typeof value["on"] === "string" &&
|
|
597
|
+
DATE.test(value["on"]) &&
|
|
598
|
+
(value["how"] === "docs" || value["how"] === "live") &&
|
|
599
|
+
Object.keys(value).length === 2) {
|
|
600
|
+
return { on: value["on"], how: value["how"] };
|
|
601
|
+
}
|
|
602
|
+
problems.push({
|
|
603
|
+
path: "verified",
|
|
604
|
+
message: 'must be false, or { "on": "YYYY-MM-DD", "how": "docs" | "live" }',
|
|
605
|
+
});
|
|
606
|
+
return undefined;
|
|
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
|
+
}
|
|
645
|
+
/**
|
|
646
|
+
* Parse a provider spec. Every problem is reported, with its path, so a
|
|
647
|
+
* person fixes the file once. On success the id and the catalog-shaped
|
|
648
|
+
* capability, defaults filled in (`access` read, `readMethods` GET and
|
|
649
|
+
* HEAD, no account-wide paths).
|
|
650
|
+
*/
|
|
651
|
+
export function parseProviderSpec(input) {
|
|
652
|
+
const problems = [];
|
|
653
|
+
if (!isRecord(input)) {
|
|
654
|
+
return {
|
|
655
|
+
ok: false,
|
|
656
|
+
problems: [{ path: "(root)", message: "must be a JSON object" }],
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
for (const field of Object.keys(input)) {
|
|
660
|
+
if (!KNOWN_FIELDS.has(field)) {
|
|
661
|
+
problems.push({ path: field, message: "unknown field" });
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
const id = input["id"];
|
|
665
|
+
if (typeof id !== "string" || !PROVIDER_ID.test(id)) {
|
|
666
|
+
problems.push({
|
|
667
|
+
path: "id",
|
|
668
|
+
message: "must be lowercase letters, digits and hyphens, a letter first, 2 to 40 characters (stripe)",
|
|
669
|
+
});
|
|
670
|
+
}
|
|
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) {
|
|
675
|
+
problems.push({
|
|
676
|
+
path: "mode",
|
|
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",
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
const name = text(input["name"], "name", problems, { max: 64 });
|
|
689
|
+
if (input["name"] === undefined) {
|
|
690
|
+
problems.push({ path: "name", message: "required" });
|
|
691
|
+
}
|
|
692
|
+
const description = text(input["description"], "description", problems, {
|
|
693
|
+
max: 200,
|
|
694
|
+
});
|
|
695
|
+
const docs = httpsUrl(input["docs"], "docs", problems);
|
|
696
|
+
if (input["docs"] === undefined) {
|
|
697
|
+
problems.push({
|
|
698
|
+
path: "docs",
|
|
699
|
+
message: "required: where the person gets the key, an https URL",
|
|
700
|
+
});
|
|
701
|
+
}
|
|
702
|
+
const api = httpsUrl(input["api"], "api", problems);
|
|
703
|
+
const hosts = stringList(input["hosts"], "hosts", problems, {
|
|
704
|
+
max: MAX_HOSTS,
|
|
705
|
+
each: (host, at) => {
|
|
706
|
+
if (!PROVIDER_HOST.test(host)) {
|
|
707
|
+
problems.push({
|
|
708
|
+
path: at,
|
|
709
|
+
message: "must be a bare lowercase host with a dot and no port, path or scheme (api.stripe.com)",
|
|
710
|
+
});
|
|
711
|
+
}
|
|
712
|
+
},
|
|
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
|
+
}
|
|
772
|
+
const hostSetting = input["hostSetting"] === undefined
|
|
773
|
+
? undefined
|
|
774
|
+
: secretName(input["hostSetting"], "hostSetting", problems);
|
|
775
|
+
if (hostSetting === undefined &&
|
|
776
|
+
(input["hosts"] === undefined || (hosts && hosts.length === 0))) {
|
|
777
|
+
problems.push({
|
|
778
|
+
path: "hosts",
|
|
779
|
+
message: "required: at least one host, or a hostSetting naming one",
|
|
780
|
+
});
|
|
781
|
+
}
|
|
782
|
+
const rawData = input["data"];
|
|
783
|
+
if (rawData !== undefined &&
|
|
784
|
+
rawData !== "per-call" &&
|
|
785
|
+
rawData !== "account") {
|
|
786
|
+
problems.push({ path: "data", message: "must be per-call or account" });
|
|
787
|
+
}
|
|
788
|
+
const data = rawData === "per-call" || rawData === "account" ? rawData : undefined;
|
|
789
|
+
const parsedAuth = auth(input["auth"], problems);
|
|
790
|
+
if (input["auth"] === undefined) {
|
|
791
|
+
problems.push({ path: "auth", message: "required" });
|
|
792
|
+
}
|
|
793
|
+
if (hostSetting !== undefined &&
|
|
794
|
+
parsedAuth &&
|
|
795
|
+
authSettingNames(parsedAuth).includes(hostSetting)) {
|
|
796
|
+
problems.push({
|
|
797
|
+
path: "hostSetting",
|
|
798
|
+
message: "must be another setting than the key",
|
|
799
|
+
});
|
|
800
|
+
}
|
|
801
|
+
const secrets = secretDetails(input["secrets"], [
|
|
802
|
+
...(parsedAuth ? authSettingNames(parsedAuth) : []),
|
|
803
|
+
...(hostSetting === undefined ? [] : [hostSetting]),
|
|
804
|
+
], problems);
|
|
805
|
+
const { access, readMethods, writeMethods } = grants(input, problems);
|
|
806
|
+
const paths = accountWidePaths(input["accountWidePaths"], problems);
|
|
807
|
+
const hint = text(input["hint"], "hint", problems, { max: 500 });
|
|
808
|
+
const smokeRequest = smoke(input["smoke"], hosts ?? [], problems);
|
|
809
|
+
const parsedCost = cost(input["cost"], problems);
|
|
810
|
+
const parsedRate = rate(input["rate"], problems);
|
|
811
|
+
const example = text(input["example"], "example", problems, { max: 200 });
|
|
812
|
+
const parsedVerified = verified(input["verified"], problems);
|
|
813
|
+
const tags = tagList(input["tags"], problems);
|
|
814
|
+
if (problems.length > 0 ||
|
|
815
|
+
typeof id !== "string" ||
|
|
816
|
+
name === undefined ||
|
|
817
|
+
docs === undefined ||
|
|
818
|
+
(!hosts && hostSetting === undefined) ||
|
|
819
|
+
!parsedAuth) {
|
|
820
|
+
return { ok: false, problems };
|
|
821
|
+
}
|
|
822
|
+
const capability = {
|
|
823
|
+
mode: "api_key",
|
|
824
|
+
label: name,
|
|
825
|
+
auth: parsedAuth,
|
|
826
|
+
docs,
|
|
827
|
+
hosts: hosts ?? [],
|
|
828
|
+
...(hostSetting === undefined ? {} : { hostSetting }),
|
|
829
|
+
...(data === undefined ? {} : { data }),
|
|
830
|
+
access: access ?? ["read"],
|
|
831
|
+
readMethods: readMethods ?? ["GET", "HEAD"],
|
|
832
|
+
accountWidePaths: paths ?? [],
|
|
833
|
+
...(description === undefined ? {} : { description }),
|
|
834
|
+
...(api === undefined ? {} : { api }),
|
|
835
|
+
...(secrets === undefined ? {} : { secrets }),
|
|
836
|
+
...(writeMethods === undefined ? {} : { writeMethods }),
|
|
837
|
+
...(hint === undefined ? {} : { hint }),
|
|
838
|
+
...(smokeRequest === undefined ? {} : { smoke: smokeRequest }),
|
|
839
|
+
...(parsedCost === undefined ? {} : { cost: parsedCost }),
|
|
840
|
+
...(parsedRate === undefined ? {} : { rate: parsedRate }),
|
|
841
|
+
...(example === undefined ? {} : { example }),
|
|
842
|
+
...(parsedVerified === undefined ? {} : { verified: parsedVerified }),
|
|
843
|
+
...(tags === undefined ? {} : { tags }),
|
|
844
|
+
};
|
|
845
|
+
return { ok: true, id, capability };
|
|
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
|
+
}
|
|
1038
|
+
/** The problems as one line: `auth.secret: must name a Gateway setting; hosts.0: …`. */
|
|
1039
|
+
export function describeSpecProblems(problems) {
|
|
1040
|
+
return problems
|
|
1041
|
+
.map((problem) => `${problem.path}: ${problem.message}`)
|
|
1042
|
+
.join("; ");
|
|
1043
|
+
}
|
|
1044
|
+
/**
|
|
1045
|
+
* The spec of an API-key capability as JSON (`provider show`, the
|
|
1046
|
+
* catalog's round trip): the entry's fields in the spec's names, with the
|
|
1047
|
+
* defaults the parser fills in spelled out, so `parseProviderSpec` of it
|
|
1048
|
+
* gives the entry back.
|
|
1049
|
+
*/
|
|
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
|
+
}
|
|
1110
|
+
return {
|
|
1111
|
+
id,
|
|
1112
|
+
name: capability.label,
|
|
1113
|
+
...(capability.description === undefined
|
|
1114
|
+
? {}
|
|
1115
|
+
: { description: capability.description }),
|
|
1116
|
+
docs: capability.docs,
|
|
1117
|
+
...(capability.api === undefined ? {} : { api: capability.api }),
|
|
1118
|
+
hosts: [...capability.hosts],
|
|
1119
|
+
...(capability.hostSetting === undefined
|
|
1120
|
+
? {}
|
|
1121
|
+
: { hostSetting: capability.hostSetting }),
|
|
1122
|
+
...(capability.data === undefined ? {} : { data: capability.data }),
|
|
1123
|
+
auth: { ...capability.auth },
|
|
1124
|
+
...(capability.secrets === undefined
|
|
1125
|
+
? {}
|
|
1126
|
+
: { secrets: capability.secrets.map((detail) => ({ ...detail })) }),
|
|
1127
|
+
access: [...capability.access],
|
|
1128
|
+
readMethods: [...capability.readMethods],
|
|
1129
|
+
...(capability.writeMethods === undefined
|
|
1130
|
+
? {}
|
|
1131
|
+
: { writeMethods: [...capability.writeMethods] }),
|
|
1132
|
+
accountWidePaths: capability.accountWidePaths.map((run) => [...run]),
|
|
1133
|
+
...(capability.hint === undefined ? {} : { hint: capability.hint }),
|
|
1134
|
+
...(capability.smoke === undefined
|
|
1135
|
+
? {}
|
|
1136
|
+
: {
|
|
1137
|
+
smoke: { ...capability.smoke, expect: [...capability.smoke.expect] },
|
|
1138
|
+
}),
|
|
1139
|
+
...(capability.cost === undefined ? {} : { cost: { ...capability.cost } }),
|
|
1140
|
+
...(capability.rate === undefined ? {} : { rate: { ...capability.rate } }),
|
|
1141
|
+
...(capability.example === undefined
|
|
1142
|
+
? {}
|
|
1143
|
+
: { example: capability.example }),
|
|
1144
|
+
...(capability.verified === undefined
|
|
1145
|
+
? {}
|
|
1146
|
+
: { verified: capability.verified }),
|
|
1147
|
+
...(capability.tags === undefined ? {} : { tags: [...capability.tags] }),
|
|
1148
|
+
};
|
|
1149
|
+
}
|
|
1150
|
+
/** The methods an app with `access: "write"` may use on `capability` besides its read methods. */
|
|
1151
|
+
export function writeMethodsOf(capability) {
|
|
1152
|
+
return ((capability.mode === "ai" ? undefined : capability.writeMethods) ??
|
|
1153
|
+
DEFAULT_WRITE_METHODS);
|
|
1154
|
+
}
|