@tangle-network/agent-integrations 0.25.7 → 0.26.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.
- package/README.md +11 -2
- package/dist/bin/tangle-catalog-runtime.js +3 -2
- package/dist/bin/tangle-catalog-runtime.js.map +1 -1
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +3 -2
- package/dist/chunk-2TW2QKGZ.js +94 -0
- package/dist/chunk-2TW2QKGZ.js.map +1 -0
- package/dist/{chunk-A5I3EYU5.js → chunk-ALCIWTIR.js} +96 -1
- package/dist/chunk-ALCIWTIR.js.map +1 -0
- package/dist/{chunk-WC63AI4Q.js → chunk-GA4VTE3U.js} +1249 -169
- package/dist/chunk-GA4VTE3U.js.map +1 -0
- package/dist/connectors/adapters/index.d.ts +1 -1
- package/dist/connectors/adapters/index.js +8 -1
- package/dist/connectors/index.d.ts +1 -1
- package/dist/connectors/index.js +14 -6
- package/dist/{index-BQY5ry2s.d.ts → index-D4D4CEKX.d.ts} +177 -9
- package/dist/index.d.ts +2 -2
- package/dist/index.js +17 -7
- package/dist/registry.d.ts +139 -2
- package/dist/registry.js +3 -2
- package/dist/runtime.d.ts +1 -1
- package/dist/runtime.js +3 -2
- package/dist/specs.d.ts +1 -1
- package/dist/tangle-catalog-runtime.d.ts +1 -1
- package/dist/tangle-catalog-runtime.js +3 -2
- package/dist/webhooks/index.d.ts +193 -0
- package/dist/webhooks/index.js +285 -0
- package/dist/webhooks/index.js.map +1 -0
- package/examples/discover-capabilities.ts +46 -0
- package/examples/webhook-router.ts +56 -0
- package/package.json +15 -12
- package/dist/chunk-A5I3EYU5.js.map +0 -1
- package/dist/chunk-WC63AI4Q.js.map +0 -1
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
import {
|
|
2
|
+
firstHeader,
|
|
3
|
+
verifySlackSignature,
|
|
4
|
+
verifyStripeSignature
|
|
5
|
+
} from "./chunk-2TW2QKGZ.js";
|
|
6
|
+
|
|
1
7
|
// src/connectors/types.ts
|
|
2
8
|
var ResourceContention = class extends Error {
|
|
3
9
|
constructor(message, alternatives = [], currentState) {
|
|
@@ -462,11 +468,320 @@ function readMetaString(meta, key) {
|
|
|
462
468
|
return v;
|
|
463
469
|
}
|
|
464
470
|
|
|
471
|
+
// src/connectors/adapters/google-drive.ts
|
|
472
|
+
var SCOPES_READONLY = ["https://www.googleapis.com/auth/drive.readonly"];
|
|
473
|
+
var SCOPE_WATCH = "https://www.googleapis.com/auth/drive";
|
|
474
|
+
var AUTH_URL2 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
475
|
+
var TOKEN_URL2 = "https://oauth2.googleapis.com/token";
|
|
476
|
+
var API = "https://www.googleapis.com/drive/v3";
|
|
477
|
+
function googleDrive(opts) {
|
|
478
|
+
const { clientId, clientSecret } = opts;
|
|
479
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
480
|
+
const scopes = opts.includeWatchScope ? [...SCOPES_READONLY, SCOPE_WATCH] : SCOPES_READONLY;
|
|
481
|
+
const adapter = {
|
|
482
|
+
manifest: {
|
|
483
|
+
kind: "google-drive",
|
|
484
|
+
displayName: "Google Drive",
|
|
485
|
+
description: "Read and watch files in the user's Google Drive. List a folder, fetch a document's contents (Docs/Sheets/PDFs/.docx), and subscribe to folder changes via push notifications.",
|
|
486
|
+
auth: {
|
|
487
|
+
kind: "oauth2",
|
|
488
|
+
authorizationUrl: AUTH_URL2,
|
|
489
|
+
tokenUrl: TOKEN_URL2,
|
|
490
|
+
scopes,
|
|
491
|
+
clientIdEnv: "GOOGLE_OAUTH_CLIENT_ID",
|
|
492
|
+
clientSecretEnv: "GOOGLE_OAUTH_CLIENT_SECRET",
|
|
493
|
+
extraAuthParams: { access_type: "offline", prompt: "consent", include_granted_scopes: "true" }
|
|
494
|
+
},
|
|
495
|
+
category: "storage",
|
|
496
|
+
defaultConsistencyModel: "authoritative",
|
|
497
|
+
rateLimit: { requests: 1e3, windowMs: 6e4, scope: "oauth-client" },
|
|
498
|
+
capabilities: [
|
|
499
|
+
{
|
|
500
|
+
name: "list_files",
|
|
501
|
+
class: "read",
|
|
502
|
+
description: `List files visible to the connected Drive account. Optionally scope to a folder by id and/or pass a Drive query string (e.g., "mimeType='application/pdf' and modifiedTime > '2025-01-01T00:00:00Z'").`,
|
|
503
|
+
parameters: {
|
|
504
|
+
type: "object",
|
|
505
|
+
properties: {
|
|
506
|
+
folderId: { type: "string", description: "Drive folder id; when present, restricts to direct children." },
|
|
507
|
+
query: { type: "string", description: "Optional Drive query expression; appended with AND if folderId is set." },
|
|
508
|
+
pageSize: { type: "integer", minimum: 1, maximum: 1e3, default: 100 },
|
|
509
|
+
pageToken: { type: "string", description: "Continuation token returned by a previous call." }
|
|
510
|
+
}
|
|
511
|
+
}
|
|
512
|
+
},
|
|
513
|
+
{
|
|
514
|
+
name: "read_file",
|
|
515
|
+
class: "read",
|
|
516
|
+
description: "Read a file's contents. Google-native types are exported (Docs \u2192 text/plain by default, Sheets \u2192 text/csv, Slides \u2192 application/pdf); binary types are returned as base64.",
|
|
517
|
+
parameters: {
|
|
518
|
+
type: "object",
|
|
519
|
+
properties: {
|
|
520
|
+
fileId: { type: "string" },
|
|
521
|
+
exportMime: {
|
|
522
|
+
type: "string",
|
|
523
|
+
description: "Export mime override for Google-native types. Defaults: Docs=text/plain, Sheets=text/csv, Slides=application/pdf."
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
required: ["fileId"]
|
|
527
|
+
}
|
|
528
|
+
},
|
|
529
|
+
{
|
|
530
|
+
name: "watch_folder",
|
|
531
|
+
class: "mutation",
|
|
532
|
+
description: "Create a push-notification channel for a folder. Drive POSTs change notifications to `address`; the channel expires at `expiration` (max 7 days). Pass the same channelId on retry to replay the existing channel.",
|
|
533
|
+
cas: "native-idempotency",
|
|
534
|
+
externalEffect: true,
|
|
535
|
+
requiredScopes: [SCOPE_WATCH],
|
|
536
|
+
parameters: {
|
|
537
|
+
type: "object",
|
|
538
|
+
properties: {
|
|
539
|
+
folderId: { type: "string" },
|
|
540
|
+
channelId: { type: "string", description: "Caller-controlled UUID; also used as idempotency key." },
|
|
541
|
+
address: { type: "string", description: "HTTPS URL Drive will POST change notifications to." },
|
|
542
|
+
ttlMs: { type: "integer", minimum: 6e4, description: "Channel lifetime in ms. Drive caps at 7 days." }
|
|
543
|
+
},
|
|
544
|
+
required: ["folderId", "channelId", "address"]
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
]
|
|
548
|
+
},
|
|
549
|
+
async executeRead(inv) {
|
|
550
|
+
const accessToken = await ensureFreshAccessToken2(inv.source.credentials, clientId, clientSecret);
|
|
551
|
+
if (inv.capabilityName === "list_files") return listFiles(inv, accessToken, timeoutMs);
|
|
552
|
+
if (inv.capabilityName === "read_file") return readFile(inv, accessToken, timeoutMs);
|
|
553
|
+
throw new Error(`google-drive: unknown read capability ${inv.capabilityName}`);
|
|
554
|
+
},
|
|
555
|
+
async executeMutation(inv) {
|
|
556
|
+
if (inv.capabilityName !== "watch_folder") {
|
|
557
|
+
throw new Error(`google-drive: unknown mutation capability ${inv.capabilityName}`);
|
|
558
|
+
}
|
|
559
|
+
const accessToken = await ensureFreshAccessToken2(inv.source.credentials, clientId, clientSecret);
|
|
560
|
+
return watchFolder(inv, accessToken, timeoutMs);
|
|
561
|
+
},
|
|
562
|
+
async exchangeOAuth(input) {
|
|
563
|
+
const tokens = await exchangeAuthorizationCode({
|
|
564
|
+
tokenUrl: TOKEN_URL2,
|
|
565
|
+
clientId,
|
|
566
|
+
clientSecret,
|
|
567
|
+
code: input.code,
|
|
568
|
+
codeVerifier: input.codeVerifier,
|
|
569
|
+
redirectUri: input.redirectUri
|
|
570
|
+
});
|
|
571
|
+
return {
|
|
572
|
+
credentials: {
|
|
573
|
+
kind: "oauth2",
|
|
574
|
+
accessToken: tokens.accessToken,
|
|
575
|
+
refreshToken: tokens.refreshToken,
|
|
576
|
+
expiresAt: tokens.expiresIn ? Date.now() + tokens.expiresIn * 1e3 : void 0
|
|
577
|
+
},
|
|
578
|
+
scopes: tokens.scope?.split(/\s+/) ?? scopes,
|
|
579
|
+
metadata: {}
|
|
580
|
+
};
|
|
581
|
+
},
|
|
582
|
+
async refreshToken(creds) {
|
|
583
|
+
if (creds.kind !== "oauth2" || !creds.refreshToken) {
|
|
584
|
+
throw new Error("google-drive.refreshToken: missing refresh token");
|
|
585
|
+
}
|
|
586
|
+
const refreshed = await refreshAccessToken({
|
|
587
|
+
tokenUrl: TOKEN_URL2,
|
|
588
|
+
clientId,
|
|
589
|
+
clientSecret,
|
|
590
|
+
refreshToken: creds.refreshToken
|
|
591
|
+
});
|
|
592
|
+
return {
|
|
593
|
+
kind: "oauth2",
|
|
594
|
+
accessToken: refreshed.accessToken,
|
|
595
|
+
refreshToken: refreshed.refreshToken ?? creds.refreshToken,
|
|
596
|
+
expiresAt: refreshed.expiresIn ? Date.now() + refreshed.expiresIn * 1e3 : void 0
|
|
597
|
+
};
|
|
598
|
+
},
|
|
599
|
+
async test(source) {
|
|
600
|
+
try {
|
|
601
|
+
const accessToken = await ensureFreshAccessToken2(source.credentials, clientId, clientSecret);
|
|
602
|
+
const res = await fetch(`${API}/about?fields=user`, {
|
|
603
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
604
|
+
signal: AbortSignal.timeout(8e3)
|
|
605
|
+
});
|
|
606
|
+
if (res.status === 401 || res.status === 403) {
|
|
607
|
+
return { ok: false, reason: `Google rejected Drive token (${res.status}) \u2014 reconnect required` };
|
|
608
|
+
}
|
|
609
|
+
if (!res.ok) return { ok: false, reason: `Google Drive returned ${res.status}` };
|
|
610
|
+
return { ok: true };
|
|
611
|
+
} catch (err) {
|
|
612
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
};
|
|
616
|
+
return adapter;
|
|
617
|
+
}
|
|
618
|
+
async function listFiles(inv, accessToken, timeoutMs) {
|
|
619
|
+
const args = inv.args ?? {};
|
|
620
|
+
const q = [];
|
|
621
|
+
if (args.folderId) q.push(`'${args.folderId.replace(/'/g, "\\'")}' in parents`);
|
|
622
|
+
if (args.query) q.push(`(${args.query})`);
|
|
623
|
+
q.push("trashed = false");
|
|
624
|
+
const params = new URLSearchParams({
|
|
625
|
+
q: q.join(" and "),
|
|
626
|
+
pageSize: String(args.pageSize ?? 100),
|
|
627
|
+
fields: "nextPageToken, files(id,name,mimeType,modifiedTime,size,md5Checksum,parents)"
|
|
628
|
+
});
|
|
629
|
+
if (args.pageToken) params.set("pageToken", args.pageToken);
|
|
630
|
+
const res = await fetch(`${API}/files?${params.toString()}`, {
|
|
631
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
632
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
633
|
+
});
|
|
634
|
+
if (res.status === 401 || res.status === 403) {
|
|
635
|
+
throw new CredentialsExpired(`Google Drive rejected token (${res.status})`, inv.source.id);
|
|
636
|
+
}
|
|
637
|
+
if (!res.ok) {
|
|
638
|
+
const text = await res.text().catch(() => "");
|
|
639
|
+
throw new Error(`google-drive list_files ${res.status}: ${text.slice(0, 200)}`);
|
|
640
|
+
}
|
|
641
|
+
const json = await res.json();
|
|
642
|
+
return {
|
|
643
|
+
data: { files: json.files ?? [], nextPageToken: json.nextPageToken },
|
|
644
|
+
fetchedAt: Date.now()
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
var GOOGLE_NATIVE_DEFAULTS = {
|
|
648
|
+
"application/vnd.google-apps.document": "text/plain",
|
|
649
|
+
"application/vnd.google-apps.spreadsheet": "text/csv",
|
|
650
|
+
"application/vnd.google-apps.presentation": "application/pdf"
|
|
651
|
+
};
|
|
652
|
+
async function readFile(inv, accessToken, timeoutMs) {
|
|
653
|
+
const { fileId, exportMime } = inv.args ?? {};
|
|
654
|
+
const metaRes = await fetch(`${API}/files/${encodeURIComponent(fileId)}?fields=id,name,mimeType,modifiedTime`, {
|
|
655
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
656
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
657
|
+
});
|
|
658
|
+
if (metaRes.status === 401 || metaRes.status === 403) {
|
|
659
|
+
throw new CredentialsExpired(`Google Drive rejected token (${metaRes.status})`, inv.source.id);
|
|
660
|
+
}
|
|
661
|
+
if (metaRes.status === 404) {
|
|
662
|
+
throw new Error(`google-drive read_file: file ${fileId} not found`);
|
|
663
|
+
}
|
|
664
|
+
if (!metaRes.ok) {
|
|
665
|
+
const text = await metaRes.text().catch(() => "");
|
|
666
|
+
throw new Error(`google-drive read_file meta ${metaRes.status}: ${text.slice(0, 200)}`);
|
|
667
|
+
}
|
|
668
|
+
const meta = await metaRes.json();
|
|
669
|
+
const isNative = meta.mimeType.startsWith("application/vnd.google-apps.");
|
|
670
|
+
const fetchedAt = Date.now();
|
|
671
|
+
if (isNative) {
|
|
672
|
+
const targetMime = exportMime ?? GOOGLE_NATIVE_DEFAULTS[meta.mimeType] ?? "text/plain";
|
|
673
|
+
const res2 = await fetch(`${API}/files/${encodeURIComponent(fileId)}/export?mimeType=${encodeURIComponent(targetMime)}`, {
|
|
674
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
675
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
676
|
+
});
|
|
677
|
+
if (!res2.ok) {
|
|
678
|
+
const text = await res2.text().catch(() => "");
|
|
679
|
+
throw new Error(`google-drive read_file export ${res2.status}: ${text.slice(0, 200)}`);
|
|
680
|
+
}
|
|
681
|
+
const isTextLike2 = /^text\/|application\/(json|xml|csv|javascript)/.test(targetMime);
|
|
682
|
+
if (isTextLike2) {
|
|
683
|
+
const content = await res2.text();
|
|
684
|
+
return {
|
|
685
|
+
data: { name: meta.name, mimeType: targetMime, content, encoding: "utf-8", modifiedTime: meta.modifiedTime },
|
|
686
|
+
fetchedAt
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
const buf2 = Buffer.from(await res2.arrayBuffer());
|
|
690
|
+
return {
|
|
691
|
+
data: { name: meta.name, mimeType: targetMime, content: buf2.toString("base64"), encoding: "base64", modifiedTime: meta.modifiedTime },
|
|
692
|
+
fetchedAt
|
|
693
|
+
};
|
|
694
|
+
}
|
|
695
|
+
const res = await fetch(`${API}/files/${encodeURIComponent(fileId)}?alt=media`, {
|
|
696
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
697
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
698
|
+
});
|
|
699
|
+
if (!res.ok) {
|
|
700
|
+
const text = await res.text().catch(() => "");
|
|
701
|
+
throw new Error(`google-drive read_file media ${res.status}: ${text.slice(0, 200)}`);
|
|
702
|
+
}
|
|
703
|
+
const isTextLike = /^text\/|application\/(json|xml|csv|javascript)/.test(meta.mimeType);
|
|
704
|
+
if (isTextLike) {
|
|
705
|
+
const content = await res.text();
|
|
706
|
+
return {
|
|
707
|
+
data: { name: meta.name, mimeType: meta.mimeType, content, encoding: "utf-8", modifiedTime: meta.modifiedTime },
|
|
708
|
+
fetchedAt
|
|
709
|
+
};
|
|
710
|
+
}
|
|
711
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
712
|
+
return {
|
|
713
|
+
data: { name: meta.name, mimeType: meta.mimeType, content: buf.toString("base64"), encoding: "base64", modifiedTime: meta.modifiedTime },
|
|
714
|
+
fetchedAt
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
async function watchFolder(inv, accessToken, timeoutMs) {
|
|
718
|
+
const { folderId, channelId, address, ttlMs } = inv.args;
|
|
719
|
+
const body = {
|
|
720
|
+
id: channelId,
|
|
721
|
+
type: "web_hook",
|
|
722
|
+
address
|
|
723
|
+
};
|
|
724
|
+
if (ttlMs && ttlMs > 0) body.expiration = String(Date.now() + ttlMs);
|
|
725
|
+
const res = await fetch(`${API}/files/${encodeURIComponent(folderId)}/watch`, {
|
|
726
|
+
method: "POST",
|
|
727
|
+
headers: {
|
|
728
|
+
authorization: `Bearer ${accessToken}`,
|
|
729
|
+
"content-type": "application/json"
|
|
730
|
+
},
|
|
731
|
+
body: JSON.stringify(body),
|
|
732
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
733
|
+
});
|
|
734
|
+
if (res.status === 401 || res.status === 403) {
|
|
735
|
+
throw new CredentialsExpired(`Google Drive rejected token (${res.status})`, inv.source.id);
|
|
736
|
+
}
|
|
737
|
+
if (res.status === 409) {
|
|
738
|
+
const cached = inv.source.metadata.watchedChannels?.[channelId];
|
|
739
|
+
return {
|
|
740
|
+
status: "committed",
|
|
741
|
+
data: { channelId, resourceId: cached?.resourceId, expiration: cached?.expiration },
|
|
742
|
+
committedAt: Date.now(),
|
|
743
|
+
idempotentReplay: true
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
if (!res.ok) {
|
|
747
|
+
const text = await res.text().catch(() => "");
|
|
748
|
+
throw new Error(`google-drive watch_folder ${res.status}: ${text.slice(0, 200)}`);
|
|
749
|
+
}
|
|
750
|
+
const json = await res.json();
|
|
751
|
+
return {
|
|
752
|
+
status: "committed",
|
|
753
|
+
data: { channelId: json.id, resourceId: json.resourceId, expiration: json.expiration },
|
|
754
|
+
committedAt: Date.now(),
|
|
755
|
+
idempotentReplay: false
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
async function ensureFreshAccessToken2(creds, clientId, clientSecret) {
|
|
759
|
+
if (creds.kind !== "oauth2") {
|
|
760
|
+
throw new Error("google-drive: expected oauth2 credentials");
|
|
761
|
+
}
|
|
762
|
+
if (creds.accessToken && (!creds.expiresAt || creds.expiresAt > Date.now() + 6e4)) {
|
|
763
|
+
return creds.accessToken;
|
|
764
|
+
}
|
|
765
|
+
if (!creds.refreshToken) {
|
|
766
|
+
throw new CredentialsExpired("Google Drive access token expired and no refresh token", "");
|
|
767
|
+
}
|
|
768
|
+
const refreshed = await refreshAccessToken({
|
|
769
|
+
tokenUrl: TOKEN_URL2,
|
|
770
|
+
clientId,
|
|
771
|
+
clientSecret,
|
|
772
|
+
refreshToken: creds.refreshToken
|
|
773
|
+
});
|
|
774
|
+
creds.accessToken = refreshed.accessToken;
|
|
775
|
+
creds.expiresAt = refreshed.expiresIn ? Date.now() + refreshed.expiresIn * 1e3 : void 0;
|
|
776
|
+
if (refreshed.refreshToken) creds.refreshToken = refreshed.refreshToken;
|
|
777
|
+
return creds.accessToken;
|
|
778
|
+
}
|
|
779
|
+
|
|
465
780
|
// src/connectors/adapters/google-sheets.ts
|
|
466
781
|
import { createHash as createHash2 } from "crypto";
|
|
467
782
|
var SCOPES2 = ["https://www.googleapis.com/auth/spreadsheets"];
|
|
468
|
-
var
|
|
469
|
-
var
|
|
783
|
+
var AUTH_URL3 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
784
|
+
var TOKEN_URL3 = "https://oauth2.googleapis.com/token";
|
|
470
785
|
function googleSheets(opts) {
|
|
471
786
|
const { clientId, clientSecret } = opts;
|
|
472
787
|
const adapter = {
|
|
@@ -476,8 +791,8 @@ function googleSheets(opts) {
|
|
|
476
791
|
description: "Bind your agent's knowledge base or pricing/availability lookup to a live Google Sheet. Edit the sheet, and the agent picks up changes \u2014 no redeploys.",
|
|
477
792
|
auth: {
|
|
478
793
|
kind: "oauth2",
|
|
479
|
-
authorizationUrl:
|
|
480
|
-
tokenUrl:
|
|
794
|
+
authorizationUrl: AUTH_URL3,
|
|
795
|
+
tokenUrl: TOKEN_URL3,
|
|
481
796
|
scopes: SCOPES2,
|
|
482
797
|
clientIdEnv: "GOOGLE_OAUTH_CLIENT_ID",
|
|
483
798
|
clientSecretEnv: "GOOGLE_OAUTH_CLIENT_SECRET",
|
|
@@ -544,7 +859,7 @@ function googleSheets(opts) {
|
|
|
544
859
|
},
|
|
545
860
|
async executeRead(inv) {
|
|
546
861
|
const meta = readSheetMeta(inv.source.metadata);
|
|
547
|
-
const accessToken = await
|
|
862
|
+
const accessToken = await ensureFreshAccessToken3(inv.source.credentials, clientId, clientSecret);
|
|
548
863
|
const rows = await fetchAllRows(accessToken, meta);
|
|
549
864
|
const limit = clampLimit(inv.args.limit, 100);
|
|
550
865
|
let filtered = rows;
|
|
@@ -571,7 +886,7 @@ function googleSheets(opts) {
|
|
|
571
886
|
throw new Error(`google-sheets: unknown mutation ${inv.capabilityName}`);
|
|
572
887
|
}
|
|
573
888
|
const meta = readSheetMeta(inv.source.metadata);
|
|
574
|
-
const accessToken = await
|
|
889
|
+
const accessToken = await ensureFreshAccessToken3(inv.source.credentials, clientId, clientSecret);
|
|
575
890
|
const { rowKey, patch, expectedFingerprint } = inv.args;
|
|
576
891
|
const rows = await fetchAllRows(accessToken, meta);
|
|
577
892
|
const target = rows.find((r) => normalizeKey(r.values[meta.keyColumn]) === normalizeKey(rowKey));
|
|
@@ -627,7 +942,7 @@ function googleSheets(opts) {
|
|
|
627
942
|
},
|
|
628
943
|
async exchangeOAuth(input) {
|
|
629
944
|
const tokens = await exchangeAuthorizationCode({
|
|
630
|
-
tokenUrl:
|
|
945
|
+
tokenUrl: TOKEN_URL3,
|
|
631
946
|
clientId,
|
|
632
947
|
clientSecret,
|
|
633
948
|
code: input.code,
|
|
@@ -652,7 +967,7 @@ function googleSheets(opts) {
|
|
|
652
967
|
throw new Error("google-sheets.refreshToken: missing refresh token");
|
|
653
968
|
}
|
|
654
969
|
const refreshed = await refreshAccessToken({
|
|
655
|
-
tokenUrl:
|
|
970
|
+
tokenUrl: TOKEN_URL3,
|
|
656
971
|
clientId,
|
|
657
972
|
clientSecret,
|
|
658
973
|
refreshToken: creds.refreshToken
|
|
@@ -666,7 +981,7 @@ function googleSheets(opts) {
|
|
|
666
981
|
},
|
|
667
982
|
async test(source) {
|
|
668
983
|
try {
|
|
669
|
-
const accessToken = await
|
|
984
|
+
const accessToken = await ensureFreshAccessToken3(source.credentials, clientId, clientSecret);
|
|
670
985
|
const meta = readSheetMeta(source.metadata);
|
|
671
986
|
if (!meta.spreadsheetId) {
|
|
672
987
|
return { ok: false, reason: "spreadsheetId not configured \u2014 pick a sheet in the connection settings" };
|
|
@@ -764,7 +1079,7 @@ function columnIndexToLetter(idx) {
|
|
|
764
1079
|
}
|
|
765
1080
|
return s;
|
|
766
1081
|
}
|
|
767
|
-
async function
|
|
1082
|
+
async function ensureFreshAccessToken3(creds, clientId, clientSecret) {
|
|
768
1083
|
if (creds.kind !== "oauth2") {
|
|
769
1084
|
throw new Error("google-sheets: expected oauth2 credentials");
|
|
770
1085
|
}
|
|
@@ -775,7 +1090,403 @@ async function ensureFreshAccessToken2(creds, clientId, clientSecret) {
|
|
|
775
1090
|
throw new CredentialsExpired("Google Sheets access token expired and no refresh token", "");
|
|
776
1091
|
}
|
|
777
1092
|
const refreshed = await refreshAccessToken({
|
|
778
|
-
tokenUrl:
|
|
1093
|
+
tokenUrl: TOKEN_URL3,
|
|
1094
|
+
clientId,
|
|
1095
|
+
clientSecret,
|
|
1096
|
+
refreshToken: creds.refreshToken
|
|
1097
|
+
});
|
|
1098
|
+
creds.accessToken = refreshed.accessToken;
|
|
1099
|
+
creds.expiresAt = refreshed.expiresIn ? Date.now() + refreshed.expiresIn * 1e3 : void 0;
|
|
1100
|
+
if (refreshed.refreshToken) creds.refreshToken = refreshed.refreshToken;
|
|
1101
|
+
return creds.accessToken;
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
// src/connectors/adapters/gmail.ts
|
|
1105
|
+
var SCOPE_READ = "https://www.googleapis.com/auth/gmail.readonly";
|
|
1106
|
+
var SCOPE_SEND = "https://www.googleapis.com/auth/gmail.send";
|
|
1107
|
+
var SCOPE_MODIFY = "https://www.googleapis.com/auth/gmail.modify";
|
|
1108
|
+
var AUTH_URL4 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
1109
|
+
var TOKEN_URL4 = "https://oauth2.googleapis.com/token";
|
|
1110
|
+
var API2 = "https://gmail.googleapis.com/gmail/v1/users/me";
|
|
1111
|
+
function gmail(opts) {
|
|
1112
|
+
const { clientId, clientSecret } = opts;
|
|
1113
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1114
|
+
const scopes = opts.scopes ?? [SCOPE_READ, SCOPE_SEND, SCOPE_MODIFY];
|
|
1115
|
+
const adapter = {
|
|
1116
|
+
manifest: {
|
|
1117
|
+
kind: "gmail",
|
|
1118
|
+
displayName: "Gmail",
|
|
1119
|
+
description: "Read inbox messages by label or query, fetch a single message including MIME bodies and attachment manifests, reply on a thread, and watch a label for new mail (Cloud Pub/Sub push).",
|
|
1120
|
+
auth: {
|
|
1121
|
+
kind: "oauth2",
|
|
1122
|
+
authorizationUrl: AUTH_URL4,
|
|
1123
|
+
tokenUrl: TOKEN_URL4,
|
|
1124
|
+
scopes,
|
|
1125
|
+
clientIdEnv: "GOOGLE_OAUTH_CLIENT_ID",
|
|
1126
|
+
clientSecretEnv: "GOOGLE_OAUTH_CLIENT_SECRET",
|
|
1127
|
+
extraAuthParams: { access_type: "offline", prompt: "consent", include_granted_scopes: "true" }
|
|
1128
|
+
},
|
|
1129
|
+
category: "comms",
|
|
1130
|
+
defaultConsistencyModel: "authoritative",
|
|
1131
|
+
rateLimit: { requests: 250, windowMs: 1e3, scope: "oauth-client" },
|
|
1132
|
+
capabilities: [
|
|
1133
|
+
{
|
|
1134
|
+
name: "list_messages",
|
|
1135
|
+
class: "read",
|
|
1136
|
+
description: "List inbox messages. Filter by labelIds (default INBOX) and/or a Gmail query (e.g., 'from:billing@stripe.com newer_than:7d'). Returns headers (from/to/subject/date) not bodies.",
|
|
1137
|
+
requiredScopes: [SCOPE_READ],
|
|
1138
|
+
parameters: {
|
|
1139
|
+
type: "object",
|
|
1140
|
+
properties: {
|
|
1141
|
+
labelIds: { type: "array", items: { type: "string" }, description: 'Default: ["INBOX"]' },
|
|
1142
|
+
query: { type: "string", description: "Gmail query syntax." },
|
|
1143
|
+
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 25 },
|
|
1144
|
+
pageToken: { type: "string" }
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
name: "read_message",
|
|
1150
|
+
class: "read",
|
|
1151
|
+
description: "Read a single Gmail message including parsed text and html bodies and a flat manifest of attachments. Bodies are inlined; attachment bytes are not.",
|
|
1152
|
+
requiredScopes: [SCOPE_READ],
|
|
1153
|
+
parameters: {
|
|
1154
|
+
type: "object",
|
|
1155
|
+
properties: {
|
|
1156
|
+
id: { type: "string" }
|
|
1157
|
+
},
|
|
1158
|
+
required: ["id"]
|
|
1159
|
+
}
|
|
1160
|
+
},
|
|
1161
|
+
{
|
|
1162
|
+
name: "send_reply",
|
|
1163
|
+
class: "mutation",
|
|
1164
|
+
description: "Send a reply on a thread. Pulls In-Reply-To/References from the latest message in the thread. Body is text/plain.",
|
|
1165
|
+
cas: "native-idempotency",
|
|
1166
|
+
externalEffect: true,
|
|
1167
|
+
requiredScopes: [SCOPE_SEND, SCOPE_READ],
|
|
1168
|
+
parameters: {
|
|
1169
|
+
type: "object",
|
|
1170
|
+
properties: {
|
|
1171
|
+
threadId: { type: "string" },
|
|
1172
|
+
body: { type: "string", description: "text/plain body" },
|
|
1173
|
+
replyAll: { type: "boolean", default: false },
|
|
1174
|
+
cc: { type: "array", items: { type: "string" } }
|
|
1175
|
+
},
|
|
1176
|
+
required: ["threadId", "body"]
|
|
1177
|
+
}
|
|
1178
|
+
},
|
|
1179
|
+
{
|
|
1180
|
+
name: "watch_label",
|
|
1181
|
+
class: "mutation",
|
|
1182
|
+
description: "Register a Cloud Pub/Sub topic to receive push notifications when a label changes. Returns the upstream historyId + expiration. Re-issue every 7 days.",
|
|
1183
|
+
cas: "native-idempotency",
|
|
1184
|
+
externalEffect: true,
|
|
1185
|
+
requiredScopes: [SCOPE_MODIFY],
|
|
1186
|
+
parameters: {
|
|
1187
|
+
type: "object",
|
|
1188
|
+
properties: {
|
|
1189
|
+
labelIds: { type: "array", items: { type: "string" }, description: 'Default: ["INBOX"]' },
|
|
1190
|
+
topicName: { type: "string", description: "projects/<id>/topics/<name>" },
|
|
1191
|
+
labelFilterAction: { type: "string", enum: ["include", "exclude"], default: "include" }
|
|
1192
|
+
},
|
|
1193
|
+
required: ["topicName"]
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
]
|
|
1197
|
+
},
|
|
1198
|
+
async executeRead(inv) {
|
|
1199
|
+
const accessToken = await ensureFreshAccessToken4(inv.source.credentials, clientId, clientSecret);
|
|
1200
|
+
if (inv.capabilityName === "list_messages") return listMessages(inv, accessToken, timeoutMs);
|
|
1201
|
+
if (inv.capabilityName === "read_message") return readMessage(inv, accessToken, timeoutMs);
|
|
1202
|
+
throw new Error(`gmail: unknown read capability ${inv.capabilityName}`);
|
|
1203
|
+
},
|
|
1204
|
+
async executeMutation(inv) {
|
|
1205
|
+
const accessToken = await ensureFreshAccessToken4(inv.source.credentials, clientId, clientSecret);
|
|
1206
|
+
if (inv.capabilityName === "send_reply") return sendReply(inv, accessToken, timeoutMs);
|
|
1207
|
+
if (inv.capabilityName === "watch_label") return watchLabel(inv, accessToken, timeoutMs);
|
|
1208
|
+
throw new Error(`gmail: unknown mutation capability ${inv.capabilityName}`);
|
|
1209
|
+
},
|
|
1210
|
+
async exchangeOAuth(input) {
|
|
1211
|
+
const tokens = await exchangeAuthorizationCode({
|
|
1212
|
+
tokenUrl: TOKEN_URL4,
|
|
1213
|
+
clientId,
|
|
1214
|
+
clientSecret,
|
|
1215
|
+
code: input.code,
|
|
1216
|
+
codeVerifier: input.codeVerifier,
|
|
1217
|
+
redirectUri: input.redirectUri
|
|
1218
|
+
});
|
|
1219
|
+
return {
|
|
1220
|
+
credentials: {
|
|
1221
|
+
kind: "oauth2",
|
|
1222
|
+
accessToken: tokens.accessToken,
|
|
1223
|
+
refreshToken: tokens.refreshToken,
|
|
1224
|
+
expiresAt: tokens.expiresIn ? Date.now() + tokens.expiresIn * 1e3 : void 0
|
|
1225
|
+
},
|
|
1226
|
+
scopes: tokens.scope?.split(/\s+/) ?? scopes,
|
|
1227
|
+
metadata: {}
|
|
1228
|
+
};
|
|
1229
|
+
},
|
|
1230
|
+
async refreshToken(creds) {
|
|
1231
|
+
if (creds.kind !== "oauth2" || !creds.refreshToken) {
|
|
1232
|
+
throw new Error("gmail.refreshToken: missing refresh token");
|
|
1233
|
+
}
|
|
1234
|
+
const refreshed = await refreshAccessToken({
|
|
1235
|
+
tokenUrl: TOKEN_URL4,
|
|
1236
|
+
clientId,
|
|
1237
|
+
clientSecret,
|
|
1238
|
+
refreshToken: creds.refreshToken
|
|
1239
|
+
});
|
|
1240
|
+
return {
|
|
1241
|
+
kind: "oauth2",
|
|
1242
|
+
accessToken: refreshed.accessToken,
|
|
1243
|
+
refreshToken: refreshed.refreshToken ?? creds.refreshToken,
|
|
1244
|
+
expiresAt: refreshed.expiresIn ? Date.now() + refreshed.expiresIn * 1e3 : void 0
|
|
1245
|
+
};
|
|
1246
|
+
},
|
|
1247
|
+
async test(source) {
|
|
1248
|
+
try {
|
|
1249
|
+
const accessToken = await ensureFreshAccessToken4(source.credentials, clientId, clientSecret);
|
|
1250
|
+
const res = await fetch(`${API2}/profile`, {
|
|
1251
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1252
|
+
signal: AbortSignal.timeout(8e3)
|
|
1253
|
+
});
|
|
1254
|
+
if (res.status === 401 || res.status === 403) {
|
|
1255
|
+
return { ok: false, reason: `Google rejected Gmail token (${res.status}) \u2014 reconnect required` };
|
|
1256
|
+
}
|
|
1257
|
+
if (!res.ok) return { ok: false, reason: `Gmail returned ${res.status}` };
|
|
1258
|
+
return { ok: true };
|
|
1259
|
+
} catch (err) {
|
|
1260
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
1261
|
+
}
|
|
1262
|
+
}
|
|
1263
|
+
};
|
|
1264
|
+
return adapter;
|
|
1265
|
+
}
|
|
1266
|
+
async function listMessages(inv, accessToken, timeoutMs) {
|
|
1267
|
+
const args = inv.args ?? {};
|
|
1268
|
+
const params = new URLSearchParams({
|
|
1269
|
+
maxResults: String(args.maxResults ?? 25)
|
|
1270
|
+
});
|
|
1271
|
+
for (const id of args.labelIds ?? ["INBOX"]) params.append("labelIds", id);
|
|
1272
|
+
if (args.query) params.set("q", args.query);
|
|
1273
|
+
if (args.pageToken) params.set("pageToken", args.pageToken);
|
|
1274
|
+
const listRes = await fetch(`${API2}/messages?${params.toString()}`, {
|
|
1275
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1276
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1277
|
+
});
|
|
1278
|
+
if (listRes.status === 401 || listRes.status === 403) {
|
|
1279
|
+
throw new CredentialsExpired(`Gmail rejected token (${listRes.status})`, inv.source.id);
|
|
1280
|
+
}
|
|
1281
|
+
if (!listRes.ok) {
|
|
1282
|
+
const text = await listRes.text().catch(() => "");
|
|
1283
|
+
throw new Error(`gmail list_messages ${listRes.status}: ${text.slice(0, 200)}`);
|
|
1284
|
+
}
|
|
1285
|
+
const listJson = await listRes.json();
|
|
1286
|
+
const ids = listJson.messages ?? [];
|
|
1287
|
+
const metas = await Promise.all(
|
|
1288
|
+
ids.map(async ({ id }) => {
|
|
1289
|
+
const res = await fetch(`${API2}/messages/${encodeURIComponent(id)}?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Subject&metadataHeaders=Date`, {
|
|
1290
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1291
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1292
|
+
});
|
|
1293
|
+
if (!res.ok) return null;
|
|
1294
|
+
return await res.json();
|
|
1295
|
+
})
|
|
1296
|
+
);
|
|
1297
|
+
const messages = metas.filter((m) => Boolean(m)).map(toMessageSummary);
|
|
1298
|
+
return {
|
|
1299
|
+
data: { messages, nextPageToken: listJson.nextPageToken },
|
|
1300
|
+
fetchedAt: Date.now()
|
|
1301
|
+
};
|
|
1302
|
+
}
|
|
1303
|
+
function toMessageSummary(meta) {
|
|
1304
|
+
const headers = new Map((meta.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
|
|
1305
|
+
return {
|
|
1306
|
+
id: meta.id,
|
|
1307
|
+
threadId: meta.threadId,
|
|
1308
|
+
snippet: meta.snippet,
|
|
1309
|
+
internalDate: meta.internalDate,
|
|
1310
|
+
labelIds: meta.labelIds ?? [],
|
|
1311
|
+
from: headers.get("from"),
|
|
1312
|
+
to: headers.get("to"),
|
|
1313
|
+
subject: headers.get("subject"),
|
|
1314
|
+
date: headers.get("date")
|
|
1315
|
+
};
|
|
1316
|
+
}
|
|
1317
|
+
async function readMessage(inv, accessToken, timeoutMs) {
|
|
1318
|
+
const { id } = inv.args;
|
|
1319
|
+
const res = await fetch(`${API2}/messages/${encodeURIComponent(id)}?format=full`, {
|
|
1320
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1321
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1322
|
+
});
|
|
1323
|
+
if (res.status === 401 || res.status === 403) {
|
|
1324
|
+
throw new CredentialsExpired(`Gmail rejected token (${res.status})`, inv.source.id);
|
|
1325
|
+
}
|
|
1326
|
+
if (res.status === 404) {
|
|
1327
|
+
throw new Error(`gmail read_message: message ${id} not found`);
|
|
1328
|
+
}
|
|
1329
|
+
if (!res.ok) {
|
|
1330
|
+
const text = await res.text().catch(() => "");
|
|
1331
|
+
throw new Error(`gmail read_message ${res.status}: ${text.slice(0, 200)}`);
|
|
1332
|
+
}
|
|
1333
|
+
const full = await res.json();
|
|
1334
|
+
const headers = new Map((full.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
|
|
1335
|
+
const body = {};
|
|
1336
|
+
const attachments = [];
|
|
1337
|
+
walkParts(full.payload, body, attachments);
|
|
1338
|
+
return {
|
|
1339
|
+
data: {
|
|
1340
|
+
id: full.id,
|
|
1341
|
+
threadId: full.threadId,
|
|
1342
|
+
internalDate: full.internalDate,
|
|
1343
|
+
labelIds: full.labelIds ?? [],
|
|
1344
|
+
from: headers.get("from"),
|
|
1345
|
+
to: headers.get("to"),
|
|
1346
|
+
cc: headers.get("cc"),
|
|
1347
|
+
subject: headers.get("subject"),
|
|
1348
|
+
date: headers.get("date"),
|
|
1349
|
+
body,
|
|
1350
|
+
attachments
|
|
1351
|
+
},
|
|
1352
|
+
fetchedAt: Date.now()
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
function walkParts(part, body, attachments) {
|
|
1356
|
+
if (!part) return;
|
|
1357
|
+
if (part.body?.data && part.mimeType === "text/plain" && !body.text) {
|
|
1358
|
+
body.text = decodeBase64Url(part.body.data);
|
|
1359
|
+
} else if (part.body?.data && part.mimeType === "text/html" && !body.html) {
|
|
1360
|
+
body.html = decodeBase64Url(part.body.data);
|
|
1361
|
+
}
|
|
1362
|
+
if (part.filename && part.body?.attachmentId) {
|
|
1363
|
+
attachments.push({
|
|
1364
|
+
filename: part.filename,
|
|
1365
|
+
mimeType: part.mimeType ?? "application/octet-stream",
|
|
1366
|
+
attachmentId: part.body.attachmentId,
|
|
1367
|
+
size: part.body.size ?? 0
|
|
1368
|
+
});
|
|
1369
|
+
}
|
|
1370
|
+
for (const child of part.parts ?? []) walkParts(child, body, attachments);
|
|
1371
|
+
}
|
|
1372
|
+
function decodeBase64Url(s) {
|
|
1373
|
+
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8");
|
|
1374
|
+
}
|
|
1375
|
+
function encodeBase64Url(s) {
|
|
1376
|
+
return Buffer.from(s, "utf-8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1377
|
+
}
|
|
1378
|
+
async function sendReply(inv, accessToken, timeoutMs) {
|
|
1379
|
+
const { threadId, body, replyAll, cc } = inv.args;
|
|
1380
|
+
const threadRes = await fetch(`${API2}/threads/${encodeURIComponent(threadId)}?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Cc&metadataHeaders=Subject&metadataHeaders=Message-ID&metadataHeaders=References`, {
|
|
1381
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1382
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1383
|
+
});
|
|
1384
|
+
if (threadRes.status === 401 || threadRes.status === 403) {
|
|
1385
|
+
throw new CredentialsExpired(`Gmail rejected token (${threadRes.status})`, inv.source.id);
|
|
1386
|
+
}
|
|
1387
|
+
if (!threadRes.ok) {
|
|
1388
|
+
const text = await threadRes.text().catch(() => "");
|
|
1389
|
+
throw new Error(`gmail send_reply thread fetch ${threadRes.status}: ${text.slice(0, 200)}`);
|
|
1390
|
+
}
|
|
1391
|
+
const thread = await threadRes.json();
|
|
1392
|
+
const last = thread.messages?.[thread.messages.length - 1];
|
|
1393
|
+
if (!last) throw new Error(`gmail send_reply: thread ${threadId} has no messages`);
|
|
1394
|
+
const lastHeaders = new Map((last.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
|
|
1395
|
+
const inReplyTo = lastHeaders.get("message-id");
|
|
1396
|
+
const refsHeader = lastHeaders.get("references");
|
|
1397
|
+
const refs = refsHeader ? `${refsHeader} ${inReplyTo ?? ""}`.trim() : inReplyTo;
|
|
1398
|
+
const fromHeader = lastHeaders.get("from");
|
|
1399
|
+
const toHeader = lastHeaders.get("to");
|
|
1400
|
+
const ccHeader = lastHeaders.get("cc");
|
|
1401
|
+
const subject = lastHeaders.get("subject") ?? "";
|
|
1402
|
+
const rfcHeaders = [];
|
|
1403
|
+
if (fromHeader) rfcHeaders.push(`To: ${fromHeader}`);
|
|
1404
|
+
if (replyAll) {
|
|
1405
|
+
const extra = [toHeader, ccHeader].filter(Boolean).join(", ");
|
|
1406
|
+
if (extra) rfcHeaders.push(`Cc: ${extra}`);
|
|
1407
|
+
}
|
|
1408
|
+
if (cc?.length) {
|
|
1409
|
+
const existing = rfcHeaders.findIndex((h) => h.startsWith("Cc: "));
|
|
1410
|
+
if (existing >= 0) rfcHeaders[existing] = `${rfcHeaders[existing]}, ${cc.join(", ")}`;
|
|
1411
|
+
else rfcHeaders.push(`Cc: ${cc.join(", ")}`);
|
|
1412
|
+
}
|
|
1413
|
+
rfcHeaders.push(`Subject: ${subject.toLowerCase().startsWith("re:") ? subject : "Re: " + subject}`);
|
|
1414
|
+
if (inReplyTo) rfcHeaders.push(`In-Reply-To: ${inReplyTo}`);
|
|
1415
|
+
if (refs) rfcHeaders.push(`References: ${refs}`);
|
|
1416
|
+
rfcHeaders.push(`X-Tangle-Idempotency-Key: ${inv.idempotencyKey}`);
|
|
1417
|
+
rfcHeaders.push('Content-Type: text/plain; charset="UTF-8"');
|
|
1418
|
+
rfcHeaders.push("MIME-Version: 1.0");
|
|
1419
|
+
const raw = `${rfcHeaders.join("\r\n")}\r
|
|
1420
|
+
\r
|
|
1421
|
+
${body}`;
|
|
1422
|
+
const sendBody = { threadId, raw: encodeBase64Url(raw) };
|
|
1423
|
+
const sendRes = await fetch(`${API2}/messages/send`, {
|
|
1424
|
+
method: "POST",
|
|
1425
|
+
headers: {
|
|
1426
|
+
authorization: `Bearer ${accessToken}`,
|
|
1427
|
+
"content-type": "application/json"
|
|
1428
|
+
},
|
|
1429
|
+
body: JSON.stringify(sendBody),
|
|
1430
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1431
|
+
});
|
|
1432
|
+
if (sendRes.status === 401 || sendRes.status === 403) {
|
|
1433
|
+
throw new CredentialsExpired(`Gmail rejected token (${sendRes.status})`, inv.source.id);
|
|
1434
|
+
}
|
|
1435
|
+
if (!sendRes.ok) {
|
|
1436
|
+
const text = await sendRes.text().catch(() => "");
|
|
1437
|
+
throw new Error(`gmail send_reply ${sendRes.status}: ${text.slice(0, 200)}`);
|
|
1438
|
+
}
|
|
1439
|
+
const sent = await sendRes.json();
|
|
1440
|
+
return {
|
|
1441
|
+
status: "committed",
|
|
1442
|
+
data: { id: sent.id, threadId: sent.threadId, labelIds: sent.labelIds ?? [] },
|
|
1443
|
+
committedAt: Date.now(),
|
|
1444
|
+
idempotentReplay: false
|
|
1445
|
+
};
|
|
1446
|
+
}
|
|
1447
|
+
async function watchLabel(inv, accessToken, timeoutMs) {
|
|
1448
|
+
const { labelIds, topicName, labelFilterAction } = inv.args;
|
|
1449
|
+
const body = {
|
|
1450
|
+
topicName,
|
|
1451
|
+
labelIds: labelIds ?? ["INBOX"],
|
|
1452
|
+
labelFilterAction: labelFilterAction ?? "include"
|
|
1453
|
+
};
|
|
1454
|
+
const res = await fetch(`${API2}/watch`, {
|
|
1455
|
+
method: "POST",
|
|
1456
|
+
headers: {
|
|
1457
|
+
authorization: `Bearer ${accessToken}`,
|
|
1458
|
+
"content-type": "application/json"
|
|
1459
|
+
},
|
|
1460
|
+
body: JSON.stringify(body),
|
|
1461
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1462
|
+
});
|
|
1463
|
+
if (res.status === 401 || res.status === 403) {
|
|
1464
|
+
throw new CredentialsExpired(`Gmail rejected token (${res.status})`, inv.source.id);
|
|
1465
|
+
}
|
|
1466
|
+
if (!res.ok) {
|
|
1467
|
+
const text = await res.text().catch(() => "");
|
|
1468
|
+
throw new Error(`gmail watch_label ${res.status}: ${text.slice(0, 200)}`);
|
|
1469
|
+
}
|
|
1470
|
+
const json = await res.json();
|
|
1471
|
+
return {
|
|
1472
|
+
status: "committed",
|
|
1473
|
+
data: { historyId: json.historyId, expiration: json.expiration, topicName, labelIds: body.labelIds },
|
|
1474
|
+
committedAt: Date.now(),
|
|
1475
|
+
idempotentReplay: false
|
|
1476
|
+
};
|
|
1477
|
+
}
|
|
1478
|
+
async function ensureFreshAccessToken4(creds, clientId, clientSecret) {
|
|
1479
|
+
if (creds.kind !== "oauth2") {
|
|
1480
|
+
throw new Error("gmail: expected oauth2 credentials");
|
|
1481
|
+
}
|
|
1482
|
+
if (creds.accessToken && (!creds.expiresAt || creds.expiresAt > Date.now() + 6e4)) {
|
|
1483
|
+
return creds.accessToken;
|
|
1484
|
+
}
|
|
1485
|
+
if (!creds.refreshToken) {
|
|
1486
|
+
throw new CredentialsExpired("Gmail access token expired and no refresh token", "");
|
|
1487
|
+
}
|
|
1488
|
+
const refreshed = await refreshAccessToken({
|
|
1489
|
+
tokenUrl: TOKEN_URL4,
|
|
779
1490
|
clientId,
|
|
780
1491
|
clientSecret,
|
|
781
1492
|
refreshToken: creds.refreshToken
|
|
@@ -794,8 +1505,8 @@ var SCOPES3 = [
|
|
|
794
1505
|
// connection silently dies after ~1 hour.
|
|
795
1506
|
"offline_access"
|
|
796
1507
|
];
|
|
797
|
-
var
|
|
798
|
-
var
|
|
1508
|
+
var AUTH_URL5 = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
|
|
1509
|
+
var TOKEN_URL5 = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
|
799
1510
|
function microsoftCalendar(opts) {
|
|
800
1511
|
const { clientId, clientSecret } = opts;
|
|
801
1512
|
const adapter = {
|
|
@@ -805,8 +1516,8 @@ function microsoftCalendar(opts) {
|
|
|
805
1516
|
description: "Let your agent check availability and book against an Outlook / Microsoft 365 calendar. Conflict-resolved via Graph's getSchedule pre-flight; etag-guarded on event updates.",
|
|
806
1517
|
auth: {
|
|
807
1518
|
kind: "oauth2",
|
|
808
|
-
authorizationUrl:
|
|
809
|
-
tokenUrl:
|
|
1519
|
+
authorizationUrl: AUTH_URL5,
|
|
1520
|
+
tokenUrl: TOKEN_URL5,
|
|
810
1521
|
scopes: SCOPES3,
|
|
811
1522
|
clientIdEnv: "MS_OAUTH_CLIENT_ID",
|
|
812
1523
|
clientSecretEnv: "MS_OAUTH_CLIENT_SECRET"
|
|
@@ -858,7 +1569,7 @@ function microsoftCalendar(opts) {
|
|
|
858
1569
|
}
|
|
859
1570
|
const userPrincipal = readMetaString2(inv.source.metadata, "userPrincipal");
|
|
860
1571
|
const { timeMin, timeMax } = inv.args;
|
|
861
|
-
const accessToken = await
|
|
1572
|
+
const accessToken = await ensureFreshAccessToken5(inv.source.credentials, clientId, clientSecret);
|
|
862
1573
|
const busy = await getScheduleBusy({ accessToken, userPrincipal, timeMin, timeMax });
|
|
863
1574
|
return {
|
|
864
1575
|
data: { busy },
|
|
@@ -871,7 +1582,7 @@ function microsoftCalendar(opts) {
|
|
|
871
1582
|
}
|
|
872
1583
|
const userPrincipal = readMetaString2(inv.source.metadata, "userPrincipal");
|
|
873
1584
|
const { start, end, summary, description, attendees } = inv.args;
|
|
874
|
-
const accessToken = await
|
|
1585
|
+
const accessToken = await ensureFreshAccessToken5(inv.source.credentials, clientId, clientSecret);
|
|
875
1586
|
const busy = await getScheduleBusy({ accessToken, userPrincipal, timeMin: start, timeMax: end });
|
|
876
1587
|
if (busy.length > 0) {
|
|
877
1588
|
const startMs = Date.parse(start);
|
|
@@ -936,7 +1647,7 @@ function microsoftCalendar(opts) {
|
|
|
936
1647
|
throw new Error("Microsoft OAuth client not configured (MS_OAUTH_CLIENT_ID / _SECRET)");
|
|
937
1648
|
}
|
|
938
1649
|
const tokens = await exchangeAuthorizationCode({
|
|
939
|
-
tokenUrl:
|
|
1650
|
+
tokenUrl: TOKEN_URL5,
|
|
940
1651
|
clientId,
|
|
941
1652
|
clientSecret,
|
|
942
1653
|
code: input.code,
|
|
@@ -961,7 +1672,7 @@ function microsoftCalendar(opts) {
|
|
|
961
1672
|
throw new Error("microsoft-calendar.refreshToken: missing refresh token");
|
|
962
1673
|
}
|
|
963
1674
|
const refreshed = await refreshAccessToken({
|
|
964
|
-
tokenUrl:
|
|
1675
|
+
tokenUrl: TOKEN_URL5,
|
|
965
1676
|
clientId,
|
|
966
1677
|
clientSecret,
|
|
967
1678
|
refreshToken: creds.refreshToken
|
|
@@ -975,7 +1686,7 @@ function microsoftCalendar(opts) {
|
|
|
975
1686
|
},
|
|
976
1687
|
async test(source) {
|
|
977
1688
|
try {
|
|
978
|
-
const accessToken = await
|
|
1689
|
+
const accessToken = await ensureFreshAccessToken5(source.credentials, clientId, clientSecret);
|
|
979
1690
|
const res = await fetch("https://graph.microsoft.com/v1.0/me?$select=id", {
|
|
980
1691
|
headers: { authorization: `Bearer ${accessToken}` },
|
|
981
1692
|
signal: AbortSignal.timeout(8e3)
|
|
@@ -1048,7 +1759,7 @@ async function findNextFreeSlots2(input) {
|
|
|
1048
1759
|
}
|
|
1049
1760
|
return out.slice(0, input.wanted);
|
|
1050
1761
|
}
|
|
1051
|
-
async function
|
|
1762
|
+
async function ensureFreshAccessToken5(creds, clientId, clientSecret) {
|
|
1052
1763
|
if (creds.kind !== "oauth2") {
|
|
1053
1764
|
throw new Error("microsoft-calendar: expected oauth2 credentials");
|
|
1054
1765
|
}
|
|
@@ -1059,7 +1770,7 @@ async function ensureFreshAccessToken3(creds, clientId, clientSecret) {
|
|
|
1059
1770
|
throw new CredentialsExpired("Microsoft Calendar access token expired and no refresh token", "");
|
|
1060
1771
|
}
|
|
1061
1772
|
const refreshed = await refreshAccessToken({
|
|
1062
|
-
tokenUrl:
|
|
1773
|
+
tokenUrl: TOKEN_URL5,
|
|
1063
1774
|
clientId,
|
|
1064
1775
|
clientSecret,
|
|
1065
1776
|
refreshToken: creds.refreshToken
|
|
@@ -1082,9 +1793,9 @@ var SCOPES4 = [
|
|
|
1082
1793
|
"crm.objects.contacts.read",
|
|
1083
1794
|
"crm.objects.contacts.write"
|
|
1084
1795
|
];
|
|
1085
|
-
var
|
|
1086
|
-
var
|
|
1087
|
-
var
|
|
1796
|
+
var AUTH_URL6 = "https://app.hubspot.com/oauth/authorize";
|
|
1797
|
+
var TOKEN_URL6 = "https://api.hubapi.com/oauth/v1/token";
|
|
1798
|
+
var API3 = "https://api.hubapi.com";
|
|
1088
1799
|
function hubspot(opts) {
|
|
1089
1800
|
const { clientId, clientSecret } = opts;
|
|
1090
1801
|
const adapter = {
|
|
@@ -1094,8 +1805,8 @@ function hubspot(opts) {
|
|
|
1094
1805
|
description: "Look up callers in HubSpot, upsert contacts without duplicates, and log call notes as CRM activities. Three capabilities \u2014 the voice-agent's CRM hot path.",
|
|
1095
1806
|
auth: {
|
|
1096
1807
|
kind: "oauth2",
|
|
1097
|
-
authorizationUrl:
|
|
1098
|
-
tokenUrl:
|
|
1808
|
+
authorizationUrl: AUTH_URL6,
|
|
1809
|
+
tokenUrl: TOKEN_URL6,
|
|
1099
1810
|
scopes: SCOPES4,
|
|
1100
1811
|
clientIdEnv: "HUBSPOT_OAUTH_CLIENT_ID",
|
|
1101
1812
|
clientSecretEnv: "HUBSPOT_OAUTH_CLIENT_SECRET"
|
|
@@ -1154,8 +1865,8 @@ function hubspot(opts) {
|
|
|
1154
1865
|
throw new Error(`hubspot: unknown read capability ${inv.capabilityName}`);
|
|
1155
1866
|
}
|
|
1156
1867
|
const { email } = inv.args;
|
|
1157
|
-
const accessToken = await
|
|
1158
|
-
const res = await fetch(`${
|
|
1868
|
+
const accessToken = await ensureFreshAccessToken6(inv.source.credentials, clientId, clientSecret);
|
|
1869
|
+
const res = await fetch(`${API3}/crm/v3/objects/contacts/search`, {
|
|
1159
1870
|
method: "POST",
|
|
1160
1871
|
headers: {
|
|
1161
1872
|
authorization: `Bearer ${accessToken}`,
|
|
@@ -1187,7 +1898,7 @@ function hubspot(opts) {
|
|
|
1187
1898
|
};
|
|
1188
1899
|
},
|
|
1189
1900
|
async executeMutation(inv) {
|
|
1190
|
-
const accessToken = await
|
|
1901
|
+
const accessToken = await ensureFreshAccessToken6(inv.source.credentials, clientId, clientSecret);
|
|
1191
1902
|
if (inv.capabilityName === "upsert_contact") {
|
|
1192
1903
|
return upsertContact(inv, accessToken);
|
|
1193
1904
|
}
|
|
@@ -1201,7 +1912,7 @@ function hubspot(opts) {
|
|
|
1201
1912
|
throw new Error("HubSpot OAuth client not configured (HUBSPOT_OAUTH_CLIENT_ID / _SECRET)");
|
|
1202
1913
|
}
|
|
1203
1914
|
const tokens = await exchangeAuthorizationCode({
|
|
1204
|
-
tokenUrl:
|
|
1915
|
+
tokenUrl: TOKEN_URL6,
|
|
1205
1916
|
clientId,
|
|
1206
1917
|
clientSecret,
|
|
1207
1918
|
code: input.code,
|
|
@@ -1224,7 +1935,7 @@ function hubspot(opts) {
|
|
|
1224
1935
|
throw new Error("hubspot.refreshToken: missing refresh token");
|
|
1225
1936
|
}
|
|
1226
1937
|
const refreshed = await refreshAccessToken({
|
|
1227
|
-
tokenUrl:
|
|
1938
|
+
tokenUrl: TOKEN_URL6,
|
|
1228
1939
|
clientId,
|
|
1229
1940
|
clientSecret,
|
|
1230
1941
|
refreshToken: creds.refreshToken
|
|
@@ -1238,8 +1949,8 @@ function hubspot(opts) {
|
|
|
1238
1949
|
},
|
|
1239
1950
|
async test(source) {
|
|
1240
1951
|
try {
|
|
1241
|
-
const accessToken = await
|
|
1242
|
-
const res = await fetch(`${
|
|
1952
|
+
const accessToken = await ensureFreshAccessToken6(source.credentials, clientId, clientSecret);
|
|
1953
|
+
const res = await fetch(`${API3}/oauth/v1/access-tokens/${encodeURIComponent(accessToken)}`, {
|
|
1243
1954
|
signal: AbortSignal.timeout(8e3)
|
|
1244
1955
|
});
|
|
1245
1956
|
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
@@ -1257,7 +1968,7 @@ function hubspot(opts) {
|
|
|
1257
1968
|
async function upsertContact(inv, accessToken) {
|
|
1258
1969
|
const { email, properties } = inv.args;
|
|
1259
1970
|
const idemKey = sanitizeIdempotencyKey(inv.idempotencyKey);
|
|
1260
|
-
const url = `${
|
|
1971
|
+
const url = `${API3}/crm/v3/objects/contacts/batch/upsert?idempotencyKey=${encodeURIComponent(idemKey)}`;
|
|
1261
1972
|
const body = {
|
|
1262
1973
|
inputs: [
|
|
1263
1974
|
{
|
|
@@ -1303,7 +2014,7 @@ async function upsertContact(inv, accessToken) {
|
|
|
1303
2014
|
async function createNote(inv, accessToken) {
|
|
1304
2015
|
const { contactId, body } = inv.args;
|
|
1305
2016
|
const idemKey = sanitizeIdempotencyKey(inv.idempotencyKey);
|
|
1306
|
-
const url = `${
|
|
2017
|
+
const url = `${API3}/crm/v3/objects/notes/batch/create?idempotencyKey=${encodeURIComponent(idemKey)}`;
|
|
1307
2018
|
const payload = {
|
|
1308
2019
|
inputs: [
|
|
1309
2020
|
{
|
|
@@ -1356,7 +2067,7 @@ async function createNote(inv, accessToken) {
|
|
|
1356
2067
|
function sanitizeIdempotencyKey(k) {
|
|
1357
2068
|
return k.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
1358
2069
|
}
|
|
1359
|
-
async function
|
|
2070
|
+
async function ensureFreshAccessToken6(creds, clientId, clientSecret) {
|
|
1360
2071
|
if (creds.kind !== "oauth2") {
|
|
1361
2072
|
throw new Error("hubspot: expected oauth2 credentials");
|
|
1362
2073
|
}
|
|
@@ -1367,7 +2078,7 @@ async function ensureFreshAccessToken4(creds, clientId, clientSecret) {
|
|
|
1367
2078
|
throw new CredentialsExpired("HubSpot access token expired and no refresh token", "");
|
|
1368
2079
|
}
|
|
1369
2080
|
const refreshed = await refreshAccessToken({
|
|
1370
|
-
tokenUrl:
|
|
2081
|
+
tokenUrl: TOKEN_URL6,
|
|
1371
2082
|
clientId,
|
|
1372
2083
|
clientSecret,
|
|
1373
2084
|
refreshToken: creds.refreshToken
|
|
@@ -1380,9 +2091,9 @@ async function ensureFreshAccessToken4(creds, clientId, clientSecret) {
|
|
|
1380
2091
|
|
|
1381
2092
|
// src/connectors/adapters/slack.ts
|
|
1382
2093
|
var SCOPES5 = ["chat:write", "users:read", "users:read.email", "channels:read"];
|
|
1383
|
-
var
|
|
1384
|
-
var
|
|
1385
|
-
var
|
|
2094
|
+
var AUTH_URL7 = "https://slack.com/oauth/v2/authorize";
|
|
2095
|
+
var TOKEN_URL7 = "https://slack.com/api/oauth.v2.access";
|
|
2096
|
+
var API4 = "https://slack.com/api";
|
|
1386
2097
|
function slack(opts) {
|
|
1387
2098
|
const { clientId, clientSecret } = opts;
|
|
1388
2099
|
const adapter = {
|
|
@@ -1398,8 +2109,8 @@ function slack(opts) {
|
|
|
1398
2109
|
description: "Post messages from the agent into Slack, look up users by email, and list channels. Advisory surface \u2014 Slack posts are informational, not transactional.",
|
|
1399
2110
|
auth: {
|
|
1400
2111
|
kind: "oauth2",
|
|
1401
|
-
authorizationUrl:
|
|
1402
|
-
tokenUrl:
|
|
2112
|
+
authorizationUrl: AUTH_URL7,
|
|
2113
|
+
tokenUrl: TOKEN_URL7,
|
|
1403
2114
|
scopes: SCOPES5,
|
|
1404
2115
|
clientIdEnv: "SLACK_OAUTH_CLIENT_ID",
|
|
1405
2116
|
clientSecretEnv: "SLACK_OAUTH_CLIENT_SECRET"
|
|
@@ -1451,7 +2162,7 @@ function slack(opts) {
|
|
|
1451
2162
|
const accessToken = readBotToken(inv.source.credentials);
|
|
1452
2163
|
if (inv.capabilityName === "lookup_user") {
|
|
1453
2164
|
const { email } = inv.args;
|
|
1454
|
-
const url = `${
|
|
2165
|
+
const url = `${API4}/users.lookupByEmail?email=${encodeURIComponent(email)}`;
|
|
1455
2166
|
const json = await slackGet(url, accessToken, inv.source.id);
|
|
1456
2167
|
if (!json.ok) {
|
|
1457
2168
|
if (json.error === "users_not_found") {
|
|
@@ -1471,7 +2182,7 @@ function slack(opts) {
|
|
|
1471
2182
|
limit: String(Math.min(Math.max(1, limit ?? 200), 1e3)),
|
|
1472
2183
|
types: types ?? "public_channel,private_channel"
|
|
1473
2184
|
});
|
|
1474
|
-
const json = await slackGet(`${
|
|
2185
|
+
const json = await slackGet(`${API4}/conversations.list?${params.toString()}`, accessToken, inv.source.id);
|
|
1475
2186
|
if (!json.ok) {
|
|
1476
2187
|
throw new Error(`slack list_channels: ${json.error ?? "unknown"}`);
|
|
1477
2188
|
}
|
|
@@ -1489,7 +2200,7 @@ function slack(opts) {
|
|
|
1489
2200
|
}
|
|
1490
2201
|
const accessToken = readBotToken(inv.source.credentials);
|
|
1491
2202
|
const { channel, text, blocks } = inv.args;
|
|
1492
|
-
const res = await fetch(`${
|
|
2203
|
+
const res = await fetch(`${API4}/chat.postMessage`, {
|
|
1493
2204
|
method: "POST",
|
|
1494
2205
|
headers: {
|
|
1495
2206
|
authorization: `Bearer ${accessToken}`,
|
|
@@ -1524,7 +2235,7 @@ function slack(opts) {
|
|
|
1524
2235
|
throw new Error("Slack OAuth client not configured (SLACK_OAUTH_CLIENT_ID / _SECRET)");
|
|
1525
2236
|
}
|
|
1526
2237
|
const tokens = await exchangeAuthorizationCode({
|
|
1527
|
-
tokenUrl:
|
|
2238
|
+
tokenUrl: TOKEN_URL7,
|
|
1528
2239
|
clientId,
|
|
1529
2240
|
clientSecret,
|
|
1530
2241
|
code: input.code,
|
|
@@ -1547,7 +2258,7 @@ function slack(opts) {
|
|
|
1547
2258
|
return creds;
|
|
1548
2259
|
}
|
|
1549
2260
|
const refreshed = await refreshAccessToken({
|
|
1550
|
-
tokenUrl:
|
|
2261
|
+
tokenUrl: TOKEN_URL7,
|
|
1551
2262
|
clientId,
|
|
1552
2263
|
clientSecret,
|
|
1553
2264
|
refreshToken: creds.refreshToken
|
|
@@ -1562,7 +2273,7 @@ function slack(opts) {
|
|
|
1562
2273
|
async test(source) {
|
|
1563
2274
|
try {
|
|
1564
2275
|
const accessToken = readBotToken(source.credentials);
|
|
1565
|
-
const res = await fetch(`${
|
|
2276
|
+
const res = await fetch(`${API4}/auth.test`, {
|
|
1566
2277
|
method: "POST",
|
|
1567
2278
|
headers: { authorization: `Bearer ${accessToken}` },
|
|
1568
2279
|
signal: AbortSignal.timeout(8e3)
|
|
@@ -1602,9 +2313,9 @@ async function slackGet(url, accessToken, dataSourceId) {
|
|
|
1602
2313
|
}
|
|
1603
2314
|
|
|
1604
2315
|
// src/connectors/adapters/notion-database.ts
|
|
1605
|
-
var
|
|
1606
|
-
var
|
|
1607
|
-
var
|
|
2316
|
+
var AUTH_URL8 = "https://api.notion.com/v1/oauth/authorize";
|
|
2317
|
+
var TOKEN_URL8 = "https://api.notion.com/v1/oauth/token";
|
|
2318
|
+
var API5 = "https://api.notion.com/v1";
|
|
1608
2319
|
var NOTION_VERSION = "2022-06-28";
|
|
1609
2320
|
function notionDatabase(opts) {
|
|
1610
2321
|
const { clientId, clientSecret } = opts;
|
|
@@ -1615,8 +2326,8 @@ function notionDatabase(opts) {
|
|
|
1615
2326
|
description: "Query a Notion database, create new pages, and update existing ones with optimistic concurrency via last_edited_time.",
|
|
1616
2327
|
auth: {
|
|
1617
2328
|
kind: "oauth2",
|
|
1618
|
-
authorizationUrl:
|
|
1619
|
-
tokenUrl:
|
|
2329
|
+
authorizationUrl: AUTH_URL8,
|
|
2330
|
+
tokenUrl: TOKEN_URL8,
|
|
1620
2331
|
// Notion does not use OAuth scopes — the workspace owner picks
|
|
1621
2332
|
// which pages/databases the integration sees during install. We
|
|
1622
2333
|
// declare an empty scope list so the consent screen renders cleanly.
|
|
@@ -1691,7 +2402,7 @@ function notionDatabase(opts) {
|
|
|
1691
2402
|
};
|
|
1692
2403
|
if (filter) body.filter = filter;
|
|
1693
2404
|
if (startCursor) body.start_cursor = startCursor;
|
|
1694
|
-
const res = await fetch(`${
|
|
2405
|
+
const res = await fetch(`${API5}/databases/${encodeURIComponent(databaseId)}/query`, {
|
|
1695
2406
|
method: "POST",
|
|
1696
2407
|
headers: notionHeaders(accessToken),
|
|
1697
2408
|
body: JSON.stringify(body),
|
|
@@ -1730,7 +2441,7 @@ function notionDatabase(opts) {
|
|
|
1730
2441
|
redirect_uri: input.redirectUri,
|
|
1731
2442
|
code_verifier: input.codeVerifier
|
|
1732
2443
|
});
|
|
1733
|
-
const res = await fetch(
|
|
2444
|
+
const res = await fetch(TOKEN_URL8, {
|
|
1734
2445
|
method: "POST",
|
|
1735
2446
|
headers: {
|
|
1736
2447
|
authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
|
|
@@ -1769,7 +2480,7 @@ function notionDatabase(opts) {
|
|
|
1769
2480
|
throw new Error("notion-database.refreshToken: missing refresh token");
|
|
1770
2481
|
}
|
|
1771
2482
|
const refreshed = await refreshAccessToken({
|
|
1772
|
-
tokenUrl:
|
|
2483
|
+
tokenUrl: TOKEN_URL8,
|
|
1773
2484
|
clientId,
|
|
1774
2485
|
clientSecret,
|
|
1775
2486
|
refreshToken: creds.refreshToken
|
|
@@ -1784,7 +2495,7 @@ function notionDatabase(opts) {
|
|
|
1784
2495
|
async test(source) {
|
|
1785
2496
|
try {
|
|
1786
2497
|
const accessToken = readToken(source.credentials);
|
|
1787
|
-
const res = await fetch(`${
|
|
2498
|
+
const res = await fetch(`${API5}/users/me`, {
|
|
1788
2499
|
headers: notionHeaders(accessToken),
|
|
1789
2500
|
signal: AbortSignal.timeout(8e3)
|
|
1790
2501
|
});
|
|
@@ -1801,7 +2512,7 @@ function notionDatabase(opts) {
|
|
|
1801
2512
|
async function createPage(inv, accessToken) {
|
|
1802
2513
|
const databaseId = readMetaString3(inv.source.metadata, "databaseId");
|
|
1803
2514
|
const { properties } = inv.args;
|
|
1804
|
-
const res = await fetch(`${
|
|
2515
|
+
const res = await fetch(`${API5}/pages`, {
|
|
1805
2516
|
method: "POST",
|
|
1806
2517
|
headers: {
|
|
1807
2518
|
...notionHeaders(accessToken),
|
|
@@ -1835,7 +2546,7 @@ async function createPage(inv, accessToken) {
|
|
|
1835
2546
|
async function updatePage(inv, accessToken) {
|
|
1836
2547
|
const { pageId, properties, expectedLastEditedTime } = inv.args;
|
|
1837
2548
|
if (expectedLastEditedTime) {
|
|
1838
|
-
const headRes = await fetch(`${
|
|
2549
|
+
const headRes = await fetch(`${API5}/pages/${encodeURIComponent(pageId)}`, {
|
|
1839
2550
|
headers: notionHeaders(accessToken),
|
|
1840
2551
|
signal: AbortSignal.timeout(1e4)
|
|
1841
2552
|
});
|
|
@@ -1855,7 +2566,7 @@ async function updatePage(inv, accessToken) {
|
|
|
1855
2566
|
);
|
|
1856
2567
|
}
|
|
1857
2568
|
}
|
|
1858
|
-
const res = await fetch(`${
|
|
2569
|
+
const res = await fetch(`${API5}/pages/${encodeURIComponent(pageId)}`, {
|
|
1859
2570
|
method: "PATCH",
|
|
1860
2571
|
headers: notionHeaders(accessToken),
|
|
1861
2572
|
body: JSON.stringify({ properties }),
|
|
@@ -1901,6 +2612,304 @@ function readMetaString3(meta, key) {
|
|
|
1901
2612
|
return v;
|
|
1902
2613
|
}
|
|
1903
2614
|
|
|
2615
|
+
// src/connectors/adapters/docuseal.ts
|
|
2616
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
2617
|
+
var DEFAULT_BASE = "https://api.docuseal.com";
|
|
2618
|
+
function readDocuSealCredentials(creds) {
|
|
2619
|
+
if (creds.kind === "api-key" && typeof creds.apiKey === "string" && creds.apiKey.length > 0) {
|
|
2620
|
+
return { apiKey: creds.apiKey };
|
|
2621
|
+
}
|
|
2622
|
+
if (creds.kind === "custom" && creds.values && typeof creds.values.apiKey === "string" && creds.values.apiKey.length > 0) {
|
|
2623
|
+
const webhookSecret = typeof creds.values.webhookSecret === "string" ? creds.values.webhookSecret : void 0;
|
|
2624
|
+
return { apiKey: creds.values.apiKey, webhookSecret };
|
|
2625
|
+
}
|
|
2626
|
+
throw new Error("docuseal: expected api-key credentials (apiKey + optional webhookSecret)");
|
|
2627
|
+
}
|
|
2628
|
+
function docuseal(opts = {}) {
|
|
2629
|
+
const baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
2630
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2631
|
+
const adapter = {
|
|
2632
|
+
manifest: {
|
|
2633
|
+
kind: "docuseal",
|
|
2634
|
+
displayName: "DocuSeal",
|
|
2635
|
+
description: "Send documents for e-signature via DocuSeal, poll submission status, void in-flight submissions, and react to push events when submitters sign.",
|
|
2636
|
+
auth: {
|
|
2637
|
+
kind: "api-key",
|
|
2638
|
+
hint: "Paste a DocuSeal personal API key (settings \u2192 API). Optional webhook secret enables push-driven workflows."
|
|
2639
|
+
},
|
|
2640
|
+
category: "doc",
|
|
2641
|
+
defaultConsistencyModel: "authoritative",
|
|
2642
|
+
capabilities: [
|
|
2643
|
+
{
|
|
2644
|
+
name: "get_submission",
|
|
2645
|
+
class: "read",
|
|
2646
|
+
description: "Fetch the current status of a DocuSeal submission and each submitter.",
|
|
2647
|
+
parameters: {
|
|
2648
|
+
type: "object",
|
|
2649
|
+
properties: { submissionId: { type: "string" } },
|
|
2650
|
+
required: ["submissionId"]
|
|
2651
|
+
}
|
|
2652
|
+
},
|
|
2653
|
+
{
|
|
2654
|
+
name: "create_submission",
|
|
2655
|
+
class: "mutation",
|
|
2656
|
+
description: "Send a template for signature to one or more submitters. external_id is the idempotency key; retries return the original submission.",
|
|
2657
|
+
cas: "native-idempotency",
|
|
2658
|
+
externalEffect: true,
|
|
2659
|
+
parameters: {
|
|
2660
|
+
type: "object",
|
|
2661
|
+
properties: {
|
|
2662
|
+
templateId: { type: "string" },
|
|
2663
|
+
submitters: {
|
|
2664
|
+
type: "array",
|
|
2665
|
+
items: {
|
|
2666
|
+
type: "object",
|
|
2667
|
+
properties: {
|
|
2668
|
+
email: { type: "string" },
|
|
2669
|
+
name: { type: "string" },
|
|
2670
|
+
role: { type: "string" },
|
|
2671
|
+
values: { type: "object", additionalProperties: true }
|
|
2672
|
+
},
|
|
2673
|
+
required: ["email"]
|
|
2674
|
+
}
|
|
2675
|
+
},
|
|
2676
|
+
sendEmail: { type: "boolean", default: true },
|
|
2677
|
+
message: { type: "string" }
|
|
2678
|
+
},
|
|
2679
|
+
required: ["templateId", "submitters"]
|
|
2680
|
+
}
|
|
2681
|
+
},
|
|
2682
|
+
{
|
|
2683
|
+
name: "void_submission",
|
|
2684
|
+
class: "mutation",
|
|
2685
|
+
description: "Cancel an in-flight submission. CAS via updated_at \u2014 concurrent voids return ResourceContention.",
|
|
2686
|
+
cas: "etag-if-match",
|
|
2687
|
+
externalEffect: true,
|
|
2688
|
+
parameters: {
|
|
2689
|
+
type: "object",
|
|
2690
|
+
properties: {
|
|
2691
|
+
submissionId: { type: "string" },
|
|
2692
|
+
reason: { type: "string" }
|
|
2693
|
+
},
|
|
2694
|
+
required: ["submissionId"]
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
]
|
|
2698
|
+
},
|
|
2699
|
+
async executeRead(inv) {
|
|
2700
|
+
if (inv.capabilityName !== "get_submission") {
|
|
2701
|
+
throw new Error(`docuseal: unknown read capability ${inv.capabilityName}`);
|
|
2702
|
+
}
|
|
2703
|
+
const { apiKey } = readDocuSealCredentials(inv.source.credentials);
|
|
2704
|
+
const { submissionId } = inv.args;
|
|
2705
|
+
const res = await fetch(`${baseUrl}/submissions/${encodeURIComponent(submissionId)}`, {
|
|
2706
|
+
headers: { "X-Auth-Token": apiKey, accept: "application/json" },
|
|
2707
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2708
|
+
});
|
|
2709
|
+
if (res.status === 401) throw new CredentialsExpired("DocuSeal rejected API key (401)", inv.source.id);
|
|
2710
|
+
if (res.status === 404) {
|
|
2711
|
+
throw new Error(`docuseal get_submission: submission ${submissionId} not found`);
|
|
2712
|
+
}
|
|
2713
|
+
if (!res.ok) {
|
|
2714
|
+
const text = await res.text().catch(() => "");
|
|
2715
|
+
throw new Error(`docuseal get_submission ${res.status}: ${text.slice(0, 200)}`);
|
|
2716
|
+
}
|
|
2717
|
+
const json = await res.json();
|
|
2718
|
+
return {
|
|
2719
|
+
data: normalizeSubmission(json),
|
|
2720
|
+
etag: json.updated_at,
|
|
2721
|
+
fetchedAt: Date.now()
|
|
2722
|
+
};
|
|
2723
|
+
},
|
|
2724
|
+
async executeMutation(inv) {
|
|
2725
|
+
const { apiKey } = readDocuSealCredentials(inv.source.credentials);
|
|
2726
|
+
if (inv.capabilityName === "create_submission") return createSubmission(inv, apiKey, baseUrl, timeoutMs);
|
|
2727
|
+
if (inv.capabilityName === "void_submission") return voidSubmission(inv, apiKey, baseUrl, timeoutMs);
|
|
2728
|
+
throw new Error(`docuseal: unknown mutation capability ${inv.capabilityName}`);
|
|
2729
|
+
},
|
|
2730
|
+
verifySignature({ rawBody, headers, source }) {
|
|
2731
|
+
const creds = (() => {
|
|
2732
|
+
try {
|
|
2733
|
+
return readDocuSealCredentials(source.credentials);
|
|
2734
|
+
} catch {
|
|
2735
|
+
return null;
|
|
2736
|
+
}
|
|
2737
|
+
})();
|
|
2738
|
+
if (!creds?.webhookSecret) return { valid: false, reason: "missing_webhook_secret" };
|
|
2739
|
+
const sig = firstHeader(headers, "x-docuseal-signature");
|
|
2740
|
+
if (!sig) return { valid: false, reason: "missing_signature_header" };
|
|
2741
|
+
const expected = createHmac("sha256", creds.webhookSecret).update(rawBody).digest("hex");
|
|
2742
|
+
const a = Buffer.from(sig.toLowerCase(), "utf-8");
|
|
2743
|
+
const b = Buffer.from(expected, "utf-8");
|
|
2744
|
+
if (a.length !== b.length) return { valid: false, reason: "invalid_signature" };
|
|
2745
|
+
return timingSafeEqual(a, b) ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
2746
|
+
},
|
|
2747
|
+
async handleInboundEvent({ rawBody }) {
|
|
2748
|
+
let parsed;
|
|
2749
|
+
try {
|
|
2750
|
+
parsed = JSON.parse(rawBody);
|
|
2751
|
+
} catch {
|
|
2752
|
+
return { events: [], response: { status: 400, body: { error: "invalid_json" } } };
|
|
2753
|
+
}
|
|
2754
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2755
|
+
return { events: [], response: { status: 400, body: { error: "invalid_payload" } } };
|
|
2756
|
+
}
|
|
2757
|
+
const evt = parsed;
|
|
2758
|
+
const eventType = typeof evt.event_type === "string" ? `docuseal.${evt.event_type}` : "docuseal.unknown";
|
|
2759
|
+
const providerEventId = typeof evt.event_id === "string" ? evt.event_id : void 0;
|
|
2760
|
+
const events = [
|
|
2761
|
+
{
|
|
2762
|
+
eventType,
|
|
2763
|
+
providerEventId,
|
|
2764
|
+
payload: evt
|
|
2765
|
+
}
|
|
2766
|
+
];
|
|
2767
|
+
return { events };
|
|
2768
|
+
},
|
|
2769
|
+
async test(source) {
|
|
2770
|
+
try {
|
|
2771
|
+
const { apiKey } = readDocuSealCredentials(source.credentials);
|
|
2772
|
+
const res = await fetch(`${baseUrl}/templates?limit=1`, {
|
|
2773
|
+
headers: { "X-Auth-Token": apiKey },
|
|
2774
|
+
signal: AbortSignal.timeout(8e3)
|
|
2775
|
+
});
|
|
2776
|
+
if (res.status === 401) return { ok: false, reason: "DocuSeal rejected API key (401) \u2014 reconnect required" };
|
|
2777
|
+
if (!res.ok) return { ok: false, reason: `DocuSeal returned ${res.status}` };
|
|
2778
|
+
return { ok: true };
|
|
2779
|
+
} catch (err) {
|
|
2780
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2783
|
+
};
|
|
2784
|
+
return adapter;
|
|
2785
|
+
}
|
|
2786
|
+
function normalizeSubmission(s) {
|
|
2787
|
+
return {
|
|
2788
|
+
submissionId: String(s.id),
|
|
2789
|
+
status: s.status ?? "pending",
|
|
2790
|
+
updatedAt: s.updated_at,
|
|
2791
|
+
createdAt: s.created_at,
|
|
2792
|
+
completedAt: s.completed_at ?? void 0,
|
|
2793
|
+
auditLogUrl: s.audit_log_url,
|
|
2794
|
+
combinedDocumentUrl: s.combined_document_url,
|
|
2795
|
+
submitters: (s.submitters ?? []).map((sub) => ({
|
|
2796
|
+
email: sub.email,
|
|
2797
|
+
name: sub.name,
|
|
2798
|
+
role: sub.role,
|
|
2799
|
+
slug: sub.slug,
|
|
2800
|
+
status: sub.status ?? "awaiting",
|
|
2801
|
+
completedAt: sub.completed_at ?? void 0,
|
|
2802
|
+
url: sub.embed_src
|
|
2803
|
+
}))
|
|
2804
|
+
};
|
|
2805
|
+
}
|
|
2806
|
+
async function createSubmission(inv, apiKey, baseUrl, timeoutMs) {
|
|
2807
|
+
const { templateId, submitters, sendEmail, message } = inv.args;
|
|
2808
|
+
const body = {
|
|
2809
|
+
template_id: templateId,
|
|
2810
|
+
external_id: inv.idempotencyKey,
|
|
2811
|
+
send_email: sendEmail ?? true,
|
|
2812
|
+
message,
|
|
2813
|
+
submitters: submitters.map((s) => ({
|
|
2814
|
+
email: s.email,
|
|
2815
|
+
name: s.name,
|
|
2816
|
+
role: s.role,
|
|
2817
|
+
values: s.values
|
|
2818
|
+
}))
|
|
2819
|
+
};
|
|
2820
|
+
const res = await fetch(`${baseUrl}/submissions`, {
|
|
2821
|
+
method: "POST",
|
|
2822
|
+
headers: {
|
|
2823
|
+
"X-Auth-Token": apiKey,
|
|
2824
|
+
"content-type": "application/json",
|
|
2825
|
+
accept: "application/json"
|
|
2826
|
+
},
|
|
2827
|
+
body: JSON.stringify(body),
|
|
2828
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2829
|
+
});
|
|
2830
|
+
if (res.status === 401) throw new CredentialsExpired("DocuSeal rejected API key (401)", inv.source.id);
|
|
2831
|
+
if (res.status === 429) {
|
|
2832
|
+
const retryAfter = Number(res.headers.get("retry-after") ?? "5");
|
|
2833
|
+
return {
|
|
2834
|
+
status: "rate-limited",
|
|
2835
|
+
retryAfterMs: Number.isFinite(retryAfter) ? retryAfter * 1e3 : 5e3,
|
|
2836
|
+
message: "DocuSeal rate-limited create_submission"
|
|
2837
|
+
};
|
|
2838
|
+
}
|
|
2839
|
+
if (res.status === 409) {
|
|
2840
|
+
const conflictJson = await res.json().catch(() => ({}));
|
|
2841
|
+
const original = conflictJson.submission ?? conflictJson;
|
|
2842
|
+
return {
|
|
2843
|
+
status: "committed",
|
|
2844
|
+
data: normalizeSubmission(original),
|
|
2845
|
+
etagAfter: original.updated_at,
|
|
2846
|
+
committedAt: Date.now(),
|
|
2847
|
+
idempotentReplay: true
|
|
2848
|
+
};
|
|
2849
|
+
}
|
|
2850
|
+
if (!res.ok) {
|
|
2851
|
+
const text = await res.text().catch(() => "");
|
|
2852
|
+
throw new Error(`docuseal create_submission ${res.status}: ${text.slice(0, 200)}`);
|
|
2853
|
+
}
|
|
2854
|
+
const created = await res.json();
|
|
2855
|
+
const sub = Array.isArray(created) ? created[0] : created;
|
|
2856
|
+
if (!sub) {
|
|
2857
|
+
throw new Error("docuseal create_submission: empty response body");
|
|
2858
|
+
}
|
|
2859
|
+
return {
|
|
2860
|
+
status: "committed",
|
|
2861
|
+
data: normalizeSubmission(sub),
|
|
2862
|
+
etagAfter: sub.updated_at,
|
|
2863
|
+
committedAt: Date.now(),
|
|
2864
|
+
idempotentReplay: false
|
|
2865
|
+
};
|
|
2866
|
+
}
|
|
2867
|
+
async function voidSubmission(inv, apiKey, baseUrl, timeoutMs) {
|
|
2868
|
+
const { submissionId, reason } = inv.args;
|
|
2869
|
+
const headers = {
|
|
2870
|
+
"X-Auth-Token": apiKey,
|
|
2871
|
+
accept: "application/json"
|
|
2872
|
+
};
|
|
2873
|
+
if (inv.expectedEtag) headers["if-match"] = inv.expectedEtag;
|
|
2874
|
+
if (reason) headers["x-docuseal-void-reason"] = reason;
|
|
2875
|
+
const res = await fetch(`${baseUrl}/submissions/${encodeURIComponent(submissionId)}`, {
|
|
2876
|
+
method: "DELETE",
|
|
2877
|
+
headers,
|
|
2878
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2879
|
+
});
|
|
2880
|
+
if (res.status === 401) throw new CredentialsExpired("DocuSeal rejected API key (401)", inv.source.id);
|
|
2881
|
+
if (res.status === 404) {
|
|
2882
|
+
throw new Error(`docuseal void_submission: submission ${submissionId} not found`);
|
|
2883
|
+
}
|
|
2884
|
+
if (res.status === 412) {
|
|
2885
|
+
throw new ResourceContention(`docuseal void_submission: submission ${submissionId} updated since last read`);
|
|
2886
|
+
}
|
|
2887
|
+
if (res.status === 429) {
|
|
2888
|
+
const retryAfter = Number(res.headers.get("retry-after") ?? "5");
|
|
2889
|
+
return {
|
|
2890
|
+
status: "rate-limited",
|
|
2891
|
+
retryAfterMs: Number.isFinite(retryAfter) ? retryAfter * 1e3 : 5e3,
|
|
2892
|
+
message: "DocuSeal rate-limited void_submission"
|
|
2893
|
+
};
|
|
2894
|
+
}
|
|
2895
|
+
if (!res.ok) {
|
|
2896
|
+
const text = await res.text().catch(() => "");
|
|
2897
|
+
throw new Error(`docuseal void_submission ${res.status}: ${text.slice(0, 200)}`);
|
|
2898
|
+
}
|
|
2899
|
+
const json = await res.json().catch(() => ({}));
|
|
2900
|
+
return {
|
|
2901
|
+
status: "committed",
|
|
2902
|
+
data: {
|
|
2903
|
+
submissionId,
|
|
2904
|
+
status: "voided",
|
|
2905
|
+
voidedAt: json.updated_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2906
|
+
},
|
|
2907
|
+
etagAfter: json.updated_at,
|
|
2908
|
+
committedAt: Date.now(),
|
|
2909
|
+
idempotentReplay: false
|
|
2910
|
+
};
|
|
2911
|
+
}
|
|
2912
|
+
|
|
1904
2913
|
// src/connectors/adapters/declarative-rest.ts
|
|
1905
2914
|
function declarativeRestConnector(spec) {
|
|
1906
2915
|
const capabilities = spec.capabilities.map(operationToCapability);
|
|
@@ -2100,7 +3109,7 @@ async function safeErrorText(res) {
|
|
|
2100
3109
|
}
|
|
2101
3110
|
|
|
2102
3111
|
// src/connectors/adapters/twilio-sms.ts
|
|
2103
|
-
var
|
|
3112
|
+
var API6 = "https://api.twilio.com/2010-04-01";
|
|
2104
3113
|
var LOOKUP_API = "https://lookups.twilio.com/v1";
|
|
2105
3114
|
var twilioSmsConnector = {
|
|
2106
3115
|
manifest: {
|
|
@@ -2192,7 +3201,7 @@ var twilioSmsConnector = {
|
|
|
2192
3201
|
params.set("PageSize", String(Math.min(Math.max(1, limit ?? 20), 100)));
|
|
2193
3202
|
if (to) params.set("To", to);
|
|
2194
3203
|
if (from) params.set("From", from);
|
|
2195
|
-
const url = `${
|
|
3204
|
+
const url = `${API6}/Accounts/${encodeURIComponent(auth.accountSid)}/Messages.json?${params.toString()}`;
|
|
2196
3205
|
const res = await fetch(url, {
|
|
2197
3206
|
headers: { authorization: basicAuth(auth) },
|
|
2198
3207
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -2218,7 +3227,7 @@ var twilioSmsConnector = {
|
|
|
2218
3227
|
const { to, body, from } = inv.args;
|
|
2219
3228
|
const fromNumber = from ?? readMetaString4(inv.source.metadata, "fromNumber");
|
|
2220
3229
|
const formBody = new URLSearchParams({ To: to, From: fromNumber, Body: body });
|
|
2221
|
-
const url = `${
|
|
3230
|
+
const url = `${API6}/Accounts/${encodeURIComponent(auth.accountSid)}/Messages.json`;
|
|
2222
3231
|
const res = await fetch(url, {
|
|
2223
3232
|
method: "POST",
|
|
2224
3233
|
headers: {
|
|
@@ -2248,7 +3257,7 @@ var twilioSmsConnector = {
|
|
|
2248
3257
|
async test(source) {
|
|
2249
3258
|
try {
|
|
2250
3259
|
const auth = parseAuth(source.credentials);
|
|
2251
|
-
const res = await fetch(`${
|
|
3260
|
+
const res = await fetch(`${API6}/Accounts/${encodeURIComponent(auth.accountSid)}.json`, {
|
|
2252
3261
|
headers: { authorization: basicAuth(auth) },
|
|
2253
3262
|
signal: AbortSignal.timeout(8e3)
|
|
2254
3263
|
});
|
|
@@ -2293,15 +3302,15 @@ function readMetaString4(meta, key) {
|
|
|
2293
3302
|
}
|
|
2294
3303
|
|
|
2295
3304
|
// src/connectors/adapters/stripe-pack.ts
|
|
2296
|
-
var
|
|
3305
|
+
var API7 = "https://api.stripe.com/v1";
|
|
2297
3306
|
var stripePackConnector = {
|
|
2298
3307
|
manifest: {
|
|
2299
3308
|
kind: "stripe-pack",
|
|
2300
|
-
displayName: "Stripe (customers, invoices, checkout)",
|
|
2301
|
-
description: "Look up Stripe customers, draft invoices,
|
|
3309
|
+
displayName: "Stripe (customers, invoices, checkout, subscriptions)",
|
|
3310
|
+
description: "Look up Stripe customers, draft invoices, spin up hosted Checkout sessions, manage subscriptions, and hand off to the customer billing portal \u2014 all from one Stripe restricted key. Idempotency-Key forwarded on every mutation.",
|
|
2302
3311
|
auth: {
|
|
2303
3312
|
kind: "api-key",
|
|
2304
|
-
hint: "Paste a Stripe restricted key (rk_live_\u2026) with read
|
|
3313
|
+
hint: "Paste a Stripe restricted key (rk_live_\u2026) with read on customers + subscriptions and write on invoices + checkout + subscriptions + billing portal."
|
|
2305
3314
|
},
|
|
2306
3315
|
category: "commerce",
|
|
2307
3316
|
defaultConsistencyModel: "authoritative",
|
|
@@ -2316,6 +3325,20 @@ var stripePackConnector = {
|
|
|
2316
3325
|
required: ["email"]
|
|
2317
3326
|
}
|
|
2318
3327
|
},
|
|
3328
|
+
{
|
|
3329
|
+
name: "list_subscriptions",
|
|
3330
|
+
class: "read",
|
|
3331
|
+
description: "List a customer's subscriptions. Optionally filter by status ('active', 'past_due', 'canceled', 'all').",
|
|
3332
|
+
parameters: {
|
|
3333
|
+
type: "object",
|
|
3334
|
+
properties: {
|
|
3335
|
+
customerId: { type: "string" },
|
|
3336
|
+
status: { type: "string", enum: ["active", "past_due", "unpaid", "canceled", "incomplete", "trialing", "all"], default: "all" },
|
|
3337
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }
|
|
3338
|
+
},
|
|
3339
|
+
required: ["customerId"]
|
|
3340
|
+
}
|
|
3341
|
+
},
|
|
2319
3342
|
{
|
|
2320
3343
|
name: "create_invoice",
|
|
2321
3344
|
class: "mutation",
|
|
@@ -2371,16 +3394,47 @@ var stripePackConnector = {
|
|
|
2371
3394
|
},
|
|
2372
3395
|
required: ["successUrl", "cancelUrl", "lineItems"]
|
|
2373
3396
|
}
|
|
3397
|
+
},
|
|
3398
|
+
{
|
|
3399
|
+
name: "cancel_subscription",
|
|
3400
|
+
class: "mutation",
|
|
3401
|
+
description: "Cancel a subscription. Default cancels immediately; pass atPeriodEnd=true to schedule cancellation for the end of the current billing period.",
|
|
3402
|
+
cas: "native-idempotency",
|
|
3403
|
+
externalEffect: true,
|
|
3404
|
+
parameters: {
|
|
3405
|
+
type: "object",
|
|
3406
|
+
properties: {
|
|
3407
|
+
subscriptionId: { type: "string" },
|
|
3408
|
+
atPeriodEnd: { type: "boolean", default: false }
|
|
3409
|
+
},
|
|
3410
|
+
required: ["subscriptionId"]
|
|
3411
|
+
}
|
|
3412
|
+
},
|
|
3413
|
+
{
|
|
3414
|
+
name: "create_billing_portal_session",
|
|
3415
|
+
class: "mutation",
|
|
3416
|
+
description: "Create a Stripe billing portal session and return its URL. Hand-off for the customer to self-serve cancel/upgrade/update-card.",
|
|
3417
|
+
cas: "native-idempotency",
|
|
3418
|
+
externalEffect: true,
|
|
3419
|
+
parameters: {
|
|
3420
|
+
type: "object",
|
|
3421
|
+
properties: {
|
|
3422
|
+
customerId: { type: "string" },
|
|
3423
|
+
returnUrl: { type: "string" }
|
|
3424
|
+
},
|
|
3425
|
+
required: ["customerId", "returnUrl"]
|
|
3426
|
+
}
|
|
2374
3427
|
}
|
|
2375
3428
|
]
|
|
2376
3429
|
},
|
|
2377
3430
|
async executeRead(inv) {
|
|
3431
|
+
const apiKey = readApiKey(inv.source.credentials);
|
|
3432
|
+
if (inv.capabilityName === "list_subscriptions") return listSubscriptions(inv, apiKey);
|
|
2378
3433
|
if (inv.capabilityName !== "find_customer") {
|
|
2379
3434
|
throw new Error(`stripe-pack: unknown read capability ${inv.capabilityName}`);
|
|
2380
3435
|
}
|
|
2381
|
-
const apiKey = readApiKey(inv.source.credentials);
|
|
2382
3436
|
const { email } = inv.args;
|
|
2383
|
-
const url = `${
|
|
3437
|
+
const url = `${API7}/customers/search?query=${encodeURIComponent(`email:'${email.toLowerCase()}'`)}&limit=1`;
|
|
2384
3438
|
const res = await fetch(url, {
|
|
2385
3439
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
2386
3440
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -2403,12 +3457,14 @@ var stripePackConnector = {
|
|
|
2403
3457
|
const apiKey = readApiKey(inv.source.credentials);
|
|
2404
3458
|
if (inv.capabilityName === "create_invoice") return createInvoice(inv, apiKey);
|
|
2405
3459
|
if (inv.capabilityName === "create_checkout_session") return createCheckoutSession(inv, apiKey);
|
|
3460
|
+
if (inv.capabilityName === "cancel_subscription") return cancelSubscription(inv, apiKey);
|
|
3461
|
+
if (inv.capabilityName === "create_billing_portal_session") return createBillingPortalSession(inv, apiKey);
|
|
2406
3462
|
throw new Error(`stripe-pack: unknown mutation capability ${inv.capabilityName}`);
|
|
2407
3463
|
},
|
|
2408
3464
|
async test(source) {
|
|
2409
3465
|
try {
|
|
2410
3466
|
const apiKey = readApiKey(source.credentials);
|
|
2411
|
-
const res = await fetch(`${
|
|
3467
|
+
const res = await fetch(`${API7}/account`, {
|
|
2412
3468
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
2413
3469
|
signal: AbortSignal.timeout(8e3)
|
|
2414
3470
|
});
|
|
@@ -2434,7 +3490,7 @@ async function createInvoice(inv, apiKey) {
|
|
|
2434
3490
|
quantity: String(it.quantity ?? 1)
|
|
2435
3491
|
});
|
|
2436
3492
|
if (it.description) body.set("description", it.description);
|
|
2437
|
-
const res = await fetch(`${
|
|
3493
|
+
const res = await fetch(`${API7}/invoiceitems`, {
|
|
2438
3494
|
method: "POST",
|
|
2439
3495
|
headers: {
|
|
2440
3496
|
authorization: `Bearer ${apiKey}`,
|
|
@@ -2459,7 +3515,7 @@ async function createInvoice(inv, apiKey) {
|
|
|
2459
3515
|
collection_method: "send_invoice",
|
|
2460
3516
|
days_until_due: "14"
|
|
2461
3517
|
});
|
|
2462
|
-
const invRes = await fetch(`${
|
|
3518
|
+
const invRes = await fetch(`${API7}/invoices`, {
|
|
2463
3519
|
method: "POST",
|
|
2464
3520
|
headers: {
|
|
2465
3521
|
authorization: `Bearer ${apiKey}`,
|
|
@@ -2497,7 +3553,7 @@ async function createCheckoutSession(inv, apiKey) {
|
|
|
2497
3553
|
body.set(`line_items[${i}][price]`, it.price);
|
|
2498
3554
|
body.set(`line_items[${i}][quantity]`, String(it.quantity ?? 1));
|
|
2499
3555
|
});
|
|
2500
|
-
const res = await fetch(`${
|
|
3556
|
+
const res = await fetch(`${API7}/checkout/sessions`, {
|
|
2501
3557
|
method: "POST",
|
|
2502
3558
|
headers: {
|
|
2503
3559
|
authorization: `Bearer ${apiKey}`,
|
|
@@ -2523,6 +3579,118 @@ async function createCheckoutSession(inv, apiKey) {
|
|
|
2523
3579
|
idempotentReplay: false
|
|
2524
3580
|
};
|
|
2525
3581
|
}
|
|
3582
|
+
async function listSubscriptions(inv, apiKey) {
|
|
3583
|
+
const { customerId, status, limit } = inv.args;
|
|
3584
|
+
const params = new URLSearchParams({ customer: customerId, limit: String(limit ?? 10) });
|
|
3585
|
+
if (status && status !== "all") params.set("status", status);
|
|
3586
|
+
else params.set("status", "all");
|
|
3587
|
+
const res = await fetch(`${API7}/subscriptions?${params.toString()}`, {
|
|
3588
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
3589
|
+
signal: AbortSignal.timeout(1e4)
|
|
3590
|
+
});
|
|
3591
|
+
if (res.status === 401) {
|
|
3592
|
+
throw new CredentialsExpired("Stripe rejected API key (401)", inv.source.id);
|
|
3593
|
+
}
|
|
3594
|
+
if (!res.ok) {
|
|
3595
|
+
const text = await res.text().catch(() => "");
|
|
3596
|
+
throw new Error(`stripe-pack list_subscriptions ${res.status}: ${text.slice(0, 200)}`);
|
|
3597
|
+
}
|
|
3598
|
+
const json = await res.json();
|
|
3599
|
+
const subscriptions = (json.data ?? []).map((sub) => ({
|
|
3600
|
+
id: sub.id,
|
|
3601
|
+
status: sub.status,
|
|
3602
|
+
currentPeriodEnd: sub.current_period_end,
|
|
3603
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end ?? false,
|
|
3604
|
+
items: (sub.items?.data ?? []).map((it) => ({
|
|
3605
|
+
priceId: it.price?.id,
|
|
3606
|
+
productId: it.price?.product,
|
|
3607
|
+
unitAmount: it.price?.unit_amount,
|
|
3608
|
+
currency: it.price?.currency,
|
|
3609
|
+
interval: it.price?.recurring?.interval
|
|
3610
|
+
}))
|
|
3611
|
+
}));
|
|
3612
|
+
return {
|
|
3613
|
+
data: { subscriptions },
|
|
3614
|
+
fetchedAt: Date.now()
|
|
3615
|
+
};
|
|
3616
|
+
}
|
|
3617
|
+
async function cancelSubscription(inv, apiKey) {
|
|
3618
|
+
const { subscriptionId, atPeriodEnd } = inv.args;
|
|
3619
|
+
let res;
|
|
3620
|
+
if (atPeriodEnd) {
|
|
3621
|
+
const body = new URLSearchParams({ cancel_at_period_end: "true" });
|
|
3622
|
+
res = await fetch(`${API7}/subscriptions/${encodeURIComponent(subscriptionId)}`, {
|
|
3623
|
+
method: "POST",
|
|
3624
|
+
headers: {
|
|
3625
|
+
authorization: `Bearer ${apiKey}`,
|
|
3626
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
3627
|
+
"idempotency-key": inv.idempotencyKey
|
|
3628
|
+
},
|
|
3629
|
+
body,
|
|
3630
|
+
signal: AbortSignal.timeout(15e3)
|
|
3631
|
+
});
|
|
3632
|
+
} else {
|
|
3633
|
+
res = await fetch(`${API7}/subscriptions/${encodeURIComponent(subscriptionId)}`, {
|
|
3634
|
+
method: "DELETE",
|
|
3635
|
+
headers: {
|
|
3636
|
+
authorization: `Bearer ${apiKey}`,
|
|
3637
|
+
"idempotency-key": inv.idempotencyKey
|
|
3638
|
+
},
|
|
3639
|
+
signal: AbortSignal.timeout(15e3)
|
|
3640
|
+
});
|
|
3641
|
+
}
|
|
3642
|
+
if (res.status === 401) throw new CredentialsExpired("Stripe rejected API key (401)", inv.source.id);
|
|
3643
|
+
if (res.status === 404) throw new Error(`stripe-pack cancel_subscription: subscription ${subscriptionId} not found`);
|
|
3644
|
+
if (res.status === 409) {
|
|
3645
|
+
throw new ResourceContention("Stripe subscription conflict \u2014 retry rejected by idempotency check");
|
|
3646
|
+
}
|
|
3647
|
+
if (!res.ok) {
|
|
3648
|
+
const text = await res.text().catch(() => "");
|
|
3649
|
+
throw new Error(`stripe-pack cancel_subscription ${res.status}: ${text.slice(0, 200)}`);
|
|
3650
|
+
}
|
|
3651
|
+
const updated = await res.json();
|
|
3652
|
+
return {
|
|
3653
|
+
status: "committed",
|
|
3654
|
+
data: {
|
|
3655
|
+
id: updated.id,
|
|
3656
|
+
status: updated.status,
|
|
3657
|
+
cancelAtPeriodEnd: updated.cancel_at_period_end ?? false,
|
|
3658
|
+
canceledAt: updated.canceled_at,
|
|
3659
|
+
currentPeriodEnd: updated.current_period_end
|
|
3660
|
+
},
|
|
3661
|
+
committedAt: Date.now(),
|
|
3662
|
+
idempotentReplay: false
|
|
3663
|
+
};
|
|
3664
|
+
}
|
|
3665
|
+
async function createBillingPortalSession(inv, apiKey) {
|
|
3666
|
+
const { customerId, returnUrl } = inv.args;
|
|
3667
|
+
const body = new URLSearchParams({ customer: customerId, return_url: returnUrl });
|
|
3668
|
+
const res = await fetch(`${API7}/billing_portal/sessions`, {
|
|
3669
|
+
method: "POST",
|
|
3670
|
+
headers: {
|
|
3671
|
+
authorization: `Bearer ${apiKey}`,
|
|
3672
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
3673
|
+
"idempotency-key": inv.idempotencyKey
|
|
3674
|
+
},
|
|
3675
|
+
body,
|
|
3676
|
+
signal: AbortSignal.timeout(15e3)
|
|
3677
|
+
});
|
|
3678
|
+
if (res.status === 401) throw new CredentialsExpired("Stripe rejected API key (401)", inv.source.id);
|
|
3679
|
+
if (res.status === 409) {
|
|
3680
|
+
throw new ResourceContention("Stripe billing portal session conflict \u2014 retry rejected by idempotency check");
|
|
3681
|
+
}
|
|
3682
|
+
if (!res.ok) {
|
|
3683
|
+
const text = await res.text().catch(() => "");
|
|
3684
|
+
throw new Error(`stripe-pack create_billing_portal_session ${res.status}: ${text.slice(0, 200)}`);
|
|
3685
|
+
}
|
|
3686
|
+
const created = await res.json();
|
|
3687
|
+
return {
|
|
3688
|
+
status: "committed",
|
|
3689
|
+
data: { sessionId: created.id, url: created.url, returnUrl: created.return_url },
|
|
3690
|
+
committedAt: Date.now(),
|
|
3691
|
+
idempotentReplay: false
|
|
3692
|
+
};
|
|
3693
|
+
}
|
|
2526
3694
|
function readApiKey(creds) {
|
|
2527
3695
|
if (creds.kind !== "api-key" || typeof creds.apiKey !== "string" || creds.apiKey.length === 0) {
|
|
2528
3696
|
throw new Error("stripe-pack: expected api-key credentials");
|
|
@@ -2531,7 +3699,7 @@ function readApiKey(creds) {
|
|
|
2531
3699
|
}
|
|
2532
3700
|
|
|
2533
3701
|
// src/connectors/adapters/webhook.ts
|
|
2534
|
-
import { createHmac } from "crypto";
|
|
3702
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
2535
3703
|
var webhookConnector = {
|
|
2536
3704
|
manifest: {
|
|
2537
3705
|
kind: "webhook",
|
|
@@ -2645,96 +3813,12 @@ function signHeaders(creds, body, idempotencyKey) {
|
|
|
2645
3813
|
"x-phony-idempotency-key": idempotencyKey
|
|
2646
3814
|
};
|
|
2647
3815
|
if (creds.kind === "hmac" && typeof creds.secret === "string" && creds.secret.length > 0) {
|
|
2648
|
-
const sig =
|
|
3816
|
+
const sig = createHmac2("sha256", creds.secret).update(`${ts}.${body}`).digest("hex");
|
|
2649
3817
|
headers["x-phony-signature"] = `sha256=${sig}`;
|
|
2650
3818
|
}
|
|
2651
3819
|
return headers;
|
|
2652
3820
|
}
|
|
2653
3821
|
|
|
2654
|
-
// src/connectors/webhooks.ts
|
|
2655
|
-
import { createHmac as createHmac2, timingSafeEqual } from "crypto";
|
|
2656
|
-
var DEFAULT_SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
|
|
2657
|
-
function parseStripeSignatureHeader(header) {
|
|
2658
|
-
const acc = { sigs: [] };
|
|
2659
|
-
for (const part of header.split(",")) {
|
|
2660
|
-
const idx = part.indexOf("=");
|
|
2661
|
-
if (idx < 0) continue;
|
|
2662
|
-
const key = part.slice(0, idx).trim();
|
|
2663
|
-
const val = part.slice(idx + 1).trim();
|
|
2664
|
-
if (key === "t") {
|
|
2665
|
-
const n = Number(val);
|
|
2666
|
-
if (Number.isFinite(n)) acc.ts = n;
|
|
2667
|
-
} else if (key === "v1") {
|
|
2668
|
-
acc.sigs.push(val);
|
|
2669
|
-
}
|
|
2670
|
-
}
|
|
2671
|
-
if (acc.ts === void 0 || acc.sigs.length === 0) return null;
|
|
2672
|
-
return { t: acc.ts, sigs: acc.sigs };
|
|
2673
|
-
}
|
|
2674
|
-
function verifyStripeSignature(rawBody, signatureHeader, secret, options = {}) {
|
|
2675
|
-
const parsed = parseStripeSignatureHeader(signatureHeader);
|
|
2676
|
-
if (!parsed) return false;
|
|
2677
|
-
const tolerance = options.toleranceSeconds ?? DEFAULT_SIGNATURE_TOLERANCE_SECONDS;
|
|
2678
|
-
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
2679
|
-
if (Math.abs(now - parsed.t) > tolerance) return false;
|
|
2680
|
-
const expected = createHmac2("sha256", secret).update(`${parsed.t}.${rawBody}`).digest("hex");
|
|
2681
|
-
const expectedBuf = Buffer.from(expected, "utf8");
|
|
2682
|
-
for (const sig of parsed.sigs) {
|
|
2683
|
-
const sigBuf = Buffer.from(sig, "utf8");
|
|
2684
|
-
if (sigBuf.length !== expectedBuf.length) continue;
|
|
2685
|
-
if (timingSafeEqual(sigBuf, expectedBuf)) return true;
|
|
2686
|
-
}
|
|
2687
|
-
return false;
|
|
2688
|
-
}
|
|
2689
|
-
function verifySlackSignature(rawBody, signatureHeader, timestampHeader, secret, options = {}) {
|
|
2690
|
-
if (!signatureHeader.startsWith("v0=")) return false;
|
|
2691
|
-
const ts = Number(timestampHeader);
|
|
2692
|
-
if (!Number.isFinite(ts)) return false;
|
|
2693
|
-
const tolerance = options.toleranceSeconds ?? DEFAULT_SIGNATURE_TOLERANCE_SECONDS;
|
|
2694
|
-
const now = options.now ?? Math.floor(Date.now() / 1e3);
|
|
2695
|
-
if (Math.abs(now - ts) > tolerance) return false;
|
|
2696
|
-
const expected = "v0=" + createHmac2("sha256", secret).update(`v0:${ts}:${rawBody}`).digest("hex");
|
|
2697
|
-
const expectedBuf = Buffer.from(expected, "utf8");
|
|
2698
|
-
const sigBuf = Buffer.from(signatureHeader, "utf8");
|
|
2699
|
-
if (sigBuf.length !== expectedBuf.length) return false;
|
|
2700
|
-
return timingSafeEqual(sigBuf, expectedBuf);
|
|
2701
|
-
}
|
|
2702
|
-
function verifyHmacSignature(rawBody, signatureHeader, secret, options = {}) {
|
|
2703
|
-
const algorithm = options.algorithm ?? "sha256";
|
|
2704
|
-
const prefix = options.signaturePrefix ?? "";
|
|
2705
|
-
const lower = options.lowercaseHex ?? true;
|
|
2706
|
-
let candidate = signatureHeader;
|
|
2707
|
-
if (prefix) {
|
|
2708
|
-
if (!candidate.startsWith(prefix)) return false;
|
|
2709
|
-
candidate = candidate.slice(prefix.length);
|
|
2710
|
-
}
|
|
2711
|
-
if (lower) candidate = candidate.toLowerCase();
|
|
2712
|
-
const expected = createHmac2(algorithm, secret).update(rawBody).digest("hex");
|
|
2713
|
-
const expectedBuf = Buffer.from(expected, "utf8");
|
|
2714
|
-
const sigBuf = Buffer.from(candidate, "utf8");
|
|
2715
|
-
if (sigBuf.length !== expectedBuf.length) return false;
|
|
2716
|
-
return timingSafeEqual(sigBuf, expectedBuf);
|
|
2717
|
-
}
|
|
2718
|
-
function verifyTwilioSignature(input, options = {}) {
|
|
2719
|
-
if (!input.authToken) {
|
|
2720
|
-
return options.skipWhenAuthTokenMissing === true;
|
|
2721
|
-
}
|
|
2722
|
-
const signature = input.signatureHeader;
|
|
2723
|
-
if (!signature || Array.isArray(signature)) return false;
|
|
2724
|
-
if (!input.fullUrl) return false;
|
|
2725
|
-
const data = options.bodyAsRaw === true ? input.fullUrl + (options.rawBody ?? "") : Object.keys(input.params ?? {}).sort().reduce((acc, key) => acc + key + (input.params[key] ?? ""), input.fullUrl);
|
|
2726
|
-
const expected = createHmac2("sha1", input.authToken).update(data).digest("base64");
|
|
2727
|
-
const expectedBuf = Buffer.from(expected);
|
|
2728
|
-
const sigBuf = Buffer.from(signature);
|
|
2729
|
-
if (expectedBuf.length !== sigBuf.length) return false;
|
|
2730
|
-
return timingSafeEqual(expectedBuf, sigBuf);
|
|
2731
|
-
}
|
|
2732
|
-
function firstHeader(headers, name) {
|
|
2733
|
-
const v = headers[name] ?? headers[name.toLowerCase()] ?? Object.entries(headers).find(([key]) => key.toLowerCase() === name.toLowerCase())?.[1];
|
|
2734
|
-
if (Array.isArray(v)) return v[0];
|
|
2735
|
-
return typeof v === "string" ? v : void 0;
|
|
2736
|
-
}
|
|
2737
|
-
|
|
2738
3822
|
// src/connectors/adapters/stripe-webhook-receiver.ts
|
|
2739
3823
|
var stripeWebhookReceiverConnector = {
|
|
2740
3824
|
manifest: {
|
|
@@ -3205,19 +4289,15 @@ export {
|
|
|
3205
4289
|
exchangeAuthorizationCode,
|
|
3206
4290
|
refreshAccessToken,
|
|
3207
4291
|
_resetPendingFlowsForTests,
|
|
3208
|
-
DEFAULT_SIGNATURE_TOLERANCE_SECONDS,
|
|
3209
|
-
parseStripeSignatureHeader,
|
|
3210
|
-
verifyStripeSignature,
|
|
3211
|
-
verifySlackSignature,
|
|
3212
|
-
verifyHmacSignature,
|
|
3213
|
-
verifyTwilioSignature,
|
|
3214
|
-
firstHeader,
|
|
3215
4292
|
googleCalendar,
|
|
4293
|
+
googleDrive,
|
|
3216
4294
|
googleSheets,
|
|
4295
|
+
gmail,
|
|
3217
4296
|
microsoftCalendar,
|
|
3218
4297
|
hubspot,
|
|
3219
4298
|
slack,
|
|
3220
4299
|
notionDatabase,
|
|
4300
|
+
docuseal,
|
|
3221
4301
|
declarativeRestConnector,
|
|
3222
4302
|
twilioSmsConnector,
|
|
3223
4303
|
stripePackConnector,
|
|
@@ -3230,4 +4310,4 @@ export {
|
|
|
3230
4310
|
asanaConnector,
|
|
3231
4311
|
salesforceConnector
|
|
3232
4312
|
};
|
|
3233
|
-
//# sourceMappingURL=chunk-
|
|
4313
|
+
//# sourceMappingURL=chunk-GA4VTE3U.js.map
|