@opengeni/capabilities 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +190 -0
- package/README.md +23 -0
- package/THIRD_PARTY_NOTICES +28 -0
- package/dist/auth.d.ts +3 -0
- package/dist/graphql.d.ts +52 -0
- package/dist/http.d.ts +15 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.js +1944 -0
- package/dist/index.js.map +1 -0
- package/dist/mcp-manifest.d.ts +25 -0
- package/dist/openapi.d.ts +69 -0
- package/dist/providers.d.ts +44 -0
- package/dist/revision.d.ts +4 -0
- package/dist/types.d.ts +94 -0
- package/package.json +46 -0
- package/src/auth.ts +171 -0
- package/src/graphql.ts +625 -0
- package/src/http.ts +131 -0
- package/src/index.ts +8 -0
- package/src/mcp-manifest.ts +90 -0
- package/src/openapi.ts +846 -0
- package/src/providers.ts +559 -0
- package/src/revision.ts +40 -0
- package/src/types.ts +126 -0
package/src/providers.ts
ADDED
|
@@ -0,0 +1,559 @@
|
|
|
1
|
+
import { IntegrationProtocolError } from "./types";
|
|
2
|
+
|
|
3
|
+
export interface OAuthProviderPreset {
|
|
4
|
+
readonly authorizationUrl: string;
|
|
5
|
+
readonly tokenUrl: string;
|
|
6
|
+
readonly scopes: readonly string[];
|
|
7
|
+
readonly tokenPlacement: {
|
|
8
|
+
readonly carrier: "header";
|
|
9
|
+
readonly name: "Authorization";
|
|
10
|
+
readonly prefix: "Bearer ";
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface OpenApiProviderPreset {
|
|
15
|
+
readonly id: string;
|
|
16
|
+
readonly name: string;
|
|
17
|
+
readonly summary: string;
|
|
18
|
+
readonly family: "google" | "microsoft";
|
|
19
|
+
readonly sourceFormat: "google-discovery" | "openapi";
|
|
20
|
+
readonly sourceUrl: string;
|
|
21
|
+
readonly baseUrl: string;
|
|
22
|
+
readonly oauth: OAuthProviderPreset;
|
|
23
|
+
readonly pathPrefixes?: readonly string[];
|
|
24
|
+
readonly healthOperation?: string;
|
|
25
|
+
readonly healthArgs?: Readonly<Record<string, unknown>>;
|
|
26
|
+
readonly features: readonly IntegrationFeatureDefinition[];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface IntegrationFeatureDefinition {
|
|
30
|
+
readonly featureKey: string;
|
|
31
|
+
readonly kind: "knowledge_source" | "inbound_trigger" | "delivery_destination" | "identity_link";
|
|
32
|
+
readonly configSchema: Readonly<Record<string, unknown>>;
|
|
33
|
+
readonly capabilities: Readonly<Record<string, unknown>>;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const accountIdentityFeature = (
|
|
37
|
+
provider: "google" | "microsoft",
|
|
38
|
+
): IntegrationFeatureDefinition => ({
|
|
39
|
+
featureKey: "account-identity",
|
|
40
|
+
kind: "identity_link",
|
|
41
|
+
configSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
42
|
+
capabilities: {
|
|
43
|
+
provider,
|
|
44
|
+
connectionRequired: true,
|
|
45
|
+
identity: "connected_account",
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
const driveKnowledgeFeature = (
|
|
50
|
+
provider: "google-drive" | "microsoft-onedrive",
|
|
51
|
+
): IntegrationFeatureDefinition => ({
|
|
52
|
+
featureKey: "drive-content",
|
|
53
|
+
kind: "knowledge_source",
|
|
54
|
+
configSchema: {
|
|
55
|
+
type: "object",
|
|
56
|
+
required: ["sources", "destination", "syncCadence", "readPolicy"],
|
|
57
|
+
properties: {
|
|
58
|
+
sources: {
|
|
59
|
+
type: "array",
|
|
60
|
+
minItems: 1,
|
|
61
|
+
maxItems: 100,
|
|
62
|
+
items: {
|
|
63
|
+
type: "object",
|
|
64
|
+
required: ["id", "name", "mimeType", "sourceKind", "includeDescendants"],
|
|
65
|
+
properties: {
|
|
66
|
+
id: { type: "string", minLength: 1, maxLength: 512 },
|
|
67
|
+
name: { type: "string", minLength: 1, maxLength: 1024 },
|
|
68
|
+
mimeType: { type: "string", minLength: 1, maxLength: 256 },
|
|
69
|
+
driveId: { type: "string", minLength: 1, maxLength: 512 },
|
|
70
|
+
sourceKind: {
|
|
71
|
+
type: "string",
|
|
72
|
+
enum:
|
|
73
|
+
provider === "google-drive"
|
|
74
|
+
? ["my_drive", "shared_drive", "folder"]
|
|
75
|
+
: ["my_drive", "shared_library", "folder"],
|
|
76
|
+
},
|
|
77
|
+
includeDescendants: { type: "boolean" },
|
|
78
|
+
},
|
|
79
|
+
additionalProperties: false,
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
destination: {
|
|
83
|
+
type: "object",
|
|
84
|
+
required: ["authorityKind", "authorityAccountId"],
|
|
85
|
+
properties: {
|
|
86
|
+
authorityKind: {
|
|
87
|
+
type: "string",
|
|
88
|
+
enum: ["organization", "workspace", "personal"],
|
|
89
|
+
},
|
|
90
|
+
authorityAccountId: { type: "string", minLength: 1, maxLength: 128 },
|
|
91
|
+
authorityWorkspaceId: { type: "string", minLength: 1, maxLength: 128 },
|
|
92
|
+
authoritySubjectId: { type: "string", minLength: 1, maxLength: 512 },
|
|
93
|
+
collectionId: { type: "string", minLength: 1, maxLength: 512 },
|
|
94
|
+
},
|
|
95
|
+
additionalProperties: false,
|
|
96
|
+
},
|
|
97
|
+
syncCadence: { type: "string", enum: ["manual", "hourly", "daily"] },
|
|
98
|
+
readPolicy: { type: "string", enum: ["allow", "ask", "block"] },
|
|
99
|
+
},
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
},
|
|
102
|
+
capabilities: {
|
|
103
|
+
provider,
|
|
104
|
+
connectionRequired: true,
|
|
105
|
+
sync: "incremental",
|
|
106
|
+
cursor: provider === "google-drive" ? "page_token" : "delta_link",
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
const mailboxFeatures = (
|
|
111
|
+
provider: "google-gmail" | "microsoft-outlook-mail",
|
|
112
|
+
): readonly IntegrationFeatureDefinition[] => [
|
|
113
|
+
{
|
|
114
|
+
featureKey: "mail-inbox",
|
|
115
|
+
kind: "inbound_trigger",
|
|
116
|
+
configSchema: {
|
|
117
|
+
type: "object",
|
|
118
|
+
properties: {
|
|
119
|
+
folder: { type: "string", minLength: 1, maxLength: 256 },
|
|
120
|
+
unreadOnly: { type: "boolean" },
|
|
121
|
+
},
|
|
122
|
+
additionalProperties: false,
|
|
123
|
+
},
|
|
124
|
+
capabilities: {
|
|
125
|
+
provider,
|
|
126
|
+
connectionRequired: true,
|
|
127
|
+
delivery: "poll",
|
|
128
|
+
cursor: provider === "google-gmail" ? "history_id" : "delta_link",
|
|
129
|
+
},
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
featureKey: "mail-delivery",
|
|
133
|
+
kind: "delivery_destination",
|
|
134
|
+
configSchema: {
|
|
135
|
+
type: "object",
|
|
136
|
+
properties: {
|
|
137
|
+
fromAlias: { type: "string", minLength: 1, maxLength: 512 },
|
|
138
|
+
saveToSent: { type: "boolean" },
|
|
139
|
+
},
|
|
140
|
+
additionalProperties: false,
|
|
141
|
+
},
|
|
142
|
+
capabilities: {
|
|
143
|
+
provider,
|
|
144
|
+
connectionRequired: true,
|
|
145
|
+
delivery: "email",
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
accountIdentityFeature(provider === "google-gmail" ? "google" : "microsoft"),
|
|
149
|
+
];
|
|
150
|
+
|
|
151
|
+
const googleDiscoveryUrl = (service: string, version: string): string =>
|
|
152
|
+
`https://www.googleapis.com/discovery/v1/apis/${service}/${version}/rest`;
|
|
153
|
+
|
|
154
|
+
const googleOAuth = (scopes: readonly string[]): OAuthProviderPreset => ({
|
|
155
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
156
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
157
|
+
scopes: ["openid", "email", "profile", ...scopes],
|
|
158
|
+
tokenPlacement: { carrier: "header", name: "Authorization", prefix: "Bearer " },
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
export const GOOGLE_DRIVE_PRESET: OpenApiProviderPreset = {
|
|
162
|
+
id: "google-drive",
|
|
163
|
+
name: "Google Drive",
|
|
164
|
+
summary: "Files, folders, permissions, and shared drives.",
|
|
165
|
+
family: "google",
|
|
166
|
+
sourceFormat: "google-discovery",
|
|
167
|
+
sourceUrl: googleDiscoveryUrl("drive", "v3"),
|
|
168
|
+
baseUrl: "https://www.googleapis.com/drive/v3/",
|
|
169
|
+
oauth: googleOAuth(["https://www.googleapis.com/auth/drive"]),
|
|
170
|
+
healthOperation: "drive.about.get",
|
|
171
|
+
healthArgs: { query: { fields: "user" } },
|
|
172
|
+
features: [driveKnowledgeFeature("google-drive"), accountIdentityFeature("google")],
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
export const GOOGLE_GMAIL_PRESET: OpenApiProviderPreset = {
|
|
176
|
+
id: "google-gmail",
|
|
177
|
+
name: "Gmail",
|
|
178
|
+
summary: "Messages, threads, labels, drafts, and sending mail.",
|
|
179
|
+
family: "google",
|
|
180
|
+
sourceFormat: "google-discovery",
|
|
181
|
+
sourceUrl: googleDiscoveryUrl("gmail", "v1"),
|
|
182
|
+
baseUrl: "https://gmail.googleapis.com/",
|
|
183
|
+
oauth: googleOAuth(["https://mail.google.com/"]),
|
|
184
|
+
healthOperation: "gmail.users.labels.list",
|
|
185
|
+
healthArgs: { path: { userId: "me" } },
|
|
186
|
+
features: mailboxFeatures("google-gmail"),
|
|
187
|
+
};
|
|
188
|
+
|
|
189
|
+
export const MICROSOFT_GRAPH_OPENAPI_URL =
|
|
190
|
+
"https://raw.githubusercontent.com/microsoftgraph/msgraph-metadata/master/openapi/v1.0/openapi.yaml";
|
|
191
|
+
export const MICROSOFT_GRAPH_BASE_URL = "https://graph.microsoft.com/v1.0";
|
|
192
|
+
|
|
193
|
+
const microsoftOAuth = (scopes: readonly string[]): OAuthProviderPreset => ({
|
|
194
|
+
authorizationUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/authorize",
|
|
195
|
+
tokenUrl: "https://login.microsoftonline.com/common/oauth2/v2.0/token",
|
|
196
|
+
scopes: ["offline_access", "User.Read", ...scopes],
|
|
197
|
+
tokenPlacement: { carrier: "header", name: "Authorization", prefix: "Bearer " },
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
export const MICROSOFT_OUTLOOK_MAIL_PRESET: OpenApiProviderPreset = {
|
|
201
|
+
id: "microsoft-outlook-mail",
|
|
202
|
+
name: "Outlook Mail",
|
|
203
|
+
summary: "Messages, folders, attachments, settings, and sending mail.",
|
|
204
|
+
family: "microsoft",
|
|
205
|
+
sourceFormat: "openapi",
|
|
206
|
+
sourceUrl: MICROSOFT_GRAPH_OPENAPI_URL,
|
|
207
|
+
baseUrl: MICROSOFT_GRAPH_BASE_URL,
|
|
208
|
+
oauth: microsoftOAuth(["Mail.ReadWrite", "Mail.Send", "MailboxSettings.ReadWrite"]),
|
|
209
|
+
pathPrefixes: [
|
|
210
|
+
"/me/messages",
|
|
211
|
+
"/me/mailFolders",
|
|
212
|
+
"/me/sendMail",
|
|
213
|
+
"/me/getMailTips",
|
|
214
|
+
"/me/inferenceClassification",
|
|
215
|
+
"/me/mailboxSettings",
|
|
216
|
+
"/me/outlook",
|
|
217
|
+
],
|
|
218
|
+
features: mailboxFeatures("microsoft-outlook-mail"),
|
|
219
|
+
};
|
|
220
|
+
|
|
221
|
+
export const MICROSOFT_OUTLOOK_CALENDAR_PRESET: OpenApiProviderPreset = {
|
|
222
|
+
id: "microsoft-outlook-calendar",
|
|
223
|
+
name: "Outlook Calendar",
|
|
224
|
+
summary: "Calendars, events, availability, and scheduling.",
|
|
225
|
+
family: "microsoft",
|
|
226
|
+
sourceFormat: "openapi",
|
|
227
|
+
sourceUrl: MICROSOFT_GRAPH_OPENAPI_URL,
|
|
228
|
+
baseUrl: MICROSOFT_GRAPH_BASE_URL,
|
|
229
|
+
oauth: microsoftOAuth(["Calendars.ReadWrite"]),
|
|
230
|
+
pathPrefixes: [
|
|
231
|
+
"/me/calendar",
|
|
232
|
+
"/me/calendars",
|
|
233
|
+
"/me/calendarGroups",
|
|
234
|
+
"/me/calendarView",
|
|
235
|
+
"/me/events",
|
|
236
|
+
"/me/findMeetingTimes",
|
|
237
|
+
"/me/reminderView",
|
|
238
|
+
],
|
|
239
|
+
features: [
|
|
240
|
+
{
|
|
241
|
+
featureKey: "calendar-events",
|
|
242
|
+
kind: "inbound_trigger",
|
|
243
|
+
configSchema: {
|
|
244
|
+
type: "object",
|
|
245
|
+
properties: {
|
|
246
|
+
calendarId: { type: "string", minLength: 1, maxLength: 512 },
|
|
247
|
+
lookaheadDays: { type: "integer", minimum: 1, maximum: 365 },
|
|
248
|
+
},
|
|
249
|
+
additionalProperties: false,
|
|
250
|
+
},
|
|
251
|
+
capabilities: {
|
|
252
|
+
provider: "microsoft-outlook-calendar",
|
|
253
|
+
connectionRequired: true,
|
|
254
|
+
delivery: "poll",
|
|
255
|
+
cursor: "delta_link",
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
featureKey: "calendar-delivery",
|
|
260
|
+
kind: "delivery_destination",
|
|
261
|
+
configSchema: {
|
|
262
|
+
type: "object",
|
|
263
|
+
properties: {
|
|
264
|
+
calendarId: { type: "string", minLength: 1, maxLength: 512 },
|
|
265
|
+
},
|
|
266
|
+
additionalProperties: false,
|
|
267
|
+
},
|
|
268
|
+
capabilities: {
|
|
269
|
+
provider: "microsoft-outlook-calendar",
|
|
270
|
+
connectionRequired: true,
|
|
271
|
+
delivery: "calendar_event",
|
|
272
|
+
},
|
|
273
|
+
},
|
|
274
|
+
accountIdentityFeature("microsoft"),
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
export const MICROSOFT_OUTLOOK_CONTACTS_PRESET: OpenApiProviderPreset = {
|
|
279
|
+
id: "microsoft-outlook-contacts",
|
|
280
|
+
name: "Outlook Contacts",
|
|
281
|
+
summary: "Contacts, contact folders, and people suggestions.",
|
|
282
|
+
family: "microsoft",
|
|
283
|
+
sourceFormat: "openapi",
|
|
284
|
+
sourceUrl: MICROSOFT_GRAPH_OPENAPI_URL,
|
|
285
|
+
baseUrl: MICROSOFT_GRAPH_BASE_URL,
|
|
286
|
+
oauth: microsoftOAuth(["Contacts.ReadWrite", "People.Read.All"]),
|
|
287
|
+
pathPrefixes: ["/me/contacts", "/me/contactFolders", "/me/people"],
|
|
288
|
+
features: [accountIdentityFeature("microsoft")],
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
export const MICROSOFT_ONEDRIVE_PRESET: OpenApiProviderPreset = {
|
|
292
|
+
id: "microsoft-onedrive",
|
|
293
|
+
name: "OneDrive",
|
|
294
|
+
summary: "Drives, files, folders, sharing links, and permissions.",
|
|
295
|
+
family: "microsoft",
|
|
296
|
+
sourceFormat: "openapi",
|
|
297
|
+
sourceUrl: MICROSOFT_GRAPH_OPENAPI_URL,
|
|
298
|
+
baseUrl: MICROSOFT_GRAPH_BASE_URL,
|
|
299
|
+
oauth: microsoftOAuth(["Files.ReadWrite.All", "Sites.ReadWrite.All"]),
|
|
300
|
+
pathPrefixes: ["/me/drive", "/me/drives", "/me/followedSites", "/drives", "/shares"],
|
|
301
|
+
features: [driveKnowledgeFeature("microsoft-onedrive"), accountIdentityFeature("microsoft")],
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
export const CORE_PROVIDER_PRESETS: readonly OpenApiProviderPreset[] = [
|
|
305
|
+
GOOGLE_DRIVE_PRESET,
|
|
306
|
+
GOOGLE_GMAIL_PRESET,
|
|
307
|
+
MICROSOFT_OUTLOOK_MAIL_PRESET,
|
|
308
|
+
MICROSOFT_OUTLOOK_CALENDAR_PRESET,
|
|
309
|
+
MICROSOFT_OUTLOOK_CONTACTS_PRESET,
|
|
310
|
+
MICROSOFT_ONEDRIVE_PRESET,
|
|
311
|
+
];
|
|
312
|
+
|
|
313
|
+
export function providerPresetById(id: string): OpenApiProviderPreset | undefined {
|
|
314
|
+
return CORE_PROVIDER_PRESETS.find((preset) => preset.id === id);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
export function providerDomainForPreset(preset: OpenApiProviderPreset): string {
|
|
318
|
+
return new URL(preset.baseUrl).hostname.toLowerCase();
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
export function integrationFeaturesForPreset(
|
|
322
|
+
presetId: string | null | undefined,
|
|
323
|
+
): readonly IntegrationFeatureDefinition[] {
|
|
324
|
+
return presetId ? (providerPresetById(presetId)?.features ?? []) : [];
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
export function filterOpenApiDocumentForPreset(
|
|
328
|
+
document: Record<string, unknown>,
|
|
329
|
+
preset: OpenApiProviderPreset,
|
|
330
|
+
): Record<string, unknown> {
|
|
331
|
+
if (!preset.pathPrefixes?.length) return document;
|
|
332
|
+
if (!isRecord(document.paths)) {
|
|
333
|
+
throw new IntegrationProtocolError("openapi_paths", "OpenAPI document has no paths object");
|
|
334
|
+
}
|
|
335
|
+
const paths = Object.fromEntries(
|
|
336
|
+
Object.entries(document.paths).filter(([path]) =>
|
|
337
|
+
preset.pathPrefixes!.some((prefix) => path === prefix || path.startsWith(`${prefix}/`)),
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
if (Object.keys(paths).length === 0) {
|
|
341
|
+
throw new IntegrationProtocolError(
|
|
342
|
+
"provider_preset_empty",
|
|
343
|
+
`${preset.name} did not match any operations in the supplied OpenAPI document`,
|
|
344
|
+
);
|
|
345
|
+
}
|
|
346
|
+
return {
|
|
347
|
+
...document,
|
|
348
|
+
paths,
|
|
349
|
+
...(preset.baseUrl ? { servers: [{ url: preset.baseUrl }] } : {}),
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export function googleDiscoveryToOpenApi(discovery: unknown): Record<string, unknown> {
|
|
354
|
+
if (!isRecord(discovery)) {
|
|
355
|
+
throw new IntegrationProtocolError(
|
|
356
|
+
"google_discovery_shape",
|
|
357
|
+
"Google Discovery document is invalid",
|
|
358
|
+
);
|
|
359
|
+
}
|
|
360
|
+
const rootUrl = stringValue(discovery.rootUrl) ?? stringValue(discovery.baseUrl);
|
|
361
|
+
const servicePath = stringValue(discovery.servicePath) ?? "";
|
|
362
|
+
if (!rootUrl || !URL.canParse(rootUrl)) {
|
|
363
|
+
throw new IntegrationProtocolError(
|
|
364
|
+
"google_discovery_server",
|
|
365
|
+
"Google Discovery document has no valid root URL",
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
const paths: Record<string, unknown> = {};
|
|
369
|
+
collectGoogleMethods(discovery, discovery.methods, paths);
|
|
370
|
+
collectGoogleResources(discovery, discovery.resources, paths);
|
|
371
|
+
if (Object.keys(paths).length === 0) {
|
|
372
|
+
throw new IntegrationProtocolError(
|
|
373
|
+
"google_discovery_empty",
|
|
374
|
+
"Google Discovery document exposes no methods",
|
|
375
|
+
);
|
|
376
|
+
}
|
|
377
|
+
const scopes =
|
|
378
|
+
isRecord(discovery.auth) && isRecord(discovery.auth.oauth2)
|
|
379
|
+
? discovery.auth.oauth2.scopes
|
|
380
|
+
: undefined;
|
|
381
|
+
const scopeMap = isRecord(scopes)
|
|
382
|
+
? Object.fromEntries(
|
|
383
|
+
Object.entries(scopes).map(([scope, value]) => [
|
|
384
|
+
scope,
|
|
385
|
+
isRecord(value) && typeof value.description === "string" ? value.description : "",
|
|
386
|
+
]),
|
|
387
|
+
)
|
|
388
|
+
: {};
|
|
389
|
+
return {
|
|
390
|
+
openapi: "3.1.0",
|
|
391
|
+
info: {
|
|
392
|
+
title: stringValue(discovery.title) ?? stringValue(discovery.name) ?? "Google API",
|
|
393
|
+
description: stringValue(discovery.description) ?? "Google Discovery API",
|
|
394
|
+
version: stringValue(discovery.version) ?? "v1",
|
|
395
|
+
},
|
|
396
|
+
servers: [{ url: new URL(servicePath, rootUrl).toString() }],
|
|
397
|
+
paths,
|
|
398
|
+
components: {
|
|
399
|
+
schemas: Object.fromEntries(
|
|
400
|
+
Object.entries(isRecord(discovery.schemas) ? discovery.schemas : {}).map(
|
|
401
|
+
([name, schema]) => [name, convertGoogleSchema(schema)],
|
|
402
|
+
),
|
|
403
|
+
),
|
|
404
|
+
securitySchemes: {
|
|
405
|
+
googleOAuth2: {
|
|
406
|
+
type: "oauth2",
|
|
407
|
+
flows: {
|
|
408
|
+
authorizationCode: {
|
|
409
|
+
authorizationUrl: "https://accounts.google.com/o/oauth2/v2/auth",
|
|
410
|
+
tokenUrl: "https://oauth2.googleapis.com/token",
|
|
411
|
+
scopes: scopeMap,
|
|
412
|
+
},
|
|
413
|
+
},
|
|
414
|
+
},
|
|
415
|
+
},
|
|
416
|
+
},
|
|
417
|
+
security: Object.keys(scopeMap).length > 0 ? [{ googleOAuth2: [] }] : [],
|
|
418
|
+
};
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
function collectGoogleResources(
|
|
422
|
+
document: Record<string, unknown>,
|
|
423
|
+
value: unknown,
|
|
424
|
+
paths: Record<string, unknown>,
|
|
425
|
+
): void {
|
|
426
|
+
if (!isRecord(value)) return;
|
|
427
|
+
for (const resource of Object.values(value)) {
|
|
428
|
+
if (!isRecord(resource)) continue;
|
|
429
|
+
collectGoogleMethods(document, resource.methods, paths);
|
|
430
|
+
collectGoogleResources(document, resource.resources, paths);
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
function collectGoogleMethods(
|
|
435
|
+
document: Record<string, unknown>,
|
|
436
|
+
value: unknown,
|
|
437
|
+
paths: Record<string, unknown>,
|
|
438
|
+
): void {
|
|
439
|
+
if (!isRecord(value)) return;
|
|
440
|
+
for (const [fallbackId, rawMethod] of Object.entries(value)) {
|
|
441
|
+
if (!isRecord(rawMethod)) continue;
|
|
442
|
+
const path = stringValue(rawMethod.path);
|
|
443
|
+
const httpMethod = stringValue(rawMethod.httpMethod)?.toLowerCase();
|
|
444
|
+
if (!path || !httpMethod) continue;
|
|
445
|
+
const parameters = Object.entries(
|
|
446
|
+
isRecord(rawMethod.parameters) ? rawMethod.parameters : {},
|
|
447
|
+
).flatMap(([name, rawParameter]): Record<string, unknown>[] => {
|
|
448
|
+
if (!isRecord(rawParameter)) return [];
|
|
449
|
+
const location = rawParameter.location === "path" ? "path" : "query";
|
|
450
|
+
return [
|
|
451
|
+
{
|
|
452
|
+
name,
|
|
453
|
+
in: location,
|
|
454
|
+
required: location === "path" || rawParameter.required === true,
|
|
455
|
+
...(stringValue(rawParameter.description)
|
|
456
|
+
? { description: stringValue(rawParameter.description) }
|
|
457
|
+
: {}),
|
|
458
|
+
schema: convertGoogleSchema(rawParameter),
|
|
459
|
+
},
|
|
460
|
+
];
|
|
461
|
+
});
|
|
462
|
+
const requestRef = isRecord(rawMethod.request)
|
|
463
|
+
? stringValue(rawMethod.request.$ref)
|
|
464
|
+
: undefined;
|
|
465
|
+
const responseRef = isRecord(rawMethod.response)
|
|
466
|
+
? stringValue(rawMethod.response.$ref)
|
|
467
|
+
: undefined;
|
|
468
|
+
const operation: Record<string, unknown> = {
|
|
469
|
+
operationId: stringValue(rawMethod.id) ?? fallbackId,
|
|
470
|
+
summary: stringValue(rawMethod.description) ?? stringValue(rawMethod.id) ?? fallbackId,
|
|
471
|
+
description: stringValue(rawMethod.description),
|
|
472
|
+
parameters,
|
|
473
|
+
responses: {
|
|
474
|
+
"200": {
|
|
475
|
+
description: "Successful response",
|
|
476
|
+
...(responseRef
|
|
477
|
+
? {
|
|
478
|
+
content: {
|
|
479
|
+
"application/json": {
|
|
480
|
+
schema: { $ref: `#/components/schemas/${escapeJsonPointer(responseRef)}` },
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
}
|
|
484
|
+
: {}),
|
|
485
|
+
},
|
|
486
|
+
},
|
|
487
|
+
...(Array.isArray(rawMethod.scopes) && rawMethod.scopes.length > 0
|
|
488
|
+
? { security: [{ googleOAuth2: rawMethod.scopes }] }
|
|
489
|
+
: {}),
|
|
490
|
+
};
|
|
491
|
+
if (requestRef) {
|
|
492
|
+
operation.requestBody = {
|
|
493
|
+
required: true,
|
|
494
|
+
content: {
|
|
495
|
+
"application/json": {
|
|
496
|
+
schema: { $ref: `#/components/schemas/${escapeJsonPointer(requestRef)}` },
|
|
497
|
+
},
|
|
498
|
+
},
|
|
499
|
+
};
|
|
500
|
+
}
|
|
501
|
+
const normalizedPath = path.startsWith("/") ? path : `/${path}`;
|
|
502
|
+
const existing = isRecord(paths[normalizedPath]) ? paths[normalizedPath] : {};
|
|
503
|
+
paths[normalizedPath] = { ...existing, [httpMethod]: operation };
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function convertGoogleSchema(value: unknown, depth = 0): Record<string, unknown> {
|
|
508
|
+
if (!isRecord(value) || depth > 20) return {};
|
|
509
|
+
if (typeof value.$ref === "string") {
|
|
510
|
+
return { $ref: `#/components/schemas/${escapeJsonPointer(value.$ref)}` };
|
|
511
|
+
}
|
|
512
|
+
const result: Record<string, unknown> = {};
|
|
513
|
+
const type = stringValue(value.type);
|
|
514
|
+
if (type) result.type = type === "any" ? undefined : type;
|
|
515
|
+
for (const key of [
|
|
516
|
+
"description",
|
|
517
|
+
"format",
|
|
518
|
+
"pattern",
|
|
519
|
+
"minimum",
|
|
520
|
+
"maximum",
|
|
521
|
+
"default",
|
|
522
|
+
] as const) {
|
|
523
|
+
if (value[key] !== undefined) result[key] = value[key];
|
|
524
|
+
}
|
|
525
|
+
if (Array.isArray(value.enum)) result.enum = value.enum;
|
|
526
|
+
if (isRecord(value.properties)) {
|
|
527
|
+
result.type = result.type ?? "object";
|
|
528
|
+
result.properties = Object.fromEntries(
|
|
529
|
+
Object.entries(value.properties).map(([name, schema]) => [
|
|
530
|
+
name,
|
|
531
|
+
convertGoogleSchema(schema, depth + 1),
|
|
532
|
+
]),
|
|
533
|
+
);
|
|
534
|
+
}
|
|
535
|
+
if (value.items !== undefined) {
|
|
536
|
+
result.type = result.type ?? "array";
|
|
537
|
+
result.items = convertGoogleSchema(value.items, depth + 1);
|
|
538
|
+
}
|
|
539
|
+
if (value.additionalProperties !== undefined) {
|
|
540
|
+
result.additionalProperties =
|
|
541
|
+
value.additionalProperties === true
|
|
542
|
+
? true
|
|
543
|
+
: convertGoogleSchema(value.additionalProperties, depth + 1);
|
|
544
|
+
}
|
|
545
|
+
if (Array.isArray(value.required)) result.required = value.required;
|
|
546
|
+
return Object.fromEntries(Object.entries(result).filter(([, entry]) => entry !== undefined));
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
function escapeJsonPointer(value: string): string {
|
|
550
|
+
return value.replaceAll("~", "~0").replaceAll("/", "~1");
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function stringValue(value: unknown): string | undefined {
|
|
554
|
+
return typeof value === "string" && value.trim() ? value : undefined;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
558
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
559
|
+
}
|
package/src/revision.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
|
|
3
|
+
function canonicalize(value: unknown): unknown {
|
|
4
|
+
if (Array.isArray(value)) return value.map(canonicalize);
|
|
5
|
+
if (!value || typeof value !== "object") return value;
|
|
6
|
+
return Object.fromEntries(
|
|
7
|
+
Object.entries(value as Record<string, unknown>)
|
|
8
|
+
.sort(([left], [right]) => left.localeCompare(right))
|
|
9
|
+
.map(([key, entry]) => [key, canonicalize(entry)]),
|
|
10
|
+
);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function canonicalJson(value: unknown): string {
|
|
14
|
+
return JSON.stringify(canonicalize(value));
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function sha256Hex(value: string | Uint8Array): string {
|
|
18
|
+
return createHash("sha256").update(value).digest("hex");
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function immutableRevisionId(protocol: string, contentSha256: string): string {
|
|
22
|
+
if (!/^[a-f0-9]{64}$/.test(contentSha256)) {
|
|
23
|
+
throw new Error("contentSha256 must be a lowercase SHA-256 digest");
|
|
24
|
+
}
|
|
25
|
+
return `${protocol}:${contentSha256.slice(0, 24)}`;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export function stableToolId(value: string, seen?: Map<string, number>): string {
|
|
29
|
+
const normalized = value
|
|
30
|
+
.trim()
|
|
31
|
+
.toLowerCase()
|
|
32
|
+
.replace(/[^a-z0-9]+/g, "_")
|
|
33
|
+
.replace(/^_+|_+$/g, "")
|
|
34
|
+
.slice(0, 54);
|
|
35
|
+
const base = normalized || "tool";
|
|
36
|
+
if (!seen) return base;
|
|
37
|
+
const count = (seen.get(base) ?? 0) + 1;
|
|
38
|
+
seen.set(base, count);
|
|
39
|
+
return count === 1 ? base : `${base}_${count}`;
|
|
40
|
+
}
|