@agent-native/core 0.32.17 → 0.34.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1533 @@
1
+ import { createHash } from "node:crypto";
2
+ import { resolveCredential, } from "../credentials/index.js";
3
+ import { createSsrfSafeDispatcher, isBlockedExtensionUrlWithDns, } from "../extensions/url-safety.js";
4
+ import { listOAuthAccountsByOwner, saveOAuthTokens, } from "../oauth-tokens/index.js";
5
+ import { getCredentialContext } from "../server/request-context.js";
6
+ import { resolveWorkspaceConnectionCredentialForApp } from "../workspace-connections/credentials.js";
7
+ export const PROVIDER_API_IDS = [
8
+ "amplitude",
9
+ "apollo",
10
+ "bigquery",
11
+ "commonroom",
12
+ "dataforseo",
13
+ "ga4",
14
+ "gcloud",
15
+ "github",
16
+ "gmail",
17
+ "gong",
18
+ "google_calendar",
19
+ "google_drive",
20
+ "granola",
21
+ "grafana",
22
+ "hubspot",
23
+ "jira",
24
+ "mixpanel",
25
+ "notion",
26
+ "posthog",
27
+ "prometheus",
28
+ "pylon",
29
+ "sentry",
30
+ "slack",
31
+ "stripe",
32
+ "twitter",
33
+ ];
34
+ const DEFAULT_TIMEOUT_MS = 30_000;
35
+ const MAX_TIMEOUT_MS = 120_000;
36
+ const DEFAULT_MAX_BYTES = 1024 * 1024;
37
+ const MAX_MAX_BYTES = 4 * 1024 * 1024;
38
+ const HEADER_NAME_RE = /^[!#$%&'*+.^_`|~0-9A-Za-z-]+$/;
39
+ const BLOCKED_OUTBOUND_HEADERS = new Set([
40
+ "connection",
41
+ "content-length",
42
+ "cookie",
43
+ "forwarded",
44
+ "host",
45
+ "keep-alive",
46
+ "origin",
47
+ "proxy-authenticate",
48
+ "proxy-authorization",
49
+ "referer",
50
+ "set-cookie",
51
+ "te",
52
+ "trailer",
53
+ "transfer-encoding",
54
+ "upgrade",
55
+ "x-forwarded-for",
56
+ "x-forwarded-host",
57
+ "x-forwarded-proto",
58
+ ]);
59
+ const PROVIDER_CONFIGS = {
60
+ amplitude: {
61
+ id: "amplitude",
62
+ label: "Amplitude",
63
+ defaultBaseUrl: "https://amplitude.com/api/2",
64
+ auth: {
65
+ type: "basic",
66
+ usernameKey: "AMPLITUDE_API_KEY",
67
+ passwordKey: "AMPLITUDE_SECRET_KEY",
68
+ },
69
+ credentialKeys: ["AMPLITUDE_API_KEY", "AMPLITUDE_SECRET_KEY"],
70
+ docsUrls: ["https://amplitude.com/docs/apis"],
71
+ allowedHostSuffixes: ["amplitude.com"],
72
+ templateUses: ["analytics"],
73
+ examples: [
74
+ {
75
+ label: "Export events",
76
+ method: "GET",
77
+ path: "/export?start=20260601T00&end=20260602T00",
78
+ },
79
+ ],
80
+ },
81
+ apollo: {
82
+ id: "apollo",
83
+ label: "Apollo",
84
+ defaultBaseUrl: "https://api.apollo.io",
85
+ auth: {
86
+ type: "api-key-header",
87
+ key: "APOLLO_API_KEY",
88
+ header: "x-api-key",
89
+ },
90
+ credentialKeys: ["APOLLO_API_KEY"],
91
+ docsUrls: ["https://docs.apollo.io/reference/api-reference"],
92
+ templateUses: ["analytics"],
93
+ examples: [
94
+ {
95
+ label: "Search people",
96
+ method: "POST",
97
+ path: "/api/v1/mixed_people/search",
98
+ body: { q_keywords: "vp marketing", page: 1, per_page: 10 },
99
+ },
100
+ ],
101
+ },
102
+ bigquery: {
103
+ id: "bigquery",
104
+ label: "BigQuery REST API",
105
+ defaultBaseUrl: "https://bigquery.googleapis.com/bigquery/v2",
106
+ auth: {
107
+ type: "google-service-account",
108
+ scopes: [
109
+ "https://www.googleapis.com/auth/cloud-platform",
110
+ "https://www.googleapis.com/auth/bigquery",
111
+ ],
112
+ },
113
+ credentialKeys: [
114
+ "GOOGLE_APPLICATION_CREDENTIALS_JSON",
115
+ "BIGQUERY_PROJECT_ID",
116
+ ],
117
+ docsUrls: ["https://cloud.google.com/bigquery/docs/reference/rest"],
118
+ specUrls: ["https://bigquery.googleapis.com/$discovery/rest?version=v2"],
119
+ allowedHostSuffixes: ["googleapis.com"],
120
+ templateUses: ["analytics"],
121
+ placeholders: [
122
+ {
123
+ name: "projectId",
124
+ credentialKey: "BIGQUERY_PROJECT_ID",
125
+ label: "Configured BigQuery project ID",
126
+ },
127
+ ],
128
+ examples: [
129
+ {
130
+ label: "List datasets",
131
+ method: "GET",
132
+ path: "/projects/{projectId}/datasets",
133
+ },
134
+ {
135
+ label: "Run query",
136
+ method: "POST",
137
+ path: "/projects/{projectId}/queries",
138
+ body: { query: "SELECT 1", useLegacySql: false },
139
+ },
140
+ ],
141
+ },
142
+ commonroom: {
143
+ id: "commonroom",
144
+ label: "Common Room",
145
+ defaultBaseUrl: "https://api.commonroom.io/community/v1",
146
+ auth: {
147
+ type: "bearer",
148
+ keys: ["COMMONROOM_API_TOKEN"],
149
+ },
150
+ credentialKeys: ["COMMONROOM_API_TOKEN"],
151
+ docsUrls: ["https://developer.commonroom.io/reference/overview"],
152
+ templateUses: ["analytics"],
153
+ examples: [{ label: "List members", method: "GET", path: "/members" }],
154
+ },
155
+ dataforseo: {
156
+ id: "dataforseo",
157
+ label: "DataForSEO",
158
+ defaultBaseUrl: "https://api.dataforseo.com/v3",
159
+ auth: {
160
+ type: "basic",
161
+ usernameKey: "DATAFORSEO_LOGIN",
162
+ passwordKey: "DATAFORSEO_PASSWORD",
163
+ },
164
+ credentialKeys: ["DATAFORSEO_LOGIN", "DATAFORSEO_PASSWORD"],
165
+ docsUrls: ["https://docs.dataforseo.com/v3/"],
166
+ templateUses: ["analytics"],
167
+ examples: [
168
+ {
169
+ label: "SERP task post",
170
+ method: "POST",
171
+ path: "/serp/google/organic/task_post",
172
+ body: [
173
+ { keyword: "builder.io", location_code: 2840, language_code: "en" },
174
+ ],
175
+ },
176
+ ],
177
+ },
178
+ ga4: {
179
+ id: "ga4",
180
+ label: "Google Analytics Data API",
181
+ defaultBaseUrl: "https://analyticsdata.googleapis.com/v1beta",
182
+ auth: {
183
+ type: "google-service-account",
184
+ scopes: ["https://www.googleapis.com/auth/analytics.readonly"],
185
+ },
186
+ credentialKeys: ["GOOGLE_APPLICATION_CREDENTIALS_JSON", "GA4_PROPERTY_ID"],
187
+ docsUrls: [
188
+ "https://developers.google.com/analytics/devguides/reporting/data/v1/rest",
189
+ ],
190
+ specUrls: [
191
+ "https://analyticsdata.googleapis.com/$discovery/rest?version=v1beta",
192
+ ],
193
+ allowedHostSuffixes: ["googleapis.com"],
194
+ templateUses: ["analytics"],
195
+ placeholders: [
196
+ {
197
+ name: "propertyId",
198
+ credentialKey: "GA4_PROPERTY_ID",
199
+ label: "Configured GA4 property ID",
200
+ },
201
+ ],
202
+ examples: [
203
+ {
204
+ label: "Run report",
205
+ method: "POST",
206
+ path: "/properties/{propertyId}:runReport",
207
+ body: {
208
+ dateRanges: [{ startDate: "30daysAgo", endDate: "today" }],
209
+ metrics: [{ name: "activeUsers" }],
210
+ },
211
+ },
212
+ ],
213
+ },
214
+ gcloud: {
215
+ id: "gcloud",
216
+ label: "Google Cloud APIs",
217
+ defaultBaseUrl: "https://cloudresourcemanager.googleapis.com",
218
+ auth: {
219
+ type: "google-service-account",
220
+ scopes: [
221
+ "https://www.googleapis.com/auth/cloud-platform",
222
+ "https://www.googleapis.com/auth/monitoring.read",
223
+ "https://www.googleapis.com/auth/logging.read",
224
+ "https://www.googleapis.com/auth/bigquery",
225
+ ],
226
+ },
227
+ credentialKeys: [
228
+ "GOOGLE_APPLICATION_CREDENTIALS_JSON",
229
+ "BIGQUERY_PROJECT_ID",
230
+ ],
231
+ docsUrls: ["https://cloud.google.com/apis/docs/overview"],
232
+ specUrls: ["https://www.googleapis.com/discovery/v1/apis"],
233
+ allowedHostSuffixes: ["googleapis.com"],
234
+ templateUses: ["analytics"],
235
+ placeholders: [
236
+ {
237
+ name: "projectId",
238
+ credentialKey: "BIGQUERY_PROJECT_ID",
239
+ label: "Configured Google Cloud project ID",
240
+ },
241
+ ],
242
+ examples: [
243
+ {
244
+ label: "Get project",
245
+ method: "GET",
246
+ path: "https://cloudresourcemanager.googleapis.com/v1/projects/{projectId}",
247
+ },
248
+ ],
249
+ },
250
+ github: {
251
+ id: "github",
252
+ label: "GitHub REST API",
253
+ defaultBaseUrl: "https://api.github.com",
254
+ auth: {
255
+ type: "bearer",
256
+ keys: ["GITHUB_TOKEN"],
257
+ workspaceProvider: "github",
258
+ },
259
+ credentialKeys: ["GITHUB_TOKEN"],
260
+ docsUrls: ["https://docs.github.com/rest"],
261
+ specUrls: [
262
+ "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/api.github.com.json",
263
+ ],
264
+ defaultHeaders: {
265
+ Accept: "application/vnd.github+json",
266
+ "X-GitHub-Api-Version": "2022-11-28",
267
+ },
268
+ templateUses: ["analytics", "brain", "dispatch"],
269
+ examples: [
270
+ { label: "Authenticated user", method: "GET", path: "/user" },
271
+ { label: "Search issues", method: "GET", path: "/search/issues" },
272
+ ],
273
+ },
274
+ gmail: {
275
+ id: "gmail",
276
+ label: "Gmail API",
277
+ defaultBaseUrl: "https://gmail.googleapis.com/gmail/v1",
278
+ auth: {
279
+ type: "oauth-bearer",
280
+ oauthProvider: "google",
281
+ tokenLabel: "Google OAuth token",
282
+ },
283
+ credentialKeys: ["GOOGLE_OAUTH_ACCOUNT"],
284
+ docsUrls: ["https://developers.google.com/gmail/api/reference/rest"],
285
+ specUrls: ["https://gmail.googleapis.com/$discovery/rest?version=v1"],
286
+ allowedHostSuffixes: ["googleapis.com"],
287
+ templateUses: ["brain", "mail", "dispatch"],
288
+ examples: [
289
+ {
290
+ label: "List messages",
291
+ method: "GET",
292
+ path: "/users/me/messages",
293
+ },
294
+ {
295
+ label: "Search messages",
296
+ method: "GET",
297
+ path: "/users/me/messages",
298
+ body: undefined,
299
+ },
300
+ ],
301
+ notes: [
302
+ "Uses the current user's stored Google OAuth account. Pass accountId when the user has multiple Google accounts connected.",
303
+ ],
304
+ },
305
+ gong: {
306
+ id: "gong",
307
+ label: "Gong",
308
+ defaultBaseUrl: "https://api.gong.io/v2",
309
+ baseUrlCredentialKey: "GONG_API_BASE",
310
+ auth: {
311
+ type: "basic",
312
+ usernameKey: "GONG_ACCESS_KEY",
313
+ passwordKey: "GONG_ACCESS_SECRET",
314
+ },
315
+ credentialKeys: ["GONG_ACCESS_KEY", "GONG_ACCESS_SECRET", "GONG_API_BASE"],
316
+ docsUrls: ["https://gong.app.gong.io/settings/api/documentation"],
317
+ templateUses: ["analytics"],
318
+ examples: [
319
+ { label: "List calls", method: "GET", path: "/calls" },
320
+ {
321
+ label: "Call transcript",
322
+ method: "POST",
323
+ path: "/calls/transcript",
324
+ body: { filter: { callIds: ["<call-id>"] } },
325
+ },
326
+ ],
327
+ },
328
+ google_calendar: {
329
+ id: "google_calendar",
330
+ label: "Google Calendar API",
331
+ defaultBaseUrl: "https://www.googleapis.com/calendar/v3",
332
+ auth: {
333
+ type: "oauth-bearer",
334
+ oauthProvider: "google",
335
+ tokenLabel: "Google OAuth token",
336
+ },
337
+ credentialKeys: ["GOOGLE_OAUTH_ACCOUNT"],
338
+ docsUrls: ["https://developers.google.com/calendar/api/v3/reference"],
339
+ specUrls: ["https://www.googleapis.com/discovery/v1/apis/calendar/v3/rest"],
340
+ allowedHostSuffixes: ["googleapis.com"],
341
+ templateUses: ["brain", "calendar", "dispatch"],
342
+ examples: [
343
+ {
344
+ label: "List calendars",
345
+ method: "GET",
346
+ path: "/users/me/calendarList",
347
+ },
348
+ {
349
+ label: "Search events",
350
+ method: "GET",
351
+ path: "/calendars/primary/events",
352
+ },
353
+ ],
354
+ notes: [
355
+ "Uses the current user's stored Google OAuth account. Pass accountId when the user has multiple Google accounts connected.",
356
+ ],
357
+ },
358
+ google_drive: {
359
+ id: "google_drive",
360
+ label: "Google Drive API",
361
+ defaultBaseUrl: "https://www.googleapis.com/drive/v3",
362
+ auth: {
363
+ type: "oauth-bearer",
364
+ oauthProvider: "google",
365
+ tokenLabel: "Google OAuth token",
366
+ },
367
+ credentialKeys: ["GOOGLE_OAUTH_ACCOUNT"],
368
+ docsUrls: ["https://developers.google.com/drive/api/reference/rest/v3"],
369
+ specUrls: ["https://www.googleapis.com/discovery/v1/apis/drive/v3/rest"],
370
+ allowedHostSuffixes: ["googleapis.com"],
371
+ templateUses: ["brain", "content", "slides", "dispatch"],
372
+ examples: [
373
+ { label: "List files", method: "GET", path: "/files" },
374
+ { label: "Get file metadata", method: "GET", path: "/files/{fileId}" },
375
+ ],
376
+ notes: [
377
+ "Uses the current user's stored Google OAuth account. Pass accountId when the user has multiple Google accounts connected.",
378
+ ],
379
+ },
380
+ granola: {
381
+ id: "granola",
382
+ label: "Granola Public API",
383
+ defaultBaseUrl: "https://public-api.granola.ai/v1",
384
+ auth: {
385
+ type: "bearer",
386
+ keys: ["GRANOLA_API_KEY"],
387
+ workspaceProvider: "granola",
388
+ },
389
+ credentialKeys: ["GRANOLA_API_KEY"],
390
+ docsUrls: ["https://docs.granola.ai/"],
391
+ templateUses: ["brain", "dispatch"],
392
+ examples: [
393
+ { label: "List notes", method: "GET", path: "/notes" },
394
+ { label: "Get note", method: "GET", path: "/notes/<note-id>" },
395
+ ],
396
+ },
397
+ grafana: {
398
+ id: "grafana",
399
+ label: "Grafana",
400
+ defaultBaseUrl: "https://grafana.example.com",
401
+ baseUrlCredentialKey: "GRAFANA_URL",
402
+ auth: {
403
+ type: "bearer",
404
+ keys: ["GRAFANA_API_TOKEN"],
405
+ },
406
+ credentialKeys: ["GRAFANA_URL", "GRAFANA_API_TOKEN"],
407
+ docsUrls: ["https://grafana.com/docs/grafana/latest/developers/http_api/"],
408
+ templateUses: ["analytics"],
409
+ examples: [
410
+ { label: "List dashboards", method: "GET", path: "/api/search" },
411
+ ],
412
+ },
413
+ hubspot: {
414
+ id: "hubspot",
415
+ label: "HubSpot",
416
+ defaultBaseUrl: "https://api.hubapi.com",
417
+ auth: {
418
+ type: "bearer",
419
+ keys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
420
+ workspaceProvider: "hubspot",
421
+ },
422
+ credentialKeys: ["HUBSPOT_PRIVATE_APP_TOKEN", "HUBSPOT_ACCESS_TOKEN"],
423
+ docsUrls: ["https://developers.hubspot.com/docs/api/overview"],
424
+ templateUses: ["analytics", "brain", "mail", "dispatch"],
425
+ examples: [
426
+ {
427
+ label: "Search deals with any HubSpot CRM filter",
428
+ method: "POST",
429
+ path: "/crm/v3/objects/deals/search",
430
+ body: {
431
+ filterGroups: [
432
+ {
433
+ filters: [
434
+ {
435
+ propertyName: "products",
436
+ operator: "CONTAINS_TOKEN",
437
+ value: "Publish",
438
+ },
439
+ ],
440
+ },
441
+ ],
442
+ properties: ["dealname", "products", "dealstage", "closedate"],
443
+ limit: 100,
444
+ },
445
+ },
446
+ {
447
+ label: "List deal property metadata",
448
+ method: "GET",
449
+ path: "/crm/v3/properties/deals",
450
+ },
451
+ ],
452
+ },
453
+ jira: {
454
+ id: "jira",
455
+ label: "Jira Cloud",
456
+ defaultBaseUrl: "https://example.atlassian.net",
457
+ baseUrlCredentialKey: "JIRA_BASE_URL",
458
+ auth: {
459
+ type: "basic",
460
+ usernameKey: "JIRA_USER_EMAIL",
461
+ passwordKey: "JIRA_API_TOKEN",
462
+ },
463
+ credentialKeys: ["JIRA_BASE_URL", "JIRA_USER_EMAIL", "JIRA_API_TOKEN"],
464
+ docsUrls: [
465
+ "https://developer.atlassian.com/cloud/jira/platform/rest/v3/intro/",
466
+ ],
467
+ specUrls: [
468
+ "https://dac-static.atlassian.com/cloud/jira/platform/swagger-v3.v3.json",
469
+ ],
470
+ templateUses: ["analytics"],
471
+ examples: [
472
+ {
473
+ label: "JQL search",
474
+ method: "GET",
475
+ path: "/rest/api/3/search/jql",
476
+ },
477
+ ],
478
+ },
479
+ mixpanel: {
480
+ id: "mixpanel",
481
+ label: "Mixpanel",
482
+ defaultBaseUrl: "https://mixpanel.com/api/query",
483
+ auth: {
484
+ type: "basic-raw",
485
+ key: "MIXPANEL_SERVICE_ACCOUNT",
486
+ },
487
+ credentialKeys: ["MIXPANEL_PROJECT_ID", "MIXPANEL_SERVICE_ACCOUNT"],
488
+ docsUrls: ["https://developer.mixpanel.com/reference/overview"],
489
+ allowedHostSuffixes: ["mixpanel.com"],
490
+ templateUses: ["analytics"],
491
+ placeholders: [
492
+ {
493
+ name: "projectId",
494
+ credentialKey: "MIXPANEL_PROJECT_ID",
495
+ label: "Configured Mixpanel project ID",
496
+ },
497
+ ],
498
+ examples: [
499
+ {
500
+ label: "Query events",
501
+ method: "GET",
502
+ path: "/events",
503
+ },
504
+ ],
505
+ notes: [
506
+ "Mixpanel uses multiple API hosts. You may pass full URLs for mixpanel.com or data.mixpanel.com endpoints.",
507
+ ],
508
+ },
509
+ notion: {
510
+ id: "notion",
511
+ label: "Notion",
512
+ defaultBaseUrl: "https://api.notion.com/v1",
513
+ auth: {
514
+ type: "bearer",
515
+ keys: ["NOTION_API_KEY"],
516
+ workspaceProvider: "notion",
517
+ },
518
+ credentialKeys: ["NOTION_API_KEY"],
519
+ docsUrls: ["https://developers.notion.com/reference/intro"],
520
+ defaultHeaders: { "Notion-Version": "2022-06-28" },
521
+ templateUses: ["analytics", "brain", "content", "dispatch"],
522
+ examples: [{ label: "Search", method: "POST", path: "/search", body: {} }],
523
+ },
524
+ posthog: {
525
+ id: "posthog",
526
+ label: "PostHog",
527
+ defaultBaseUrl: "https://app.posthog.com",
528
+ baseUrlCredentialKey: "POSTHOG_HOST",
529
+ auth: {
530
+ type: "bearer",
531
+ keys: ["POSTHOG_API_KEY"],
532
+ },
533
+ credentialKeys: ["POSTHOG_API_KEY", "POSTHOG_PROJECT_ID", "POSTHOG_HOST"],
534
+ docsUrls: ["https://posthog.com/docs/api"],
535
+ templateUses: ["analytics"],
536
+ placeholders: [
537
+ {
538
+ name: "projectId",
539
+ credentialKey: "POSTHOG_PROJECT_ID",
540
+ label: "Configured PostHog project ID",
541
+ },
542
+ ],
543
+ examples: [
544
+ {
545
+ label: "List events",
546
+ method: "GET",
547
+ path: "/api/projects/{projectId}/events/",
548
+ },
549
+ ],
550
+ },
551
+ prometheus: {
552
+ id: "prometheus",
553
+ label: "Prometheus",
554
+ defaultBaseUrl: "https://prometheus.example.com",
555
+ baseUrlCredentialKey: "PROMETHEUS_URL",
556
+ auth: { type: "prometheus" },
557
+ credentialKeys: [
558
+ "PROMETHEUS_URL",
559
+ "PROMETHEUS_USERNAME",
560
+ "PROMETHEUS_PASSWORD",
561
+ "PROMETHEUS_BEARER_TOKEN",
562
+ ],
563
+ docsUrls: ["https://prometheus.io/docs/prometheus/latest/querying/api/"],
564
+ templateUses: ["analytics"],
565
+ examples: [
566
+ {
567
+ label: "Instant query",
568
+ method: "GET",
569
+ path: "/api/v1/query",
570
+ },
571
+ ],
572
+ },
573
+ pylon: {
574
+ id: "pylon",
575
+ label: "Pylon",
576
+ defaultBaseUrl: "https://api.usepylon.com",
577
+ auth: {
578
+ type: "bearer",
579
+ keys: ["PYLON_API_KEY"],
580
+ },
581
+ credentialKeys: ["PYLON_API_KEY"],
582
+ docsUrls: ["https://docs.usepylon.com/pylon-docs/developer/api-reference"],
583
+ templateUses: ["analytics"],
584
+ examples: [{ label: "List issues", method: "GET", path: "/issues" }],
585
+ },
586
+ sentry: {
587
+ id: "sentry",
588
+ label: "Sentry",
589
+ defaultBaseUrl: "https://sentry.io/api/0",
590
+ auth: {
591
+ type: "bearer",
592
+ keys: ["SENTRY_AUTH_TOKEN", "SENTRY_SERVER_TOKEN"],
593
+ },
594
+ credentialKeys: [
595
+ "SENTRY_AUTH_TOKEN",
596
+ "SENTRY_SERVER_TOKEN",
597
+ "SENTRY_ORG_SLUG",
598
+ ],
599
+ docsUrls: ["https://docs.sentry.io/api/"],
600
+ templateUses: ["analytics"],
601
+ placeholders: [
602
+ {
603
+ name: "orgSlug",
604
+ credentialKey: "SENTRY_ORG_SLUG",
605
+ label: "Configured Sentry organization slug",
606
+ },
607
+ ],
608
+ examples: [
609
+ {
610
+ label: "List issues for org",
611
+ method: "GET",
612
+ path: "/organizations/{orgSlug}/issues/",
613
+ },
614
+ ],
615
+ },
616
+ slack: {
617
+ id: "slack",
618
+ label: "Slack Web API",
619
+ defaultBaseUrl: "https://slack.com/api",
620
+ auth: {
621
+ type: "bearer",
622
+ keys: ["SLACK_BOT_TOKEN"],
623
+ workspaceProvider: "slack",
624
+ },
625
+ credentialKeys: ["SLACK_BOT_TOKEN", "SLACK_BOT_TOKEN_2"],
626
+ docsUrls: ["https://api.slack.com/web"],
627
+ specUrls: [
628
+ "https://api.slack.com/specs/openapi/v2/slack_web_openapi_v2_without_examples.json",
629
+ ],
630
+ templateUses: ["analytics", "brain", "dispatch"],
631
+ examples: [
632
+ { label: "Search messages", method: "GET", path: "/search.messages" },
633
+ { label: "Post message", method: "POST", path: "/chat.postMessage" },
634
+ ],
635
+ },
636
+ stripe: {
637
+ id: "stripe",
638
+ label: "Stripe",
639
+ defaultBaseUrl: "https://api.stripe.com/v1",
640
+ auth: {
641
+ type: "bearer",
642
+ keys: ["STRIPE_SECRET_KEY"],
643
+ },
644
+ credentialKeys: ["STRIPE_SECRET_KEY"],
645
+ docsUrls: ["https://docs.stripe.com/api"],
646
+ specUrls: [
647
+ "https://raw.githubusercontent.com/stripe/openapi/master/openapi/spec3.json",
648
+ ],
649
+ templateUses: ["analytics"],
650
+ examples: [{ label: "List customers", method: "GET", path: "/customers" }],
651
+ },
652
+ twitter: {
653
+ id: "twitter",
654
+ label: "Twitter/X via twitterapi.io",
655
+ defaultBaseUrl: "https://api.twitterapi.io",
656
+ auth: {
657
+ type: "api-key-header",
658
+ key: "TWITTER_BEARER_TOKEN",
659
+ header: "X-API-Key",
660
+ },
661
+ credentialKeys: ["TWITTER_BEARER_TOKEN"],
662
+ docsUrls: ["https://twitterapi.io/docs"],
663
+ templateUses: ["analytics"],
664
+ examples: [
665
+ {
666
+ label: "User tweets",
667
+ method: "GET",
668
+ path: "/twitter/user/last_tweets",
669
+ },
670
+ ],
671
+ },
672
+ };
673
+ export function getProviderApiConfig(provider) {
674
+ const config = PROVIDER_CONFIGS[provider];
675
+ if (!config)
676
+ throw new Error(`Unsupported provider API: ${provider}`);
677
+ return config;
678
+ }
679
+ export function isProviderApiId(provider) {
680
+ return Object.prototype.hasOwnProperty.call(PROVIDER_CONFIGS, provider);
681
+ }
682
+ export function listProviderApiIdsForTemplateUse(templateUse) {
683
+ return PROVIDER_API_IDS.filter((id) => (PROVIDER_CONFIGS[id].templateUses ?? []).includes(templateUse));
684
+ }
685
+ export function listProviderApiCatalog(provider, options = {}) {
686
+ const providerIds = normalizeProviderIds(options.providerIds);
687
+ const configs = provider
688
+ ? [getProviderApiConfig(provider)]
689
+ : providerIds.map((id) => getProviderApiConfig(id));
690
+ return configs.map((config) => ({
691
+ id: config.id,
692
+ label: config.label,
693
+ defaultBaseUrl: config.defaultBaseUrl,
694
+ baseUrlCredentialKey: config.baseUrlCredentialKey ?? null,
695
+ auth: describeAuth(config.auth),
696
+ credentialKeys: config.credentialKeys,
697
+ docsUrls: config.docsUrls,
698
+ specUrls: config.specUrls ?? [],
699
+ allowedHostSuffixes: config.allowedHostSuffixes ?? [],
700
+ placeholders: config.placeholders ?? [],
701
+ defaultHeaders: config.defaultHeaders ?? {},
702
+ examples: config.examples ?? [],
703
+ notes: config.notes ?? [],
704
+ templateUses: config.templateUses ?? [],
705
+ }));
706
+ }
707
+ export function createProviderApiRuntime(options) {
708
+ const providerIds = normalizeProviderIds(options.providerIds);
709
+ const runtimeOptions = {
710
+ ...options,
711
+ providerIds,
712
+ localCredentialSource: options.localCredentialSource ?? "app_local",
713
+ };
714
+ return {
715
+ providerIds,
716
+ listCatalog: (provider) => listProviderApiCatalog(provider, { providerIds }),
717
+ fetchDocs: (docsOptions) => fetchProviderApiDocs(docsOptions, runtimeOptions),
718
+ executeRequest: (args) => executeProviderApiRequest(args, runtimeOptions),
719
+ };
720
+ }
721
+ export async function fetchProviderApiDocs(options, runtime = { appId: "app" }) {
722
+ assertProviderAllowed(options.provider, runtime.providerIds);
723
+ const config = getProviderApiConfig(options.provider);
724
+ const catalog = listProviderApiCatalog(options.provider)[0];
725
+ if (!options.url)
726
+ return { provider: config.id, catalog };
727
+ const url = new URL(options.url);
728
+ const allowed = [
729
+ ...config.docsUrls,
730
+ ...(config.specUrls ?? []),
731
+ config.defaultBaseUrl,
732
+ ].some((allowedUrl) => sameOriginOrChild(url, new URL(allowedUrl)));
733
+ if (!allowed) {
734
+ throw new Error(`Docs URL must be one of the registered ${config.label} docs/spec origins.`);
735
+ }
736
+ if (await isBlockedExtensionUrlWithDns(url.href)) {
737
+ throw new Error(`Blocked private/internal docs URL: ${url.href}`);
738
+ }
739
+ const response = await fetchWithTimeout(url.href, {
740
+ method: "GET",
741
+ maxBytes: clampMaxBytes(options.maxBytes),
742
+ });
743
+ return {
744
+ provider: config.id,
745
+ catalog,
746
+ request: { url: url.href },
747
+ response,
748
+ };
749
+ }
750
+ export async function executeProviderApiRequest(args, runtime) {
751
+ assertProviderAllowed(args.provider, runtime.providerIds);
752
+ const config = getProviderApiConfig(args.provider);
753
+ const ctx = requireRuntimeCredentialContext(runtime, config.credentialKeys[0] ?? config.id);
754
+ const baseUrl = await resolveBaseUrl(config, runtime, ctx, args);
755
+ const placeholders = await resolvePlaceholders(config, runtime, ctx, args);
756
+ const method = normalizeMethod(args.method);
757
+ const url = buildProviderUrl({
758
+ config,
759
+ baseUrl,
760
+ rawPath: substituteString(args.path, placeholders),
761
+ query: substituteUnknown(args.query, placeholders),
762
+ });
763
+ if (await isBlockedExtensionUrlWithDns(url.href)) {
764
+ throw new Error(`Blocked private/internal provider URL: ${url.href}`);
765
+ }
766
+ const auth = args.auth === "none"
767
+ ? emptyAuth()
768
+ : await resolveAuth(config, runtime, ctx, args);
769
+ const extraHeaders = substituteUnknown(args.headers ?? {}, placeholders);
770
+ const headers = sanitizeOutboundHeaders({
771
+ ...(config.defaultHeaders ?? {}),
772
+ ...(isPlainRecord(extraHeaders) ? extraHeaders : {}),
773
+ ...auth.headers,
774
+ });
775
+ const body = prepareBody(substituteUnknown(args.body, placeholders), headers);
776
+ const response = await fetchWithTimeout(url.href, {
777
+ method,
778
+ headers,
779
+ body,
780
+ maxBytes: clampMaxBytes(args.maxBytes),
781
+ timeoutMs: clampTimeout(args.timeoutMs),
782
+ secretValues: auth.secretValues,
783
+ });
784
+ return {
785
+ provider: {
786
+ id: config.id,
787
+ label: config.label,
788
+ docsUrls: config.docsUrls,
789
+ specUrls: config.specUrls ?? [],
790
+ },
791
+ request: {
792
+ method,
793
+ url: redactString(url.href, auth.secretValues),
794
+ path: redactString(`${url.pathname}${url.search}`, auth.secretValues),
795
+ auth: args.auth === "none" ? "none" : describeAuth(config.auth),
796
+ credentialSources: auth.credentialSources.map((source) => ({
797
+ ...source,
798
+ fingerprint: fingerprint(source.key),
799
+ })),
800
+ headerNames: Object.keys(headers).filter((name) => name.toLowerCase() !== "authorization"),
801
+ ...(args.accountId ? { accountId: args.accountId } : {}),
802
+ ...(args.connectionId ? { connectionId: args.connectionId } : {}),
803
+ },
804
+ response,
805
+ guidance: "This was a raw provider API request. Use provider docs/spec URLs to choose endpoints and include method/path/status plus relevant filters in the methodology. Prefer this escape hatch whenever canned actions are too narrow.",
806
+ };
807
+ }
808
+ export async function defaultProviderApiCredentialResolver(options) {
809
+ if (options.workspaceProvider) {
810
+ const result = await resolveWorkspaceConnectionCredentialForApp({
811
+ appId: options.appId,
812
+ provider: options.workspaceProvider,
813
+ key: options.key,
814
+ connectionId: options.connectionId,
815
+ userEmail: options.ctx.userEmail,
816
+ orgId: options.ctx.orgId,
817
+ });
818
+ if (result.available && result.value) {
819
+ return {
820
+ key: result.provenance?.resolvedKey ?? result.key,
821
+ value: result.value,
822
+ source: "workspace_connection",
823
+ provider: result.provider,
824
+ connectionId: result.provenance?.connectionId,
825
+ connectionLabel: result.provenance?.connectionLabel,
826
+ scope: typeof result.provenance?.secretScope === "string"
827
+ ? result.provenance.secretScope
828
+ : undefined,
829
+ };
830
+ }
831
+ }
832
+ const value = await resolveCredential(options.key, options.ctx);
833
+ if (!value)
834
+ return null;
835
+ return {
836
+ key: options.key,
837
+ value,
838
+ source: options.localCredentialSource,
839
+ provider: options.provider,
840
+ };
841
+ }
842
+ function normalizeProviderIds(providerIds) {
843
+ if (!providerIds)
844
+ return [...PROVIDER_API_IDS];
845
+ const result = [];
846
+ const seen = new Set();
847
+ for (const providerId of providerIds) {
848
+ if (!isProviderApiId(providerId)) {
849
+ throw new Error(`Unsupported provider API: ${providerId}`);
850
+ }
851
+ if (seen.has(providerId))
852
+ continue;
853
+ seen.add(providerId);
854
+ result.push(providerId);
855
+ }
856
+ return result;
857
+ }
858
+ function assertProviderAllowed(provider, providerIds) {
859
+ const allowed = normalizeProviderIds(providerIds);
860
+ if (!allowed.includes(provider)) {
861
+ throw new Error(`Provider API ${provider} is not enabled for this app.`);
862
+ }
863
+ }
864
+ function describeAuth(auth) {
865
+ if (auth.type === "none")
866
+ return "none";
867
+ if (auth.type === "bearer")
868
+ return "bearer";
869
+ if (auth.type === "basic")
870
+ return "basic";
871
+ if (auth.type === "basic-raw")
872
+ return "basic";
873
+ if (auth.type === "api-key-header")
874
+ return `api-key-header:${auth.header}`;
875
+ if (auth.type === "google-service-account")
876
+ return "google-service-account";
877
+ if (auth.type === "oauth-bearer")
878
+ return `oauth-bearer:${auth.oauthProvider}`;
879
+ return "prometheus-basic-or-bearer";
880
+ }
881
+ function requireRuntimeCredentialContext(runtime, credentialKey) {
882
+ const ctx = runtime.getCredentialContext?.() ?? getCredentialContext();
883
+ if (!ctx) {
884
+ throw new Error(`Cannot resolve credential "${credentialKey}" outside an authenticated request context.`);
885
+ }
886
+ return ctx;
887
+ }
888
+ async function resolveBaseUrl(config, runtime, ctx, args) {
889
+ if (!config.baseUrlCredentialKey)
890
+ return config.defaultBaseUrl;
891
+ const configured = await resolveCredentialValue({
892
+ config,
893
+ runtime,
894
+ ctx,
895
+ key: config.baseUrlCredentialKey,
896
+ args,
897
+ });
898
+ return (configured || config.defaultBaseUrl).replace(/\/+$/, "");
899
+ }
900
+ async function resolvePlaceholders(config, runtime, ctx, args) {
901
+ const placeholders = {};
902
+ for (const placeholder of config.placeholders ?? []) {
903
+ const value = await resolveCredentialValue({
904
+ config,
905
+ runtime,
906
+ ctx,
907
+ key: placeholder.credentialKey,
908
+ args,
909
+ });
910
+ if (value)
911
+ placeholders[placeholder.name] = value;
912
+ }
913
+ return placeholders;
914
+ }
915
+ async function resolveCredentialValue(options) {
916
+ const credential = await resolveOptionalCredential({
917
+ provider: options.config.id,
918
+ workspaceProvider: options.workspaceProvider,
919
+ key: options.key,
920
+ ctx: options.ctx,
921
+ runtime: options.runtime,
922
+ connectionId: options.args.connectionId,
923
+ });
924
+ return credential?.value;
925
+ }
926
+ function substituteString(value, placeholders) {
927
+ let result = value;
928
+ for (const [name, replacement] of Object.entries(placeholders)) {
929
+ result = result.split(`{${name}}`).join(replacement);
930
+ }
931
+ return result;
932
+ }
933
+ function substituteUnknown(value, placeholders) {
934
+ if (typeof value === "string")
935
+ return substituteString(value, placeholders);
936
+ if (Array.isArray(value)) {
937
+ return value.map((item) => substituteUnknown(item, placeholders));
938
+ }
939
+ if (value && typeof value === "object") {
940
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
941
+ key,
942
+ substituteUnknown(entry, placeholders),
943
+ ]));
944
+ }
945
+ return value;
946
+ }
947
+ function isPlainRecord(value) {
948
+ return !!value && typeof value === "object" && !Array.isArray(value);
949
+ }
950
+ function buildProviderUrl(options) {
951
+ const base = new URL(options.baseUrl);
952
+ const rawPath = options.rawPath.trim();
953
+ const url = /^https?:\/\//i.test(rawPath)
954
+ ? new URL(rawPath)
955
+ : new URL(rawPath.startsWith("/") ? rawPath : `/${rawPath}`, base);
956
+ if (!isAllowedProviderUrl(url, base, options.config)) {
957
+ throw new Error(`${options.config.label} API requests must stay on the configured provider host or registered provider host suffix.`);
958
+ }
959
+ for (const [key, value] of queryEntries(options.query)) {
960
+ url.searchParams.append(key, value);
961
+ }
962
+ return url;
963
+ }
964
+ function isAllowedProviderUrl(url, base, config) {
965
+ if (url.protocol !== "https:" && url.protocol !== "http:")
966
+ return false;
967
+ if (url.origin === base.origin)
968
+ return true;
969
+ const host = url.hostname.toLowerCase();
970
+ return (config.allowedHostSuffixes ?? []).some((suffix) => {
971
+ const normalized = suffix.toLowerCase().replace(/^\./, "");
972
+ return host === normalized || host.endsWith(`.${normalized}`);
973
+ });
974
+ }
975
+ function sameOriginOrChild(candidate, allowed) {
976
+ return (candidate.origin === allowed.origin &&
977
+ (candidate.pathname === allowed.pathname ||
978
+ candidate.pathname.startsWith(allowed.pathname.replace(/\/?$/, "/"))));
979
+ }
980
+ function queryEntries(value) {
981
+ if (!value)
982
+ return [];
983
+ if (typeof value === "string") {
984
+ const params = new URLSearchParams(value.replace(/^\?/, ""));
985
+ return Array.from(params.entries());
986
+ }
987
+ if (typeof value !== "object" || Array.isArray(value))
988
+ return [];
989
+ const entries = [];
990
+ for (const [key, raw] of Object.entries(value)) {
991
+ if (raw === undefined || raw === null)
992
+ continue;
993
+ if (Array.isArray(raw)) {
994
+ for (const item of raw)
995
+ entries.push([key, String(item)]);
996
+ }
997
+ else {
998
+ entries.push([key, String(raw)]);
999
+ }
1000
+ }
1001
+ return entries;
1002
+ }
1003
+ async function resolveAuth(config, runtime, ctx, args) {
1004
+ const auth = config.auth;
1005
+ if (auth.type === "none")
1006
+ return emptyAuth();
1007
+ if (auth.type === "bearer") {
1008
+ const credential = await resolveAnyCredential({
1009
+ provider: config.id,
1010
+ workspaceProvider: auth.workspaceProvider,
1011
+ keys: auth.keys,
1012
+ ctx,
1013
+ runtime,
1014
+ connectionId: args.connectionId,
1015
+ });
1016
+ return {
1017
+ headers: { Authorization: `Bearer ${credential.value}` },
1018
+ credentialSources: [omitCredentialValue(credential)],
1019
+ secretValues: [credential.value],
1020
+ };
1021
+ }
1022
+ if (auth.type === "basic") {
1023
+ const username = await resolveRequiredCredential({
1024
+ provider: config.id,
1025
+ workspaceProvider: auth.workspaceProvider,
1026
+ key: auth.usernameKey,
1027
+ ctx,
1028
+ runtime,
1029
+ connectionId: args.connectionId,
1030
+ });
1031
+ const password = auth.passwordKey === auth.usernameKey
1032
+ ? username
1033
+ : await resolveRequiredCredential({
1034
+ provider: config.id,
1035
+ workspaceProvider: auth.workspaceProvider,
1036
+ key: auth.passwordKey,
1037
+ ctx,
1038
+ runtime,
1039
+ connectionId: args.connectionId,
1040
+ });
1041
+ const encoded = Buffer.from(`${username.value}:${password.value}`).toString("base64");
1042
+ return {
1043
+ headers: { Authorization: `Basic ${encoded}` },
1044
+ credentialSources: [
1045
+ omitCredentialValue(username),
1046
+ ...(password.key === username.key
1047
+ ? []
1048
+ : [omitCredentialValue(password)]),
1049
+ ],
1050
+ secretValues: [username.value, password.value, encoded],
1051
+ };
1052
+ }
1053
+ if (auth.type === "basic-raw") {
1054
+ const credential = await resolveRequiredCredential({
1055
+ provider: config.id,
1056
+ workspaceProvider: auth.workspaceProvider,
1057
+ key: auth.key,
1058
+ ctx,
1059
+ runtime,
1060
+ connectionId: args.connectionId,
1061
+ });
1062
+ const encoded = Buffer.from(credential.value).toString("base64");
1063
+ return {
1064
+ headers: { Authorization: `Basic ${encoded}` },
1065
+ credentialSources: [omitCredentialValue(credential)],
1066
+ secretValues: [credential.value, encoded],
1067
+ };
1068
+ }
1069
+ if (auth.type === "api-key-header") {
1070
+ const credential = await resolveRequiredCredential({
1071
+ provider: config.id,
1072
+ workspaceProvider: auth.workspaceProvider,
1073
+ key: auth.key,
1074
+ ctx,
1075
+ runtime,
1076
+ connectionId: args.connectionId,
1077
+ });
1078
+ return {
1079
+ headers: { [auth.header]: credential.value },
1080
+ credentialSources: [omitCredentialValue(credential)],
1081
+ secretValues: [credential.value],
1082
+ };
1083
+ }
1084
+ if (auth.type === "google-service-account") {
1085
+ const token = await getGoogleServiceAccountToken(auth.scopes, runtime, ctx);
1086
+ return {
1087
+ headers: { Authorization: `Bearer ${token}` },
1088
+ credentialSources: [
1089
+ {
1090
+ key: "GOOGLE_APPLICATION_CREDENTIALS_JSON",
1091
+ provider: config.id,
1092
+ source: runtime.localCredentialSource ?? "app_local",
1093
+ },
1094
+ ],
1095
+ secretValues: [token],
1096
+ };
1097
+ }
1098
+ if (auth.type === "oauth-bearer") {
1099
+ const credential = await resolveOAuthBearerToken({
1100
+ auth,
1101
+ ctx,
1102
+ accountId: args.accountId,
1103
+ });
1104
+ return {
1105
+ headers: { Authorization: `Bearer ${credential.value}` },
1106
+ credentialSources: [omitCredentialValue(credential)],
1107
+ secretValues: [credential.value],
1108
+ };
1109
+ }
1110
+ const bearer = await resolveCredentialValue({
1111
+ config,
1112
+ runtime,
1113
+ ctx,
1114
+ key: "PROMETHEUS_BEARER_TOKEN",
1115
+ args,
1116
+ });
1117
+ if (bearer) {
1118
+ return {
1119
+ headers: { Authorization: `Bearer ${bearer}` },
1120
+ credentialSources: [
1121
+ {
1122
+ key: "PROMETHEUS_BEARER_TOKEN",
1123
+ provider: config.id,
1124
+ source: runtime.localCredentialSource ?? "app_local",
1125
+ },
1126
+ ],
1127
+ secretValues: [bearer],
1128
+ };
1129
+ }
1130
+ const username = await resolveCredentialValue({
1131
+ config,
1132
+ runtime,
1133
+ ctx,
1134
+ key: "PROMETHEUS_USERNAME",
1135
+ args,
1136
+ });
1137
+ const password = await resolveCredentialValue({
1138
+ config,
1139
+ runtime,
1140
+ ctx,
1141
+ key: "PROMETHEUS_PASSWORD",
1142
+ args,
1143
+ });
1144
+ if (username && password) {
1145
+ const encoded = Buffer.from(`${username}:${password}`).toString("base64");
1146
+ return {
1147
+ headers: { Authorization: `Basic ${encoded}` },
1148
+ credentialSources: [
1149
+ {
1150
+ key: "PROMETHEUS_USERNAME",
1151
+ provider: config.id,
1152
+ source: runtime.localCredentialSource ?? "app_local",
1153
+ },
1154
+ {
1155
+ key: "PROMETHEUS_PASSWORD",
1156
+ provider: config.id,
1157
+ source: runtime.localCredentialSource ?? "app_local",
1158
+ },
1159
+ ],
1160
+ secretValues: [username, password, encoded],
1161
+ };
1162
+ }
1163
+ return emptyAuth();
1164
+ }
1165
+ function emptyAuth() {
1166
+ return { headers: {}, credentialSources: [], secretValues: [] };
1167
+ }
1168
+ async function resolveAnyCredential(options) {
1169
+ for (const key of options.keys) {
1170
+ const credential = await resolveOptionalCredential({ ...options, key });
1171
+ if (credential?.value)
1172
+ return credential;
1173
+ }
1174
+ throw new Error(`${options.provider} credential not configured. Tried: ${options.keys.join(", ")}`);
1175
+ }
1176
+ async function resolveRequiredCredential(options) {
1177
+ const credential = await resolveOptionalCredential(options);
1178
+ if (!credential?.value)
1179
+ throw new Error(`${options.key} not configured`);
1180
+ return credential;
1181
+ }
1182
+ async function resolveOptionalCredential(options) {
1183
+ const localCredentialSource = options.runtime.localCredentialSource ?? "app_local";
1184
+ const lookup = {
1185
+ appId: options.runtime.appId,
1186
+ provider: options.provider,
1187
+ key: options.key,
1188
+ ctx: options.ctx,
1189
+ workspaceProvider: options.workspaceProvider,
1190
+ connectionId: options.connectionId,
1191
+ localCredentialSource,
1192
+ };
1193
+ const customCredential = await options.runtime.resolveCredential?.(lookup);
1194
+ if (customCredential?.value)
1195
+ return customCredential;
1196
+ return defaultProviderApiCredentialResolver(lookup);
1197
+ }
1198
+ function omitCredentialValue(credential) {
1199
+ const { value: _value, ...rest } = credential;
1200
+ return rest;
1201
+ }
1202
+ const googleServiceTokenCache = new Map();
1203
+ async function getGoogleServiceAccountToken(scopes, runtime, ctx) {
1204
+ const cacheKey = createHash("sha256")
1205
+ .update(`${runtime.appId}:${ctx.orgId ?? ctx.userEmail}:${scopes.join(" ")}`)
1206
+ .digest("hex");
1207
+ const cached = googleServiceTokenCache.get(cacheKey);
1208
+ if (cached && Date.now() < cached.expiresAt - 30_000)
1209
+ return cached.token;
1210
+ const credsJson = await resolveCredentialValue({
1211
+ config: getProviderApiConfig("gcloud"),
1212
+ runtime,
1213
+ ctx,
1214
+ key: "GOOGLE_APPLICATION_CREDENTIALS_JSON",
1215
+ args: { provider: "gcloud", path: "/" },
1216
+ });
1217
+ if (!credsJson) {
1218
+ throw new Error("GOOGLE_APPLICATION_CREDENTIALS_JSON not configured");
1219
+ }
1220
+ let creds;
1221
+ try {
1222
+ creds = JSON.parse(credsJson);
1223
+ }
1224
+ catch {
1225
+ throw new Error("GOOGLE_APPLICATION_CREDENTIALS_JSON is not valid JSON. Upload a service account JSON key.");
1226
+ }
1227
+ if (!creds.client_email || !creds.private_key) {
1228
+ throw new Error("GOOGLE_APPLICATION_CREDENTIALS_JSON must be a service account JSON key.");
1229
+ }
1230
+ const now = Math.floor(Date.now() / 1000);
1231
+ const aud = creds.token_uri || "https://oauth2.googleapis.com/token";
1232
+ const jwt = await signRs256Jwt({
1233
+ iss: creds.client_email,
1234
+ scope: scopes.join(" "),
1235
+ aud,
1236
+ iat: now,
1237
+ exp: now + 3600,
1238
+ }, creds.private_key);
1239
+ const res = await fetch(aud, {
1240
+ method: "POST",
1241
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1242
+ body: new URLSearchParams({
1243
+ grant_type: "urn:ietf:params:oauth:grant-type:jwt-bearer",
1244
+ assertion: jwt,
1245
+ }),
1246
+ });
1247
+ if (!res.ok) {
1248
+ throw new Error(`Google OAuth error ${res.status}: ${await res.text()}`);
1249
+ }
1250
+ const data = (await res.json());
1251
+ googleServiceTokenCache.set(cacheKey, {
1252
+ token: data.access_token,
1253
+ expiresAt: Date.now() + (data.expires_in ?? 3600) * 1000,
1254
+ });
1255
+ return data.access_token;
1256
+ }
1257
+ async function resolveOAuthBearerToken(options) {
1258
+ const accounts = await listOAuthAccountsByOwner(options.auth.oauthProvider, options.ctx.userEmail);
1259
+ if (accounts.length === 0) {
1260
+ throw new Error(`${options.auth.tokenLabel} is not connected for ${options.ctx.userEmail}.`);
1261
+ }
1262
+ const accountId = options.accountId?.trim();
1263
+ const account = accountId
1264
+ ? accounts.find((entry) => entry.accountId === accountId)
1265
+ : accounts[0];
1266
+ if (!account) {
1267
+ throw new Error(`${options.auth.tokenLabel} account ${accountId} is not available to ${options.ctx.userEmail}.`);
1268
+ }
1269
+ const tokens = account.tokens;
1270
+ const token = await getValidOAuthAccessToken({
1271
+ oauthProvider: options.auth.oauthProvider,
1272
+ accountId: account.accountId,
1273
+ ownerEmail: options.ctx.userEmail,
1274
+ tokens,
1275
+ });
1276
+ return {
1277
+ key: `${options.auth.oauthProvider.toUpperCase()}_OAUTH_TOKEN`,
1278
+ value: token,
1279
+ source: "oauth_token",
1280
+ provider: options.auth.oauthProvider,
1281
+ accountId: account.accountId,
1282
+ accountLabel: account.displayName,
1283
+ };
1284
+ }
1285
+ async function getValidOAuthAccessToken(options) {
1286
+ const accessToken = options.tokens.access_token ?? options.tokens.accessToken ?? "";
1287
+ if (!accessToken) {
1288
+ throw new Error(`${options.oauthProvider} OAuth account has no access token.`);
1289
+ }
1290
+ const expiresAt = options.tokens.expiry_date ?? options.tokens.expiresAt;
1291
+ if (!expiresAt ||
1292
+ !Number.isFinite(expiresAt) ||
1293
+ expiresAt > Date.now() + 60_000) {
1294
+ return accessToken;
1295
+ }
1296
+ const refreshToken = options.tokens.refresh_token ?? options.tokens.refreshToken;
1297
+ if (!refreshToken)
1298
+ return accessToken;
1299
+ if (options.oauthProvider === "google") {
1300
+ return refreshGoogleOAuthToken(options, refreshToken);
1301
+ }
1302
+ throw new Error(`${options.oauthProvider} OAuth token is expired and automatic refresh is not configured for provider-api.`);
1303
+ }
1304
+ async function refreshGoogleOAuthToken(options, refreshToken) {
1305
+ const clientId = process.env.GOOGLE_CLIENT_ID;
1306
+ const clientSecret = process.env.GOOGLE_CLIENT_SECRET;
1307
+ if (!clientId || !clientSecret) {
1308
+ throw new Error("GOOGLE_CLIENT_ID/SECRET not set for Google OAuth refresh.");
1309
+ }
1310
+ const res = await fetch("https://oauth2.googleapis.com/token", {
1311
+ method: "POST",
1312
+ headers: { "Content-Type": "application/x-www-form-urlencoded" },
1313
+ body: new URLSearchParams({
1314
+ refresh_token: refreshToken,
1315
+ client_id: clientId,
1316
+ client_secret: clientSecret,
1317
+ grant_type: "refresh_token",
1318
+ }),
1319
+ });
1320
+ const data = (await res.json());
1321
+ if (!res.ok || !data.access_token) {
1322
+ const detail = data.error_description ?? data.error ?? res.statusText;
1323
+ throw new Error(`Google OAuth refresh failed: ${detail}`);
1324
+ }
1325
+ const merged = {
1326
+ ...options.tokens,
1327
+ access_token: data.access_token,
1328
+ expiry_date: Date.now() + (data.expires_in ?? 3600) * 1000,
1329
+ token_type: data.token_type ?? options.tokens.token_type,
1330
+ scope: data.scope ?? options.tokens.scope,
1331
+ };
1332
+ await saveOAuthTokens(options.oauthProvider, options.accountId, merged, options.ownerEmail);
1333
+ return data.access_token;
1334
+ }
1335
+ function normalizeMethod(method) {
1336
+ const normalized = String(method || "GET").toUpperCase();
1337
+ if (normalized === "GET" ||
1338
+ normalized === "POST" ||
1339
+ normalized === "PUT" ||
1340
+ normalized === "PATCH" ||
1341
+ normalized === "DELETE" ||
1342
+ normalized === "HEAD") {
1343
+ return normalized;
1344
+ }
1345
+ throw new Error(`Unsupported HTTP method: ${method}`);
1346
+ }
1347
+ function sanitizeOutboundHeaders(value) {
1348
+ if (!value || typeof value !== "object" || Array.isArray(value))
1349
+ return {};
1350
+ const headers = {};
1351
+ for (const [name, rawValue] of Object.entries(value)) {
1352
+ const lower = name.toLowerCase();
1353
+ if (!HEADER_NAME_RE.test(name) || BLOCKED_OUTBOUND_HEADERS.has(lower)) {
1354
+ continue;
1355
+ }
1356
+ if (rawValue === undefined || rawValue === null)
1357
+ continue;
1358
+ const headerValue = String(rawValue);
1359
+ if (/[\r\n]/.test(headerValue))
1360
+ continue;
1361
+ headers[name] = headerValue;
1362
+ }
1363
+ return headers;
1364
+ }
1365
+ function prepareBody(body, headers) {
1366
+ if (body === undefined || body === null)
1367
+ return undefined;
1368
+ if (typeof body === "string")
1369
+ return body;
1370
+ const hasContentType = Object.keys(headers).some((name) => name.toLowerCase() === "content-type");
1371
+ if (!hasContentType)
1372
+ headers["Content-Type"] = "application/json";
1373
+ return JSON.stringify(body);
1374
+ }
1375
+ async function fetchWithTimeout(optionsUrl, options) {
1376
+ const controller = new AbortController();
1377
+ const timeout = setTimeout(() => controller.abort(), clampTimeout(options.timeoutMs));
1378
+ try {
1379
+ const dispatcher = (await createSsrfSafeDispatcher()) ?? undefined;
1380
+ const fetchOptions = {
1381
+ method: options.method ?? "GET",
1382
+ headers: options.headers,
1383
+ body: options.body,
1384
+ signal: controller.signal,
1385
+ redirect: "manual",
1386
+ };
1387
+ if (dispatcher)
1388
+ fetchOptions.dispatcher = dispatcher;
1389
+ const startedAt = Date.now();
1390
+ const res = await fetch(optionsUrl, fetchOptions);
1391
+ const elapsedMs = Date.now() - startedAt;
1392
+ const rawText = await readResponseTextWithLimit(res, clampMaxBytes(options.maxBytes));
1393
+ const secretValues = options.secretValues ?? [];
1394
+ const redactedText = redactString(rawText.text, secretValues);
1395
+ const parsed = tryParseJson(redactedText);
1396
+ return {
1397
+ status: res.status,
1398
+ statusText: res.statusText,
1399
+ ok: res.ok,
1400
+ elapsedMs,
1401
+ headers: redactSecrets(headersToObject(res.headers), secretValues),
1402
+ contentType: res.headers.get("content-type") ?? null,
1403
+ size: rawText.size,
1404
+ truncated: rawText.truncated,
1405
+ text: parsed === undefined ? redactedText : undefined,
1406
+ json: parsed,
1407
+ };
1408
+ }
1409
+ finally {
1410
+ clearTimeout(timeout);
1411
+ }
1412
+ }
1413
+ async function readResponseTextWithLimit(response, maxBytes) {
1414
+ const contentLength = response.headers.get("content-length");
1415
+ if (contentLength && Number(contentLength) > maxBytes) {
1416
+ return {
1417
+ text: `(response too large - ${contentLength} bytes, max ${maxBytes})`,
1418
+ truncated: true,
1419
+ size: Number(contentLength),
1420
+ };
1421
+ }
1422
+ const buffer = await response.arrayBuffer();
1423
+ const size = buffer.byteLength;
1424
+ const bytes = new Uint8Array(buffer.slice(0, maxBytes));
1425
+ return {
1426
+ text: new TextDecoder().decode(bytes),
1427
+ truncated: size > maxBytes,
1428
+ size,
1429
+ };
1430
+ }
1431
+ function headersToObject(headers) {
1432
+ const result = {};
1433
+ headers.forEach((value, key) => {
1434
+ if (key.toLowerCase() !== "set-cookie")
1435
+ result[key] = value;
1436
+ });
1437
+ return result;
1438
+ }
1439
+ function tryParseJson(text) {
1440
+ const trimmed = text.trim();
1441
+ if (!trimmed || !/^[{[]/.test(trimmed))
1442
+ return undefined;
1443
+ try {
1444
+ return JSON.parse(trimmed);
1445
+ }
1446
+ catch {
1447
+ return undefined;
1448
+ }
1449
+ }
1450
+ function redactSecrets(value, secretValues) {
1451
+ if (secretValues.length === 0)
1452
+ return value;
1453
+ if (typeof value === "string")
1454
+ return redactString(value, secretValues);
1455
+ if (Array.isArray(value)) {
1456
+ return value.map((item) => redactSecrets(item, secretValues));
1457
+ }
1458
+ if (value && typeof value === "object") {
1459
+ return Object.fromEntries(Object.entries(value).map(([key, entry]) => [
1460
+ key,
1461
+ redactSecrets(entry, secretValues),
1462
+ ]));
1463
+ }
1464
+ return value;
1465
+ }
1466
+ function redactString(text, secretValues) {
1467
+ let output = text;
1468
+ for (const secret of [...secretValues].sort((a, b) => b.length - a.length)) {
1469
+ if (!secret)
1470
+ continue;
1471
+ output = output.split(secret).join("[redacted]");
1472
+ try {
1473
+ output = output.split(encodeURIComponent(secret)).join("[redacted]");
1474
+ }
1475
+ catch { }
1476
+ }
1477
+ return output;
1478
+ }
1479
+ function clampTimeout(timeoutMs) {
1480
+ if (!Number.isFinite(timeoutMs))
1481
+ return DEFAULT_TIMEOUT_MS;
1482
+ return Math.max(1_000, Math.min(MAX_TIMEOUT_MS, Math.floor(timeoutMs)));
1483
+ }
1484
+ function clampMaxBytes(maxBytes) {
1485
+ if (!Number.isFinite(maxBytes))
1486
+ return DEFAULT_MAX_BYTES;
1487
+ return Math.max(1_000, Math.min(MAX_MAX_BYTES, Math.floor(maxBytes)));
1488
+ }
1489
+ function fingerprint(value) {
1490
+ return createHash("sha256").update(value).digest("hex").slice(0, 12);
1491
+ }
1492
+ function base64UrlEncode(bytes) {
1493
+ let binary = "";
1494
+ for (let i = 0; i < bytes.length; i += 1) {
1495
+ binary += String.fromCharCode(bytes[i]);
1496
+ }
1497
+ return btoa(binary)
1498
+ .replace(/\+/g, "-")
1499
+ .replace(/\//g, "_")
1500
+ .replace(/=+$/, "");
1501
+ }
1502
+ function base64UrlEncodeString(value) {
1503
+ return base64UrlEncode(new TextEncoder().encode(value));
1504
+ }
1505
+ function pemToPkcs8(pem) {
1506
+ const body = pem
1507
+ .replace(/-----BEGIN [^-]+-----/g, "")
1508
+ .replace(/-----END [^-]+-----/g, "")
1509
+ .replace(/\s+/g, "");
1510
+ const binary = atob(body);
1511
+ const out = new Uint8Array(binary.length);
1512
+ for (let i = 0; i < binary.length; i += 1) {
1513
+ out[i] = binary.charCodeAt(i);
1514
+ }
1515
+ return out;
1516
+ }
1517
+ const keyCache = new Map();
1518
+ function importRs256Key(privateKeyPem) {
1519
+ let cached = keyCache.get(privateKeyPem);
1520
+ if (!cached) {
1521
+ cached = crypto.subtle.importKey("pkcs8", pemToPkcs8(privateKeyPem), { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, false, ["sign"]);
1522
+ keyCache.set(privateKeyPem, cached);
1523
+ }
1524
+ return cached;
1525
+ }
1526
+ async function signRs256Jwt(payload, privateKeyPem) {
1527
+ const header = { alg: "RS256", typ: "JWT" };
1528
+ const signingInput = `${base64UrlEncodeString(JSON.stringify(header))}.${base64UrlEncodeString(JSON.stringify(payload))}`;
1529
+ const key = await importRs256Key(privateKeyPem);
1530
+ const signature = await crypto.subtle.sign("RSASSA-PKCS1-v1_5", key, new TextEncoder().encode(signingInput));
1531
+ return `${signingInput}.${base64UrlEncode(new Uint8Array(signature))}`;
1532
+ }
1533
+ //# sourceMappingURL=index.js.map