@tangle-network/agent-integrations 0.25.7 → 0.27.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 +6 -2
- package/dist/bin/tangle-catalog-runtime.js.map +1 -1
- package/dist/catalog.d.ts +4 -1
- package/dist/catalog.js +6 -2
- package/dist/chunk-2TW2QKGZ.js +94 -0
- package/dist/chunk-2TW2QKGZ.js.map +1 -0
- package/dist/chunk-ATYHZXLL.js +457 -0
- package/dist/chunk-ATYHZXLL.js.map +1 -0
- package/dist/{chunk-A5I3EYU5.js → chunk-ICSBYCE2.js} +122 -1
- package/dist/chunk-ICSBYCE2.js.map +1 -0
- package/dist/{chunk-WC63AI4Q.js → chunk-JU25UDN2.js} +1252 -225
- package/dist/chunk-JU25UDN2.js.map +1 -0
- package/dist/chunk-P24T3MLM.js +106 -0
- package/dist/chunk-P24T3MLM.js.map +1 -0
- package/dist/chunk-SVQ4PHDZ.js +129 -0
- package/dist/chunk-SVQ4PHDZ.js.map +1 -0
- package/dist/connect/index.d.ts +112 -0
- package/dist/connect/index.js +14 -0
- package/dist/connect/index.js.map +1 -0
- package/dist/connectors/adapters/index.d.ts +593 -1
- package/dist/connectors/adapters/index.js +22 -1
- package/dist/connectors/index.d.ts +2 -1
- package/dist/connectors/index.js +32 -10
- package/dist/index.d.ts +5 -2
- package/dist/index.js +57 -11
- package/dist/middleware/index.d.ts +137 -0
- package/dist/middleware/index.js +14 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/registry.d.ts +165 -2
- package/dist/registry.js +6 -2
- package/dist/runtime.d.ts +4 -1
- package/dist/runtime.js +6 -2
- package/dist/specs.d.ts +4 -1
- package/dist/tangle-catalog-runtime.d.ts +4 -1
- package/dist/tangle-catalog-runtime.js +6 -2
- package/dist/tangle-id-CTU4kGId.d.ts +553 -0
- 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 +25 -12
- package/dist/chunk-A5I3EYU5.js.map +0 -1
- package/dist/chunk-WC63AI4Q.js.map +0 -1
- package/dist/index-BQY5ry2s.d.ts +0 -808
|
@@ -1,55 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
name = "ResourceContention";
|
|
11
|
-
};
|
|
12
|
-
var CredentialsExpired = class extends Error {
|
|
13
|
-
constructor(message, dataSourceId) {
|
|
14
|
-
super(message);
|
|
15
|
-
this.dataSourceId = dataSourceId;
|
|
16
|
-
}
|
|
17
|
-
dataSourceId;
|
|
18
|
-
name = "CredentialsExpired";
|
|
19
|
-
};
|
|
20
|
-
function validateConnectorManifest(manifest) {
|
|
21
|
-
const issues = [];
|
|
22
|
-
if (!manifest.kind.trim()) issues.push({ path: "kind", message: "kind is required" });
|
|
23
|
-
if (!manifest.displayName.trim()) issues.push({ path: "displayName", message: "displayName is required" });
|
|
24
|
-
const seen = /* @__PURE__ */ new Set();
|
|
25
|
-
for (const [index, capability] of manifest.capabilities.entries()) {
|
|
26
|
-
const path = `capabilities[${index}]`;
|
|
27
|
-
if (!capability.name.trim()) issues.push({ path: `${path}.name`, message: "capability name is required" });
|
|
28
|
-
if (seen.has(capability.name)) issues.push({ path: `${path}.name`, message: `duplicate capability name: ${capability.name}` });
|
|
29
|
-
seen.add(capability.name);
|
|
30
|
-
if (capability.class === "mutation") {
|
|
31
|
-
if (!capability.cas) issues.push({ path: `${path}.cas`, message: "mutation capability must declare a CAS strategy" });
|
|
32
|
-
if (manifest.defaultConsistencyModel === "authoritative" && capability.cas === "none") {
|
|
33
|
-
issues.push({ path: `${path}.cas`, message: 'authoritative mutations cannot use cas="none"' });
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
if (manifest.rateLimit) {
|
|
38
|
-
if (!Number.isFinite(manifest.rateLimit.requests) || manifest.rateLimit.requests <= 0) {
|
|
39
|
-
issues.push({ path: "rateLimit.requests", message: "rateLimit.requests must be positive" });
|
|
40
|
-
}
|
|
41
|
-
if (!Number.isFinite(manifest.rateLimit.windowMs) || manifest.rateLimit.windowMs <= 0) {
|
|
42
|
-
issues.push({ path: "rateLimit.windowMs", message: "rateLimit.windowMs must be positive" });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return { ok: issues.length === 0, issues };
|
|
46
|
-
}
|
|
47
|
-
function assertValidConnectorManifest(manifest) {
|
|
48
|
-
const result = validateConnectorManifest(manifest);
|
|
49
|
-
if (!result.ok) {
|
|
50
|
-
throw new Error(`Invalid connector manifest ${manifest.kind || "<unknown>"}: ${result.issues.map((issue) => `${issue.path}: ${issue.message}`).join("; ")}`);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
1
|
+
import {
|
|
2
|
+
firstHeader,
|
|
3
|
+
verifySlackSignature,
|
|
4
|
+
verifyStripeSignature
|
|
5
|
+
} from "./chunk-2TW2QKGZ.js";
|
|
6
|
+
import {
|
|
7
|
+
CredentialsExpired,
|
|
8
|
+
ResourceContention
|
|
9
|
+
} from "./chunk-ATYHZXLL.js";
|
|
53
10
|
|
|
54
11
|
// src/connectors/oauth.ts
|
|
55
12
|
import { createHash, randomBytes } from "crypto";
|
|
@@ -462,11 +419,320 @@ function readMetaString(meta, key) {
|
|
|
462
419
|
return v;
|
|
463
420
|
}
|
|
464
421
|
|
|
422
|
+
// src/connectors/adapters/google-drive.ts
|
|
423
|
+
var SCOPES_READONLY = ["https://www.googleapis.com/auth/drive.readonly"];
|
|
424
|
+
var SCOPE_WATCH = "https://www.googleapis.com/auth/drive";
|
|
425
|
+
var AUTH_URL2 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
426
|
+
var TOKEN_URL2 = "https://oauth2.googleapis.com/token";
|
|
427
|
+
var API = "https://www.googleapis.com/drive/v3";
|
|
428
|
+
function googleDrive(opts) {
|
|
429
|
+
const { clientId, clientSecret } = opts;
|
|
430
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
431
|
+
const scopes = opts.includeWatchScope ? [...SCOPES_READONLY, SCOPE_WATCH] : SCOPES_READONLY;
|
|
432
|
+
const adapter = {
|
|
433
|
+
manifest: {
|
|
434
|
+
kind: "google-drive",
|
|
435
|
+
displayName: "Google Drive",
|
|
436
|
+
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.",
|
|
437
|
+
auth: {
|
|
438
|
+
kind: "oauth2",
|
|
439
|
+
authorizationUrl: AUTH_URL2,
|
|
440
|
+
tokenUrl: TOKEN_URL2,
|
|
441
|
+
scopes,
|
|
442
|
+
clientIdEnv: "GOOGLE_OAUTH_CLIENT_ID",
|
|
443
|
+
clientSecretEnv: "GOOGLE_OAUTH_CLIENT_SECRET",
|
|
444
|
+
extraAuthParams: { access_type: "offline", prompt: "consent", include_granted_scopes: "true" }
|
|
445
|
+
},
|
|
446
|
+
category: "storage",
|
|
447
|
+
defaultConsistencyModel: "authoritative",
|
|
448
|
+
rateLimit: { requests: 1e3, windowMs: 6e4, scope: "oauth-client" },
|
|
449
|
+
capabilities: [
|
|
450
|
+
{
|
|
451
|
+
name: "list_files",
|
|
452
|
+
class: "read",
|
|
453
|
+
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'").`,
|
|
454
|
+
parameters: {
|
|
455
|
+
type: "object",
|
|
456
|
+
properties: {
|
|
457
|
+
folderId: { type: "string", description: "Drive folder id; when present, restricts to direct children." },
|
|
458
|
+
query: { type: "string", description: "Optional Drive query expression; appended with AND if folderId is set." },
|
|
459
|
+
pageSize: { type: "integer", minimum: 1, maximum: 1e3, default: 100 },
|
|
460
|
+
pageToken: { type: "string", description: "Continuation token returned by a previous call." }
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
},
|
|
464
|
+
{
|
|
465
|
+
name: "read_file",
|
|
466
|
+
class: "read",
|
|
467
|
+
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.",
|
|
468
|
+
parameters: {
|
|
469
|
+
type: "object",
|
|
470
|
+
properties: {
|
|
471
|
+
fileId: { type: "string" },
|
|
472
|
+
exportMime: {
|
|
473
|
+
type: "string",
|
|
474
|
+
description: "Export mime override for Google-native types. Defaults: Docs=text/plain, Sheets=text/csv, Slides=application/pdf."
|
|
475
|
+
}
|
|
476
|
+
},
|
|
477
|
+
required: ["fileId"]
|
|
478
|
+
}
|
|
479
|
+
},
|
|
480
|
+
{
|
|
481
|
+
name: "watch_folder",
|
|
482
|
+
class: "mutation",
|
|
483
|
+
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.",
|
|
484
|
+
cas: "native-idempotency",
|
|
485
|
+
externalEffect: true,
|
|
486
|
+
requiredScopes: [SCOPE_WATCH],
|
|
487
|
+
parameters: {
|
|
488
|
+
type: "object",
|
|
489
|
+
properties: {
|
|
490
|
+
folderId: { type: "string" },
|
|
491
|
+
channelId: { type: "string", description: "Caller-controlled UUID; also used as idempotency key." },
|
|
492
|
+
address: { type: "string", description: "HTTPS URL Drive will POST change notifications to." },
|
|
493
|
+
ttlMs: { type: "integer", minimum: 6e4, description: "Channel lifetime in ms. Drive caps at 7 days." }
|
|
494
|
+
},
|
|
495
|
+
required: ["folderId", "channelId", "address"]
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
]
|
|
499
|
+
},
|
|
500
|
+
async executeRead(inv) {
|
|
501
|
+
const accessToken = await ensureFreshAccessToken2(inv.source.credentials, clientId, clientSecret);
|
|
502
|
+
if (inv.capabilityName === "list_files") return listFiles(inv, accessToken, timeoutMs);
|
|
503
|
+
if (inv.capabilityName === "read_file") return readFile(inv, accessToken, timeoutMs);
|
|
504
|
+
throw new Error(`google-drive: unknown read capability ${inv.capabilityName}`);
|
|
505
|
+
},
|
|
506
|
+
async executeMutation(inv) {
|
|
507
|
+
if (inv.capabilityName !== "watch_folder") {
|
|
508
|
+
throw new Error(`google-drive: unknown mutation capability ${inv.capabilityName}`);
|
|
509
|
+
}
|
|
510
|
+
const accessToken = await ensureFreshAccessToken2(inv.source.credentials, clientId, clientSecret);
|
|
511
|
+
return watchFolder(inv, accessToken, timeoutMs);
|
|
512
|
+
},
|
|
513
|
+
async exchangeOAuth(input) {
|
|
514
|
+
const tokens = await exchangeAuthorizationCode({
|
|
515
|
+
tokenUrl: TOKEN_URL2,
|
|
516
|
+
clientId,
|
|
517
|
+
clientSecret,
|
|
518
|
+
code: input.code,
|
|
519
|
+
codeVerifier: input.codeVerifier,
|
|
520
|
+
redirectUri: input.redirectUri
|
|
521
|
+
});
|
|
522
|
+
return {
|
|
523
|
+
credentials: {
|
|
524
|
+
kind: "oauth2",
|
|
525
|
+
accessToken: tokens.accessToken,
|
|
526
|
+
refreshToken: tokens.refreshToken,
|
|
527
|
+
expiresAt: tokens.expiresIn ? Date.now() + tokens.expiresIn * 1e3 : void 0
|
|
528
|
+
},
|
|
529
|
+
scopes: tokens.scope?.split(/\s+/) ?? scopes,
|
|
530
|
+
metadata: {}
|
|
531
|
+
};
|
|
532
|
+
},
|
|
533
|
+
async refreshToken(creds) {
|
|
534
|
+
if (creds.kind !== "oauth2" || !creds.refreshToken) {
|
|
535
|
+
throw new Error("google-drive.refreshToken: missing refresh token");
|
|
536
|
+
}
|
|
537
|
+
const refreshed = await refreshAccessToken({
|
|
538
|
+
tokenUrl: TOKEN_URL2,
|
|
539
|
+
clientId,
|
|
540
|
+
clientSecret,
|
|
541
|
+
refreshToken: creds.refreshToken
|
|
542
|
+
});
|
|
543
|
+
return {
|
|
544
|
+
kind: "oauth2",
|
|
545
|
+
accessToken: refreshed.accessToken,
|
|
546
|
+
refreshToken: refreshed.refreshToken ?? creds.refreshToken,
|
|
547
|
+
expiresAt: refreshed.expiresIn ? Date.now() + refreshed.expiresIn * 1e3 : void 0
|
|
548
|
+
};
|
|
549
|
+
},
|
|
550
|
+
async test(source) {
|
|
551
|
+
try {
|
|
552
|
+
const accessToken = await ensureFreshAccessToken2(source.credentials, clientId, clientSecret);
|
|
553
|
+
const res = await fetch(`${API}/about?fields=user`, {
|
|
554
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
555
|
+
signal: AbortSignal.timeout(8e3)
|
|
556
|
+
});
|
|
557
|
+
if (res.status === 401 || res.status === 403) {
|
|
558
|
+
return { ok: false, reason: `Google rejected Drive token (${res.status}) \u2014 reconnect required` };
|
|
559
|
+
}
|
|
560
|
+
if (!res.ok) return { ok: false, reason: `Google Drive returned ${res.status}` };
|
|
561
|
+
return { ok: true };
|
|
562
|
+
} catch (err) {
|
|
563
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
};
|
|
567
|
+
return adapter;
|
|
568
|
+
}
|
|
569
|
+
async function listFiles(inv, accessToken, timeoutMs) {
|
|
570
|
+
const args = inv.args ?? {};
|
|
571
|
+
const q = [];
|
|
572
|
+
if (args.folderId) q.push(`'${args.folderId.replace(/'/g, "\\'")}' in parents`);
|
|
573
|
+
if (args.query) q.push(`(${args.query})`);
|
|
574
|
+
q.push("trashed = false");
|
|
575
|
+
const params = new URLSearchParams({
|
|
576
|
+
q: q.join(" and "),
|
|
577
|
+
pageSize: String(args.pageSize ?? 100),
|
|
578
|
+
fields: "nextPageToken, files(id,name,mimeType,modifiedTime,size,md5Checksum,parents)"
|
|
579
|
+
});
|
|
580
|
+
if (args.pageToken) params.set("pageToken", args.pageToken);
|
|
581
|
+
const res = await fetch(`${API}/files?${params.toString()}`, {
|
|
582
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
583
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
584
|
+
});
|
|
585
|
+
if (res.status === 401 || res.status === 403) {
|
|
586
|
+
throw new CredentialsExpired(`Google Drive rejected token (${res.status})`, inv.source.id);
|
|
587
|
+
}
|
|
588
|
+
if (!res.ok) {
|
|
589
|
+
const text = await res.text().catch(() => "");
|
|
590
|
+
throw new Error(`google-drive list_files ${res.status}: ${text.slice(0, 200)}`);
|
|
591
|
+
}
|
|
592
|
+
const json = await res.json();
|
|
593
|
+
return {
|
|
594
|
+
data: { files: json.files ?? [], nextPageToken: json.nextPageToken },
|
|
595
|
+
fetchedAt: Date.now()
|
|
596
|
+
};
|
|
597
|
+
}
|
|
598
|
+
var GOOGLE_NATIVE_DEFAULTS = {
|
|
599
|
+
"application/vnd.google-apps.document": "text/plain",
|
|
600
|
+
"application/vnd.google-apps.spreadsheet": "text/csv",
|
|
601
|
+
"application/vnd.google-apps.presentation": "application/pdf"
|
|
602
|
+
};
|
|
603
|
+
async function readFile(inv, accessToken, timeoutMs) {
|
|
604
|
+
const { fileId, exportMime } = inv.args ?? {};
|
|
605
|
+
const metaRes = await fetch(`${API}/files/${encodeURIComponent(fileId)}?fields=id,name,mimeType,modifiedTime`, {
|
|
606
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
607
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
608
|
+
});
|
|
609
|
+
if (metaRes.status === 401 || metaRes.status === 403) {
|
|
610
|
+
throw new CredentialsExpired(`Google Drive rejected token (${metaRes.status})`, inv.source.id);
|
|
611
|
+
}
|
|
612
|
+
if (metaRes.status === 404) {
|
|
613
|
+
throw new Error(`google-drive read_file: file ${fileId} not found`);
|
|
614
|
+
}
|
|
615
|
+
if (!metaRes.ok) {
|
|
616
|
+
const text = await metaRes.text().catch(() => "");
|
|
617
|
+
throw new Error(`google-drive read_file meta ${metaRes.status}: ${text.slice(0, 200)}`);
|
|
618
|
+
}
|
|
619
|
+
const meta = await metaRes.json();
|
|
620
|
+
const isNative = meta.mimeType.startsWith("application/vnd.google-apps.");
|
|
621
|
+
const fetchedAt = Date.now();
|
|
622
|
+
if (isNative) {
|
|
623
|
+
const targetMime = exportMime ?? GOOGLE_NATIVE_DEFAULTS[meta.mimeType] ?? "text/plain";
|
|
624
|
+
const res2 = await fetch(`${API}/files/${encodeURIComponent(fileId)}/export?mimeType=${encodeURIComponent(targetMime)}`, {
|
|
625
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
626
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
627
|
+
});
|
|
628
|
+
if (!res2.ok) {
|
|
629
|
+
const text = await res2.text().catch(() => "");
|
|
630
|
+
throw new Error(`google-drive read_file export ${res2.status}: ${text.slice(0, 200)}`);
|
|
631
|
+
}
|
|
632
|
+
const isTextLike2 = /^text\/|application\/(json|xml|csv|javascript)/.test(targetMime);
|
|
633
|
+
if (isTextLike2) {
|
|
634
|
+
const content = await res2.text();
|
|
635
|
+
return {
|
|
636
|
+
data: { name: meta.name, mimeType: targetMime, content, encoding: "utf-8", modifiedTime: meta.modifiedTime },
|
|
637
|
+
fetchedAt
|
|
638
|
+
};
|
|
639
|
+
}
|
|
640
|
+
const buf2 = Buffer.from(await res2.arrayBuffer());
|
|
641
|
+
return {
|
|
642
|
+
data: { name: meta.name, mimeType: targetMime, content: buf2.toString("base64"), encoding: "base64", modifiedTime: meta.modifiedTime },
|
|
643
|
+
fetchedAt
|
|
644
|
+
};
|
|
645
|
+
}
|
|
646
|
+
const res = await fetch(`${API}/files/${encodeURIComponent(fileId)}?alt=media`, {
|
|
647
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
648
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
649
|
+
});
|
|
650
|
+
if (!res.ok) {
|
|
651
|
+
const text = await res.text().catch(() => "");
|
|
652
|
+
throw new Error(`google-drive read_file media ${res.status}: ${text.slice(0, 200)}`);
|
|
653
|
+
}
|
|
654
|
+
const isTextLike = /^text\/|application\/(json|xml|csv|javascript)/.test(meta.mimeType);
|
|
655
|
+
if (isTextLike) {
|
|
656
|
+
const content = await res.text();
|
|
657
|
+
return {
|
|
658
|
+
data: { name: meta.name, mimeType: meta.mimeType, content, encoding: "utf-8", modifiedTime: meta.modifiedTime },
|
|
659
|
+
fetchedAt
|
|
660
|
+
};
|
|
661
|
+
}
|
|
662
|
+
const buf = Buffer.from(await res.arrayBuffer());
|
|
663
|
+
return {
|
|
664
|
+
data: { name: meta.name, mimeType: meta.mimeType, content: buf.toString("base64"), encoding: "base64", modifiedTime: meta.modifiedTime },
|
|
665
|
+
fetchedAt
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
async function watchFolder(inv, accessToken, timeoutMs) {
|
|
669
|
+
const { folderId, channelId, address, ttlMs } = inv.args;
|
|
670
|
+
const body = {
|
|
671
|
+
id: channelId,
|
|
672
|
+
type: "web_hook",
|
|
673
|
+
address
|
|
674
|
+
};
|
|
675
|
+
if (ttlMs && ttlMs > 0) body.expiration = String(Date.now() + ttlMs);
|
|
676
|
+
const res = await fetch(`${API}/files/${encodeURIComponent(folderId)}/watch`, {
|
|
677
|
+
method: "POST",
|
|
678
|
+
headers: {
|
|
679
|
+
authorization: `Bearer ${accessToken}`,
|
|
680
|
+
"content-type": "application/json"
|
|
681
|
+
},
|
|
682
|
+
body: JSON.stringify(body),
|
|
683
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
684
|
+
});
|
|
685
|
+
if (res.status === 401 || res.status === 403) {
|
|
686
|
+
throw new CredentialsExpired(`Google Drive rejected token (${res.status})`, inv.source.id);
|
|
687
|
+
}
|
|
688
|
+
if (res.status === 409) {
|
|
689
|
+
const cached = inv.source.metadata.watchedChannels?.[channelId];
|
|
690
|
+
return {
|
|
691
|
+
status: "committed",
|
|
692
|
+
data: { channelId, resourceId: cached?.resourceId, expiration: cached?.expiration },
|
|
693
|
+
committedAt: Date.now(),
|
|
694
|
+
idempotentReplay: true
|
|
695
|
+
};
|
|
696
|
+
}
|
|
697
|
+
if (!res.ok) {
|
|
698
|
+
const text = await res.text().catch(() => "");
|
|
699
|
+
throw new Error(`google-drive watch_folder ${res.status}: ${text.slice(0, 200)}`);
|
|
700
|
+
}
|
|
701
|
+
const json = await res.json();
|
|
702
|
+
return {
|
|
703
|
+
status: "committed",
|
|
704
|
+
data: { channelId: json.id, resourceId: json.resourceId, expiration: json.expiration },
|
|
705
|
+
committedAt: Date.now(),
|
|
706
|
+
idempotentReplay: false
|
|
707
|
+
};
|
|
708
|
+
}
|
|
709
|
+
async function ensureFreshAccessToken2(creds, clientId, clientSecret) {
|
|
710
|
+
if (creds.kind !== "oauth2") {
|
|
711
|
+
throw new Error("google-drive: expected oauth2 credentials");
|
|
712
|
+
}
|
|
713
|
+
if (creds.accessToken && (!creds.expiresAt || creds.expiresAt > Date.now() + 6e4)) {
|
|
714
|
+
return creds.accessToken;
|
|
715
|
+
}
|
|
716
|
+
if (!creds.refreshToken) {
|
|
717
|
+
throw new CredentialsExpired("Google Drive access token expired and no refresh token", "");
|
|
718
|
+
}
|
|
719
|
+
const refreshed = await refreshAccessToken({
|
|
720
|
+
tokenUrl: TOKEN_URL2,
|
|
721
|
+
clientId,
|
|
722
|
+
clientSecret,
|
|
723
|
+
refreshToken: creds.refreshToken
|
|
724
|
+
});
|
|
725
|
+
creds.accessToken = refreshed.accessToken;
|
|
726
|
+
creds.expiresAt = refreshed.expiresIn ? Date.now() + refreshed.expiresIn * 1e3 : void 0;
|
|
727
|
+
if (refreshed.refreshToken) creds.refreshToken = refreshed.refreshToken;
|
|
728
|
+
return creds.accessToken;
|
|
729
|
+
}
|
|
730
|
+
|
|
465
731
|
// src/connectors/adapters/google-sheets.ts
|
|
466
732
|
import { createHash as createHash2 } from "crypto";
|
|
467
733
|
var SCOPES2 = ["https://www.googleapis.com/auth/spreadsheets"];
|
|
468
|
-
var
|
|
469
|
-
var
|
|
734
|
+
var AUTH_URL3 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
735
|
+
var TOKEN_URL3 = "https://oauth2.googleapis.com/token";
|
|
470
736
|
function googleSheets(opts) {
|
|
471
737
|
const { clientId, clientSecret } = opts;
|
|
472
738
|
const adapter = {
|
|
@@ -476,8 +742,8 @@ function googleSheets(opts) {
|
|
|
476
742
|
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
743
|
auth: {
|
|
478
744
|
kind: "oauth2",
|
|
479
|
-
authorizationUrl:
|
|
480
|
-
tokenUrl:
|
|
745
|
+
authorizationUrl: AUTH_URL3,
|
|
746
|
+
tokenUrl: TOKEN_URL3,
|
|
481
747
|
scopes: SCOPES2,
|
|
482
748
|
clientIdEnv: "GOOGLE_OAUTH_CLIENT_ID",
|
|
483
749
|
clientSecretEnv: "GOOGLE_OAUTH_CLIENT_SECRET",
|
|
@@ -544,7 +810,7 @@ function googleSheets(opts) {
|
|
|
544
810
|
},
|
|
545
811
|
async executeRead(inv) {
|
|
546
812
|
const meta = readSheetMeta(inv.source.metadata);
|
|
547
|
-
const accessToken = await
|
|
813
|
+
const accessToken = await ensureFreshAccessToken3(inv.source.credentials, clientId, clientSecret);
|
|
548
814
|
const rows = await fetchAllRows(accessToken, meta);
|
|
549
815
|
const limit = clampLimit(inv.args.limit, 100);
|
|
550
816
|
let filtered = rows;
|
|
@@ -571,7 +837,7 @@ function googleSheets(opts) {
|
|
|
571
837
|
throw new Error(`google-sheets: unknown mutation ${inv.capabilityName}`);
|
|
572
838
|
}
|
|
573
839
|
const meta = readSheetMeta(inv.source.metadata);
|
|
574
|
-
const accessToken = await
|
|
840
|
+
const accessToken = await ensureFreshAccessToken3(inv.source.credentials, clientId, clientSecret);
|
|
575
841
|
const { rowKey, patch, expectedFingerprint } = inv.args;
|
|
576
842
|
const rows = await fetchAllRows(accessToken, meta);
|
|
577
843
|
const target = rows.find((r) => normalizeKey(r.values[meta.keyColumn]) === normalizeKey(rowKey));
|
|
@@ -627,7 +893,7 @@ function googleSheets(opts) {
|
|
|
627
893
|
},
|
|
628
894
|
async exchangeOAuth(input) {
|
|
629
895
|
const tokens = await exchangeAuthorizationCode({
|
|
630
|
-
tokenUrl:
|
|
896
|
+
tokenUrl: TOKEN_URL3,
|
|
631
897
|
clientId,
|
|
632
898
|
clientSecret,
|
|
633
899
|
code: input.code,
|
|
@@ -652,7 +918,7 @@ function googleSheets(opts) {
|
|
|
652
918
|
throw new Error("google-sheets.refreshToken: missing refresh token");
|
|
653
919
|
}
|
|
654
920
|
const refreshed = await refreshAccessToken({
|
|
655
|
-
tokenUrl:
|
|
921
|
+
tokenUrl: TOKEN_URL3,
|
|
656
922
|
clientId,
|
|
657
923
|
clientSecret,
|
|
658
924
|
refreshToken: creds.refreshToken
|
|
@@ -666,7 +932,7 @@ function googleSheets(opts) {
|
|
|
666
932
|
},
|
|
667
933
|
async test(source) {
|
|
668
934
|
try {
|
|
669
|
-
const accessToken = await
|
|
935
|
+
const accessToken = await ensureFreshAccessToken3(source.credentials, clientId, clientSecret);
|
|
670
936
|
const meta = readSheetMeta(source.metadata);
|
|
671
937
|
if (!meta.spreadsheetId) {
|
|
672
938
|
return { ok: false, reason: "spreadsheetId not configured \u2014 pick a sheet in the connection settings" };
|
|
@@ -764,7 +1030,7 @@ function columnIndexToLetter(idx) {
|
|
|
764
1030
|
}
|
|
765
1031
|
return s;
|
|
766
1032
|
}
|
|
767
|
-
async function
|
|
1033
|
+
async function ensureFreshAccessToken3(creds, clientId, clientSecret) {
|
|
768
1034
|
if (creds.kind !== "oauth2") {
|
|
769
1035
|
throw new Error("google-sheets: expected oauth2 credentials");
|
|
770
1036
|
}
|
|
@@ -775,7 +1041,403 @@ async function ensureFreshAccessToken2(creds, clientId, clientSecret) {
|
|
|
775
1041
|
throw new CredentialsExpired("Google Sheets access token expired and no refresh token", "");
|
|
776
1042
|
}
|
|
777
1043
|
const refreshed = await refreshAccessToken({
|
|
778
|
-
tokenUrl:
|
|
1044
|
+
tokenUrl: TOKEN_URL3,
|
|
1045
|
+
clientId,
|
|
1046
|
+
clientSecret,
|
|
1047
|
+
refreshToken: creds.refreshToken
|
|
1048
|
+
});
|
|
1049
|
+
creds.accessToken = refreshed.accessToken;
|
|
1050
|
+
creds.expiresAt = refreshed.expiresIn ? Date.now() + refreshed.expiresIn * 1e3 : void 0;
|
|
1051
|
+
if (refreshed.refreshToken) creds.refreshToken = refreshed.refreshToken;
|
|
1052
|
+
return creds.accessToken;
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
// src/connectors/adapters/gmail.ts
|
|
1056
|
+
var SCOPE_READ = "https://www.googleapis.com/auth/gmail.readonly";
|
|
1057
|
+
var SCOPE_SEND = "https://www.googleapis.com/auth/gmail.send";
|
|
1058
|
+
var SCOPE_MODIFY = "https://www.googleapis.com/auth/gmail.modify";
|
|
1059
|
+
var AUTH_URL4 = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
1060
|
+
var TOKEN_URL4 = "https://oauth2.googleapis.com/token";
|
|
1061
|
+
var API2 = "https://gmail.googleapis.com/gmail/v1/users/me";
|
|
1062
|
+
function gmail(opts) {
|
|
1063
|
+
const { clientId, clientSecret } = opts;
|
|
1064
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
1065
|
+
const scopes = opts.scopes ?? [SCOPE_READ, SCOPE_SEND, SCOPE_MODIFY];
|
|
1066
|
+
const adapter = {
|
|
1067
|
+
manifest: {
|
|
1068
|
+
kind: "gmail",
|
|
1069
|
+
displayName: "Gmail",
|
|
1070
|
+
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).",
|
|
1071
|
+
auth: {
|
|
1072
|
+
kind: "oauth2",
|
|
1073
|
+
authorizationUrl: AUTH_URL4,
|
|
1074
|
+
tokenUrl: TOKEN_URL4,
|
|
1075
|
+
scopes,
|
|
1076
|
+
clientIdEnv: "GOOGLE_OAUTH_CLIENT_ID",
|
|
1077
|
+
clientSecretEnv: "GOOGLE_OAUTH_CLIENT_SECRET",
|
|
1078
|
+
extraAuthParams: { access_type: "offline", prompt: "consent", include_granted_scopes: "true" }
|
|
1079
|
+
},
|
|
1080
|
+
category: "comms",
|
|
1081
|
+
defaultConsistencyModel: "authoritative",
|
|
1082
|
+
rateLimit: { requests: 250, windowMs: 1e3, scope: "oauth-client" },
|
|
1083
|
+
capabilities: [
|
|
1084
|
+
{
|
|
1085
|
+
name: "list_messages",
|
|
1086
|
+
class: "read",
|
|
1087
|
+
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.",
|
|
1088
|
+
requiredScopes: [SCOPE_READ],
|
|
1089
|
+
parameters: {
|
|
1090
|
+
type: "object",
|
|
1091
|
+
properties: {
|
|
1092
|
+
labelIds: { type: "array", items: { type: "string" }, description: 'Default: ["INBOX"]' },
|
|
1093
|
+
query: { type: "string", description: "Gmail query syntax." },
|
|
1094
|
+
maxResults: { type: "integer", minimum: 1, maximum: 500, default: 25 },
|
|
1095
|
+
pageToken: { type: "string" }
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
name: "read_message",
|
|
1101
|
+
class: "read",
|
|
1102
|
+
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.",
|
|
1103
|
+
requiredScopes: [SCOPE_READ],
|
|
1104
|
+
parameters: {
|
|
1105
|
+
type: "object",
|
|
1106
|
+
properties: {
|
|
1107
|
+
id: { type: "string" }
|
|
1108
|
+
},
|
|
1109
|
+
required: ["id"]
|
|
1110
|
+
}
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
name: "send_reply",
|
|
1114
|
+
class: "mutation",
|
|
1115
|
+
description: "Send a reply on a thread. Pulls In-Reply-To/References from the latest message in the thread. Body is text/plain.",
|
|
1116
|
+
cas: "native-idempotency",
|
|
1117
|
+
externalEffect: true,
|
|
1118
|
+
requiredScopes: [SCOPE_SEND, SCOPE_READ],
|
|
1119
|
+
parameters: {
|
|
1120
|
+
type: "object",
|
|
1121
|
+
properties: {
|
|
1122
|
+
threadId: { type: "string" },
|
|
1123
|
+
body: { type: "string", description: "text/plain body" },
|
|
1124
|
+
replyAll: { type: "boolean", default: false },
|
|
1125
|
+
cc: { type: "array", items: { type: "string" } }
|
|
1126
|
+
},
|
|
1127
|
+
required: ["threadId", "body"]
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
{
|
|
1131
|
+
name: "watch_label",
|
|
1132
|
+
class: "mutation",
|
|
1133
|
+
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.",
|
|
1134
|
+
cas: "native-idempotency",
|
|
1135
|
+
externalEffect: true,
|
|
1136
|
+
requiredScopes: [SCOPE_MODIFY],
|
|
1137
|
+
parameters: {
|
|
1138
|
+
type: "object",
|
|
1139
|
+
properties: {
|
|
1140
|
+
labelIds: { type: "array", items: { type: "string" }, description: 'Default: ["INBOX"]' },
|
|
1141
|
+
topicName: { type: "string", description: "projects/<id>/topics/<name>" },
|
|
1142
|
+
labelFilterAction: { type: "string", enum: ["include", "exclude"], default: "include" }
|
|
1143
|
+
},
|
|
1144
|
+
required: ["topicName"]
|
|
1145
|
+
}
|
|
1146
|
+
}
|
|
1147
|
+
]
|
|
1148
|
+
},
|
|
1149
|
+
async executeRead(inv) {
|
|
1150
|
+
const accessToken = await ensureFreshAccessToken4(inv.source.credentials, clientId, clientSecret);
|
|
1151
|
+
if (inv.capabilityName === "list_messages") return listMessages(inv, accessToken, timeoutMs);
|
|
1152
|
+
if (inv.capabilityName === "read_message") return readMessage(inv, accessToken, timeoutMs);
|
|
1153
|
+
throw new Error(`gmail: unknown read capability ${inv.capabilityName}`);
|
|
1154
|
+
},
|
|
1155
|
+
async executeMutation(inv) {
|
|
1156
|
+
const accessToken = await ensureFreshAccessToken4(inv.source.credentials, clientId, clientSecret);
|
|
1157
|
+
if (inv.capabilityName === "send_reply") return sendReply(inv, accessToken, timeoutMs);
|
|
1158
|
+
if (inv.capabilityName === "watch_label") return watchLabel(inv, accessToken, timeoutMs);
|
|
1159
|
+
throw new Error(`gmail: unknown mutation capability ${inv.capabilityName}`);
|
|
1160
|
+
},
|
|
1161
|
+
async exchangeOAuth(input) {
|
|
1162
|
+
const tokens = await exchangeAuthorizationCode({
|
|
1163
|
+
tokenUrl: TOKEN_URL4,
|
|
1164
|
+
clientId,
|
|
1165
|
+
clientSecret,
|
|
1166
|
+
code: input.code,
|
|
1167
|
+
codeVerifier: input.codeVerifier,
|
|
1168
|
+
redirectUri: input.redirectUri
|
|
1169
|
+
});
|
|
1170
|
+
return {
|
|
1171
|
+
credentials: {
|
|
1172
|
+
kind: "oauth2",
|
|
1173
|
+
accessToken: tokens.accessToken,
|
|
1174
|
+
refreshToken: tokens.refreshToken,
|
|
1175
|
+
expiresAt: tokens.expiresIn ? Date.now() + tokens.expiresIn * 1e3 : void 0
|
|
1176
|
+
},
|
|
1177
|
+
scopes: tokens.scope?.split(/\s+/) ?? scopes,
|
|
1178
|
+
metadata: {}
|
|
1179
|
+
};
|
|
1180
|
+
},
|
|
1181
|
+
async refreshToken(creds) {
|
|
1182
|
+
if (creds.kind !== "oauth2" || !creds.refreshToken) {
|
|
1183
|
+
throw new Error("gmail.refreshToken: missing refresh token");
|
|
1184
|
+
}
|
|
1185
|
+
const refreshed = await refreshAccessToken({
|
|
1186
|
+
tokenUrl: TOKEN_URL4,
|
|
1187
|
+
clientId,
|
|
1188
|
+
clientSecret,
|
|
1189
|
+
refreshToken: creds.refreshToken
|
|
1190
|
+
});
|
|
1191
|
+
return {
|
|
1192
|
+
kind: "oauth2",
|
|
1193
|
+
accessToken: refreshed.accessToken,
|
|
1194
|
+
refreshToken: refreshed.refreshToken ?? creds.refreshToken,
|
|
1195
|
+
expiresAt: refreshed.expiresIn ? Date.now() + refreshed.expiresIn * 1e3 : void 0
|
|
1196
|
+
};
|
|
1197
|
+
},
|
|
1198
|
+
async test(source) {
|
|
1199
|
+
try {
|
|
1200
|
+
const accessToken = await ensureFreshAccessToken4(source.credentials, clientId, clientSecret);
|
|
1201
|
+
const res = await fetch(`${API2}/profile`, {
|
|
1202
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1203
|
+
signal: AbortSignal.timeout(8e3)
|
|
1204
|
+
});
|
|
1205
|
+
if (res.status === 401 || res.status === 403) {
|
|
1206
|
+
return { ok: false, reason: `Google rejected Gmail token (${res.status}) \u2014 reconnect required` };
|
|
1207
|
+
}
|
|
1208
|
+
if (!res.ok) return { ok: false, reason: `Gmail returned ${res.status}` };
|
|
1209
|
+
return { ok: true };
|
|
1210
|
+
} catch (err) {
|
|
1211
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
1212
|
+
}
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
return adapter;
|
|
1216
|
+
}
|
|
1217
|
+
async function listMessages(inv, accessToken, timeoutMs) {
|
|
1218
|
+
const args = inv.args ?? {};
|
|
1219
|
+
const params = new URLSearchParams({
|
|
1220
|
+
maxResults: String(args.maxResults ?? 25)
|
|
1221
|
+
});
|
|
1222
|
+
for (const id of args.labelIds ?? ["INBOX"]) params.append("labelIds", id);
|
|
1223
|
+
if (args.query) params.set("q", args.query);
|
|
1224
|
+
if (args.pageToken) params.set("pageToken", args.pageToken);
|
|
1225
|
+
const listRes = await fetch(`${API2}/messages?${params.toString()}`, {
|
|
1226
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1227
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1228
|
+
});
|
|
1229
|
+
if (listRes.status === 401 || listRes.status === 403) {
|
|
1230
|
+
throw new CredentialsExpired(`Gmail rejected token (${listRes.status})`, inv.source.id);
|
|
1231
|
+
}
|
|
1232
|
+
if (!listRes.ok) {
|
|
1233
|
+
const text = await listRes.text().catch(() => "");
|
|
1234
|
+
throw new Error(`gmail list_messages ${listRes.status}: ${text.slice(0, 200)}`);
|
|
1235
|
+
}
|
|
1236
|
+
const listJson = await listRes.json();
|
|
1237
|
+
const ids = listJson.messages ?? [];
|
|
1238
|
+
const metas = await Promise.all(
|
|
1239
|
+
ids.map(async ({ id }) => {
|
|
1240
|
+
const res = await fetch(`${API2}/messages/${encodeURIComponent(id)}?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Subject&metadataHeaders=Date`, {
|
|
1241
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1242
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1243
|
+
});
|
|
1244
|
+
if (!res.ok) return null;
|
|
1245
|
+
return await res.json();
|
|
1246
|
+
})
|
|
1247
|
+
);
|
|
1248
|
+
const messages = metas.filter((m) => Boolean(m)).map(toMessageSummary);
|
|
1249
|
+
return {
|
|
1250
|
+
data: { messages, nextPageToken: listJson.nextPageToken },
|
|
1251
|
+
fetchedAt: Date.now()
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
function toMessageSummary(meta) {
|
|
1255
|
+
const headers = new Map((meta.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
|
|
1256
|
+
return {
|
|
1257
|
+
id: meta.id,
|
|
1258
|
+
threadId: meta.threadId,
|
|
1259
|
+
snippet: meta.snippet,
|
|
1260
|
+
internalDate: meta.internalDate,
|
|
1261
|
+
labelIds: meta.labelIds ?? [],
|
|
1262
|
+
from: headers.get("from"),
|
|
1263
|
+
to: headers.get("to"),
|
|
1264
|
+
subject: headers.get("subject"),
|
|
1265
|
+
date: headers.get("date")
|
|
1266
|
+
};
|
|
1267
|
+
}
|
|
1268
|
+
async function readMessage(inv, accessToken, timeoutMs) {
|
|
1269
|
+
const { id } = inv.args;
|
|
1270
|
+
const res = await fetch(`${API2}/messages/${encodeURIComponent(id)}?format=full`, {
|
|
1271
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1272
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1273
|
+
});
|
|
1274
|
+
if (res.status === 401 || res.status === 403) {
|
|
1275
|
+
throw new CredentialsExpired(`Gmail rejected token (${res.status})`, inv.source.id);
|
|
1276
|
+
}
|
|
1277
|
+
if (res.status === 404) {
|
|
1278
|
+
throw new Error(`gmail read_message: message ${id} not found`);
|
|
1279
|
+
}
|
|
1280
|
+
if (!res.ok) {
|
|
1281
|
+
const text = await res.text().catch(() => "");
|
|
1282
|
+
throw new Error(`gmail read_message ${res.status}: ${text.slice(0, 200)}`);
|
|
1283
|
+
}
|
|
1284
|
+
const full = await res.json();
|
|
1285
|
+
const headers = new Map((full.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
|
|
1286
|
+
const body = {};
|
|
1287
|
+
const attachments = [];
|
|
1288
|
+
walkParts(full.payload, body, attachments);
|
|
1289
|
+
return {
|
|
1290
|
+
data: {
|
|
1291
|
+
id: full.id,
|
|
1292
|
+
threadId: full.threadId,
|
|
1293
|
+
internalDate: full.internalDate,
|
|
1294
|
+
labelIds: full.labelIds ?? [],
|
|
1295
|
+
from: headers.get("from"),
|
|
1296
|
+
to: headers.get("to"),
|
|
1297
|
+
cc: headers.get("cc"),
|
|
1298
|
+
subject: headers.get("subject"),
|
|
1299
|
+
date: headers.get("date"),
|
|
1300
|
+
body,
|
|
1301
|
+
attachments
|
|
1302
|
+
},
|
|
1303
|
+
fetchedAt: Date.now()
|
|
1304
|
+
};
|
|
1305
|
+
}
|
|
1306
|
+
function walkParts(part, body, attachments) {
|
|
1307
|
+
if (!part) return;
|
|
1308
|
+
if (part.body?.data && part.mimeType === "text/plain" && !body.text) {
|
|
1309
|
+
body.text = decodeBase64Url(part.body.data);
|
|
1310
|
+
} else if (part.body?.data && part.mimeType === "text/html" && !body.html) {
|
|
1311
|
+
body.html = decodeBase64Url(part.body.data);
|
|
1312
|
+
}
|
|
1313
|
+
if (part.filename && part.body?.attachmentId) {
|
|
1314
|
+
attachments.push({
|
|
1315
|
+
filename: part.filename,
|
|
1316
|
+
mimeType: part.mimeType ?? "application/octet-stream",
|
|
1317
|
+
attachmentId: part.body.attachmentId,
|
|
1318
|
+
size: part.body.size ?? 0
|
|
1319
|
+
});
|
|
1320
|
+
}
|
|
1321
|
+
for (const child of part.parts ?? []) walkParts(child, body, attachments);
|
|
1322
|
+
}
|
|
1323
|
+
function decodeBase64Url(s) {
|
|
1324
|
+
return Buffer.from(s.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8");
|
|
1325
|
+
}
|
|
1326
|
+
function encodeBase64Url(s) {
|
|
1327
|
+
return Buffer.from(s, "utf-8").toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
1328
|
+
}
|
|
1329
|
+
async function sendReply(inv, accessToken, timeoutMs) {
|
|
1330
|
+
const { threadId, body, replyAll, cc } = inv.args;
|
|
1331
|
+
const threadRes = await fetch(`${API2}/threads/${encodeURIComponent(threadId)}?format=metadata&metadataHeaders=From&metadataHeaders=To&metadataHeaders=Cc&metadataHeaders=Subject&metadataHeaders=Message-ID&metadataHeaders=References`, {
|
|
1332
|
+
headers: { authorization: `Bearer ${accessToken}` },
|
|
1333
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1334
|
+
});
|
|
1335
|
+
if (threadRes.status === 401 || threadRes.status === 403) {
|
|
1336
|
+
throw new CredentialsExpired(`Gmail rejected token (${threadRes.status})`, inv.source.id);
|
|
1337
|
+
}
|
|
1338
|
+
if (!threadRes.ok) {
|
|
1339
|
+
const text = await threadRes.text().catch(() => "");
|
|
1340
|
+
throw new Error(`gmail send_reply thread fetch ${threadRes.status}: ${text.slice(0, 200)}`);
|
|
1341
|
+
}
|
|
1342
|
+
const thread = await threadRes.json();
|
|
1343
|
+
const last = thread.messages?.[thread.messages.length - 1];
|
|
1344
|
+
if (!last) throw new Error(`gmail send_reply: thread ${threadId} has no messages`);
|
|
1345
|
+
const lastHeaders = new Map((last.payload?.headers ?? []).map((h) => [h.name.toLowerCase(), h.value]));
|
|
1346
|
+
const inReplyTo = lastHeaders.get("message-id");
|
|
1347
|
+
const refsHeader = lastHeaders.get("references");
|
|
1348
|
+
const refs = refsHeader ? `${refsHeader} ${inReplyTo ?? ""}`.trim() : inReplyTo;
|
|
1349
|
+
const fromHeader = lastHeaders.get("from");
|
|
1350
|
+
const toHeader = lastHeaders.get("to");
|
|
1351
|
+
const ccHeader = lastHeaders.get("cc");
|
|
1352
|
+
const subject = lastHeaders.get("subject") ?? "";
|
|
1353
|
+
const rfcHeaders = [];
|
|
1354
|
+
if (fromHeader) rfcHeaders.push(`To: ${fromHeader}`);
|
|
1355
|
+
if (replyAll) {
|
|
1356
|
+
const extra = [toHeader, ccHeader].filter(Boolean).join(", ");
|
|
1357
|
+
if (extra) rfcHeaders.push(`Cc: ${extra}`);
|
|
1358
|
+
}
|
|
1359
|
+
if (cc?.length) {
|
|
1360
|
+
const existing = rfcHeaders.findIndex((h) => h.startsWith("Cc: "));
|
|
1361
|
+
if (existing >= 0) rfcHeaders[existing] = `${rfcHeaders[existing]}, ${cc.join(", ")}`;
|
|
1362
|
+
else rfcHeaders.push(`Cc: ${cc.join(", ")}`);
|
|
1363
|
+
}
|
|
1364
|
+
rfcHeaders.push(`Subject: ${subject.toLowerCase().startsWith("re:") ? subject : "Re: " + subject}`);
|
|
1365
|
+
if (inReplyTo) rfcHeaders.push(`In-Reply-To: ${inReplyTo}`);
|
|
1366
|
+
if (refs) rfcHeaders.push(`References: ${refs}`);
|
|
1367
|
+
rfcHeaders.push(`X-Tangle-Idempotency-Key: ${inv.idempotencyKey}`);
|
|
1368
|
+
rfcHeaders.push('Content-Type: text/plain; charset="UTF-8"');
|
|
1369
|
+
rfcHeaders.push("MIME-Version: 1.0");
|
|
1370
|
+
const raw = `${rfcHeaders.join("\r\n")}\r
|
|
1371
|
+
\r
|
|
1372
|
+
${body}`;
|
|
1373
|
+
const sendBody = { threadId, raw: encodeBase64Url(raw) };
|
|
1374
|
+
const sendRes = await fetch(`${API2}/messages/send`, {
|
|
1375
|
+
method: "POST",
|
|
1376
|
+
headers: {
|
|
1377
|
+
authorization: `Bearer ${accessToken}`,
|
|
1378
|
+
"content-type": "application/json"
|
|
1379
|
+
},
|
|
1380
|
+
body: JSON.stringify(sendBody),
|
|
1381
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1382
|
+
});
|
|
1383
|
+
if (sendRes.status === 401 || sendRes.status === 403) {
|
|
1384
|
+
throw new CredentialsExpired(`Gmail rejected token (${sendRes.status})`, inv.source.id);
|
|
1385
|
+
}
|
|
1386
|
+
if (!sendRes.ok) {
|
|
1387
|
+
const text = await sendRes.text().catch(() => "");
|
|
1388
|
+
throw new Error(`gmail send_reply ${sendRes.status}: ${text.slice(0, 200)}`);
|
|
1389
|
+
}
|
|
1390
|
+
const sent = await sendRes.json();
|
|
1391
|
+
return {
|
|
1392
|
+
status: "committed",
|
|
1393
|
+
data: { id: sent.id, threadId: sent.threadId, labelIds: sent.labelIds ?? [] },
|
|
1394
|
+
committedAt: Date.now(),
|
|
1395
|
+
idempotentReplay: false
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
async function watchLabel(inv, accessToken, timeoutMs) {
|
|
1399
|
+
const { labelIds, topicName, labelFilterAction } = inv.args;
|
|
1400
|
+
const body = {
|
|
1401
|
+
topicName,
|
|
1402
|
+
labelIds: labelIds ?? ["INBOX"],
|
|
1403
|
+
labelFilterAction: labelFilterAction ?? "include"
|
|
1404
|
+
};
|
|
1405
|
+
const res = await fetch(`${API2}/watch`, {
|
|
1406
|
+
method: "POST",
|
|
1407
|
+
headers: {
|
|
1408
|
+
authorization: `Bearer ${accessToken}`,
|
|
1409
|
+
"content-type": "application/json"
|
|
1410
|
+
},
|
|
1411
|
+
body: JSON.stringify(body),
|
|
1412
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
1413
|
+
});
|
|
1414
|
+
if (res.status === 401 || res.status === 403) {
|
|
1415
|
+
throw new CredentialsExpired(`Gmail rejected token (${res.status})`, inv.source.id);
|
|
1416
|
+
}
|
|
1417
|
+
if (!res.ok) {
|
|
1418
|
+
const text = await res.text().catch(() => "");
|
|
1419
|
+
throw new Error(`gmail watch_label ${res.status}: ${text.slice(0, 200)}`);
|
|
1420
|
+
}
|
|
1421
|
+
const json = await res.json();
|
|
1422
|
+
return {
|
|
1423
|
+
status: "committed",
|
|
1424
|
+
data: { historyId: json.historyId, expiration: json.expiration, topicName, labelIds: body.labelIds },
|
|
1425
|
+
committedAt: Date.now(),
|
|
1426
|
+
idempotentReplay: false
|
|
1427
|
+
};
|
|
1428
|
+
}
|
|
1429
|
+
async function ensureFreshAccessToken4(creds, clientId, clientSecret) {
|
|
1430
|
+
if (creds.kind !== "oauth2") {
|
|
1431
|
+
throw new Error("gmail: expected oauth2 credentials");
|
|
1432
|
+
}
|
|
1433
|
+
if (creds.accessToken && (!creds.expiresAt || creds.expiresAt > Date.now() + 6e4)) {
|
|
1434
|
+
return creds.accessToken;
|
|
1435
|
+
}
|
|
1436
|
+
if (!creds.refreshToken) {
|
|
1437
|
+
throw new CredentialsExpired("Gmail access token expired and no refresh token", "");
|
|
1438
|
+
}
|
|
1439
|
+
const refreshed = await refreshAccessToken({
|
|
1440
|
+
tokenUrl: TOKEN_URL4,
|
|
779
1441
|
clientId,
|
|
780
1442
|
clientSecret,
|
|
781
1443
|
refreshToken: creds.refreshToken
|
|
@@ -794,8 +1456,8 @@ var SCOPES3 = [
|
|
|
794
1456
|
// connection silently dies after ~1 hour.
|
|
795
1457
|
"offline_access"
|
|
796
1458
|
];
|
|
797
|
-
var
|
|
798
|
-
var
|
|
1459
|
+
var AUTH_URL5 = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
|
|
1460
|
+
var TOKEN_URL5 = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
|
|
799
1461
|
function microsoftCalendar(opts) {
|
|
800
1462
|
const { clientId, clientSecret } = opts;
|
|
801
1463
|
const adapter = {
|
|
@@ -805,8 +1467,8 @@ function microsoftCalendar(opts) {
|
|
|
805
1467
|
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
1468
|
auth: {
|
|
807
1469
|
kind: "oauth2",
|
|
808
|
-
authorizationUrl:
|
|
809
|
-
tokenUrl:
|
|
1470
|
+
authorizationUrl: AUTH_URL5,
|
|
1471
|
+
tokenUrl: TOKEN_URL5,
|
|
810
1472
|
scopes: SCOPES3,
|
|
811
1473
|
clientIdEnv: "MS_OAUTH_CLIENT_ID",
|
|
812
1474
|
clientSecretEnv: "MS_OAUTH_CLIENT_SECRET"
|
|
@@ -858,7 +1520,7 @@ function microsoftCalendar(opts) {
|
|
|
858
1520
|
}
|
|
859
1521
|
const userPrincipal = readMetaString2(inv.source.metadata, "userPrincipal");
|
|
860
1522
|
const { timeMin, timeMax } = inv.args;
|
|
861
|
-
const accessToken = await
|
|
1523
|
+
const accessToken = await ensureFreshAccessToken5(inv.source.credentials, clientId, clientSecret);
|
|
862
1524
|
const busy = await getScheduleBusy({ accessToken, userPrincipal, timeMin, timeMax });
|
|
863
1525
|
return {
|
|
864
1526
|
data: { busy },
|
|
@@ -871,7 +1533,7 @@ function microsoftCalendar(opts) {
|
|
|
871
1533
|
}
|
|
872
1534
|
const userPrincipal = readMetaString2(inv.source.metadata, "userPrincipal");
|
|
873
1535
|
const { start, end, summary, description, attendees } = inv.args;
|
|
874
|
-
const accessToken = await
|
|
1536
|
+
const accessToken = await ensureFreshAccessToken5(inv.source.credentials, clientId, clientSecret);
|
|
875
1537
|
const busy = await getScheduleBusy({ accessToken, userPrincipal, timeMin: start, timeMax: end });
|
|
876
1538
|
if (busy.length > 0) {
|
|
877
1539
|
const startMs = Date.parse(start);
|
|
@@ -936,7 +1598,7 @@ function microsoftCalendar(opts) {
|
|
|
936
1598
|
throw new Error("Microsoft OAuth client not configured (MS_OAUTH_CLIENT_ID / _SECRET)");
|
|
937
1599
|
}
|
|
938
1600
|
const tokens = await exchangeAuthorizationCode({
|
|
939
|
-
tokenUrl:
|
|
1601
|
+
tokenUrl: TOKEN_URL5,
|
|
940
1602
|
clientId,
|
|
941
1603
|
clientSecret,
|
|
942
1604
|
code: input.code,
|
|
@@ -961,7 +1623,7 @@ function microsoftCalendar(opts) {
|
|
|
961
1623
|
throw new Error("microsoft-calendar.refreshToken: missing refresh token");
|
|
962
1624
|
}
|
|
963
1625
|
const refreshed = await refreshAccessToken({
|
|
964
|
-
tokenUrl:
|
|
1626
|
+
tokenUrl: TOKEN_URL5,
|
|
965
1627
|
clientId,
|
|
966
1628
|
clientSecret,
|
|
967
1629
|
refreshToken: creds.refreshToken
|
|
@@ -975,7 +1637,7 @@ function microsoftCalendar(opts) {
|
|
|
975
1637
|
},
|
|
976
1638
|
async test(source) {
|
|
977
1639
|
try {
|
|
978
|
-
const accessToken = await
|
|
1640
|
+
const accessToken = await ensureFreshAccessToken5(source.credentials, clientId, clientSecret);
|
|
979
1641
|
const res = await fetch("https://graph.microsoft.com/v1.0/me?$select=id", {
|
|
980
1642
|
headers: { authorization: `Bearer ${accessToken}` },
|
|
981
1643
|
signal: AbortSignal.timeout(8e3)
|
|
@@ -1048,7 +1710,7 @@ async function findNextFreeSlots2(input) {
|
|
|
1048
1710
|
}
|
|
1049
1711
|
return out.slice(0, input.wanted);
|
|
1050
1712
|
}
|
|
1051
|
-
async function
|
|
1713
|
+
async function ensureFreshAccessToken5(creds, clientId, clientSecret) {
|
|
1052
1714
|
if (creds.kind !== "oauth2") {
|
|
1053
1715
|
throw new Error("microsoft-calendar: expected oauth2 credentials");
|
|
1054
1716
|
}
|
|
@@ -1059,7 +1721,7 @@ async function ensureFreshAccessToken3(creds, clientId, clientSecret) {
|
|
|
1059
1721
|
throw new CredentialsExpired("Microsoft Calendar access token expired and no refresh token", "");
|
|
1060
1722
|
}
|
|
1061
1723
|
const refreshed = await refreshAccessToken({
|
|
1062
|
-
tokenUrl:
|
|
1724
|
+
tokenUrl: TOKEN_URL5,
|
|
1063
1725
|
clientId,
|
|
1064
1726
|
clientSecret,
|
|
1065
1727
|
refreshToken: creds.refreshToken
|
|
@@ -1082,9 +1744,9 @@ var SCOPES4 = [
|
|
|
1082
1744
|
"crm.objects.contacts.read",
|
|
1083
1745
|
"crm.objects.contacts.write"
|
|
1084
1746
|
];
|
|
1085
|
-
var
|
|
1086
|
-
var
|
|
1087
|
-
var
|
|
1747
|
+
var AUTH_URL6 = "https://app.hubspot.com/oauth/authorize";
|
|
1748
|
+
var TOKEN_URL6 = "https://api.hubapi.com/oauth/v1/token";
|
|
1749
|
+
var API3 = "https://api.hubapi.com";
|
|
1088
1750
|
function hubspot(opts) {
|
|
1089
1751
|
const { clientId, clientSecret } = opts;
|
|
1090
1752
|
const adapter = {
|
|
@@ -1094,8 +1756,8 @@ function hubspot(opts) {
|
|
|
1094
1756
|
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
1757
|
auth: {
|
|
1096
1758
|
kind: "oauth2",
|
|
1097
|
-
authorizationUrl:
|
|
1098
|
-
tokenUrl:
|
|
1759
|
+
authorizationUrl: AUTH_URL6,
|
|
1760
|
+
tokenUrl: TOKEN_URL6,
|
|
1099
1761
|
scopes: SCOPES4,
|
|
1100
1762
|
clientIdEnv: "HUBSPOT_OAUTH_CLIENT_ID",
|
|
1101
1763
|
clientSecretEnv: "HUBSPOT_OAUTH_CLIENT_SECRET"
|
|
@@ -1154,8 +1816,8 @@ function hubspot(opts) {
|
|
|
1154
1816
|
throw new Error(`hubspot: unknown read capability ${inv.capabilityName}`);
|
|
1155
1817
|
}
|
|
1156
1818
|
const { email } = inv.args;
|
|
1157
|
-
const accessToken = await
|
|
1158
|
-
const res = await fetch(`${
|
|
1819
|
+
const accessToken = await ensureFreshAccessToken6(inv.source.credentials, clientId, clientSecret);
|
|
1820
|
+
const res = await fetch(`${API3}/crm/v3/objects/contacts/search`, {
|
|
1159
1821
|
method: "POST",
|
|
1160
1822
|
headers: {
|
|
1161
1823
|
authorization: `Bearer ${accessToken}`,
|
|
@@ -1187,7 +1849,7 @@ function hubspot(opts) {
|
|
|
1187
1849
|
};
|
|
1188
1850
|
},
|
|
1189
1851
|
async executeMutation(inv) {
|
|
1190
|
-
const accessToken = await
|
|
1852
|
+
const accessToken = await ensureFreshAccessToken6(inv.source.credentials, clientId, clientSecret);
|
|
1191
1853
|
if (inv.capabilityName === "upsert_contact") {
|
|
1192
1854
|
return upsertContact(inv, accessToken);
|
|
1193
1855
|
}
|
|
@@ -1201,7 +1863,7 @@ function hubspot(opts) {
|
|
|
1201
1863
|
throw new Error("HubSpot OAuth client not configured (HUBSPOT_OAUTH_CLIENT_ID / _SECRET)");
|
|
1202
1864
|
}
|
|
1203
1865
|
const tokens = await exchangeAuthorizationCode({
|
|
1204
|
-
tokenUrl:
|
|
1866
|
+
tokenUrl: TOKEN_URL6,
|
|
1205
1867
|
clientId,
|
|
1206
1868
|
clientSecret,
|
|
1207
1869
|
code: input.code,
|
|
@@ -1224,7 +1886,7 @@ function hubspot(opts) {
|
|
|
1224
1886
|
throw new Error("hubspot.refreshToken: missing refresh token");
|
|
1225
1887
|
}
|
|
1226
1888
|
const refreshed = await refreshAccessToken({
|
|
1227
|
-
tokenUrl:
|
|
1889
|
+
tokenUrl: TOKEN_URL6,
|
|
1228
1890
|
clientId,
|
|
1229
1891
|
clientSecret,
|
|
1230
1892
|
refreshToken: creds.refreshToken
|
|
@@ -1238,8 +1900,8 @@ function hubspot(opts) {
|
|
|
1238
1900
|
},
|
|
1239
1901
|
async test(source) {
|
|
1240
1902
|
try {
|
|
1241
|
-
const accessToken = await
|
|
1242
|
-
const res = await fetch(`${
|
|
1903
|
+
const accessToken = await ensureFreshAccessToken6(source.credentials, clientId, clientSecret);
|
|
1904
|
+
const res = await fetch(`${API3}/oauth/v1/access-tokens/${encodeURIComponent(accessToken)}`, {
|
|
1243
1905
|
signal: AbortSignal.timeout(8e3)
|
|
1244
1906
|
});
|
|
1245
1907
|
if (res.status === 401 || res.status === 403 || res.status === 404) {
|
|
@@ -1257,7 +1919,7 @@ function hubspot(opts) {
|
|
|
1257
1919
|
async function upsertContact(inv, accessToken) {
|
|
1258
1920
|
const { email, properties } = inv.args;
|
|
1259
1921
|
const idemKey = sanitizeIdempotencyKey(inv.idempotencyKey);
|
|
1260
|
-
const url = `${
|
|
1922
|
+
const url = `${API3}/crm/v3/objects/contacts/batch/upsert?idempotencyKey=${encodeURIComponent(idemKey)}`;
|
|
1261
1923
|
const body = {
|
|
1262
1924
|
inputs: [
|
|
1263
1925
|
{
|
|
@@ -1303,7 +1965,7 @@ async function upsertContact(inv, accessToken) {
|
|
|
1303
1965
|
async function createNote(inv, accessToken) {
|
|
1304
1966
|
const { contactId, body } = inv.args;
|
|
1305
1967
|
const idemKey = sanitizeIdempotencyKey(inv.idempotencyKey);
|
|
1306
|
-
const url = `${
|
|
1968
|
+
const url = `${API3}/crm/v3/objects/notes/batch/create?idempotencyKey=${encodeURIComponent(idemKey)}`;
|
|
1307
1969
|
const payload = {
|
|
1308
1970
|
inputs: [
|
|
1309
1971
|
{
|
|
@@ -1356,7 +2018,7 @@ async function createNote(inv, accessToken) {
|
|
|
1356
2018
|
function sanitizeIdempotencyKey(k) {
|
|
1357
2019
|
return k.replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
1358
2020
|
}
|
|
1359
|
-
async function
|
|
2021
|
+
async function ensureFreshAccessToken6(creds, clientId, clientSecret) {
|
|
1360
2022
|
if (creds.kind !== "oauth2") {
|
|
1361
2023
|
throw new Error("hubspot: expected oauth2 credentials");
|
|
1362
2024
|
}
|
|
@@ -1367,7 +2029,7 @@ async function ensureFreshAccessToken4(creds, clientId, clientSecret) {
|
|
|
1367
2029
|
throw new CredentialsExpired("HubSpot access token expired and no refresh token", "");
|
|
1368
2030
|
}
|
|
1369
2031
|
const refreshed = await refreshAccessToken({
|
|
1370
|
-
tokenUrl:
|
|
2032
|
+
tokenUrl: TOKEN_URL6,
|
|
1371
2033
|
clientId,
|
|
1372
2034
|
clientSecret,
|
|
1373
2035
|
refreshToken: creds.refreshToken
|
|
@@ -1380,9 +2042,9 @@ async function ensureFreshAccessToken4(creds, clientId, clientSecret) {
|
|
|
1380
2042
|
|
|
1381
2043
|
// src/connectors/adapters/slack.ts
|
|
1382
2044
|
var SCOPES5 = ["chat:write", "users:read", "users:read.email", "channels:read"];
|
|
1383
|
-
var
|
|
1384
|
-
var
|
|
1385
|
-
var
|
|
2045
|
+
var AUTH_URL7 = "https://slack.com/oauth/v2/authorize";
|
|
2046
|
+
var TOKEN_URL7 = "https://slack.com/api/oauth.v2.access";
|
|
2047
|
+
var API4 = "https://slack.com/api";
|
|
1386
2048
|
function slack(opts) {
|
|
1387
2049
|
const { clientId, clientSecret } = opts;
|
|
1388
2050
|
const adapter = {
|
|
@@ -1398,8 +2060,8 @@ function slack(opts) {
|
|
|
1398
2060
|
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
2061
|
auth: {
|
|
1400
2062
|
kind: "oauth2",
|
|
1401
|
-
authorizationUrl:
|
|
1402
|
-
tokenUrl:
|
|
2063
|
+
authorizationUrl: AUTH_URL7,
|
|
2064
|
+
tokenUrl: TOKEN_URL7,
|
|
1403
2065
|
scopes: SCOPES5,
|
|
1404
2066
|
clientIdEnv: "SLACK_OAUTH_CLIENT_ID",
|
|
1405
2067
|
clientSecretEnv: "SLACK_OAUTH_CLIENT_SECRET"
|
|
@@ -1451,7 +2113,7 @@ function slack(opts) {
|
|
|
1451
2113
|
const accessToken = readBotToken(inv.source.credentials);
|
|
1452
2114
|
if (inv.capabilityName === "lookup_user") {
|
|
1453
2115
|
const { email } = inv.args;
|
|
1454
|
-
const url = `${
|
|
2116
|
+
const url = `${API4}/users.lookupByEmail?email=${encodeURIComponent(email)}`;
|
|
1455
2117
|
const json = await slackGet(url, accessToken, inv.source.id);
|
|
1456
2118
|
if (!json.ok) {
|
|
1457
2119
|
if (json.error === "users_not_found") {
|
|
@@ -1471,7 +2133,7 @@ function slack(opts) {
|
|
|
1471
2133
|
limit: String(Math.min(Math.max(1, limit ?? 200), 1e3)),
|
|
1472
2134
|
types: types ?? "public_channel,private_channel"
|
|
1473
2135
|
});
|
|
1474
|
-
const json = await slackGet(`${
|
|
2136
|
+
const json = await slackGet(`${API4}/conversations.list?${params.toString()}`, accessToken, inv.source.id);
|
|
1475
2137
|
if (!json.ok) {
|
|
1476
2138
|
throw new Error(`slack list_channels: ${json.error ?? "unknown"}`);
|
|
1477
2139
|
}
|
|
@@ -1489,7 +2151,7 @@ function slack(opts) {
|
|
|
1489
2151
|
}
|
|
1490
2152
|
const accessToken = readBotToken(inv.source.credentials);
|
|
1491
2153
|
const { channel, text, blocks } = inv.args;
|
|
1492
|
-
const res = await fetch(`${
|
|
2154
|
+
const res = await fetch(`${API4}/chat.postMessage`, {
|
|
1493
2155
|
method: "POST",
|
|
1494
2156
|
headers: {
|
|
1495
2157
|
authorization: `Bearer ${accessToken}`,
|
|
@@ -1524,7 +2186,7 @@ function slack(opts) {
|
|
|
1524
2186
|
throw new Error("Slack OAuth client not configured (SLACK_OAUTH_CLIENT_ID / _SECRET)");
|
|
1525
2187
|
}
|
|
1526
2188
|
const tokens = await exchangeAuthorizationCode({
|
|
1527
|
-
tokenUrl:
|
|
2189
|
+
tokenUrl: TOKEN_URL7,
|
|
1528
2190
|
clientId,
|
|
1529
2191
|
clientSecret,
|
|
1530
2192
|
code: input.code,
|
|
@@ -1547,7 +2209,7 @@ function slack(opts) {
|
|
|
1547
2209
|
return creds;
|
|
1548
2210
|
}
|
|
1549
2211
|
const refreshed = await refreshAccessToken({
|
|
1550
|
-
tokenUrl:
|
|
2212
|
+
tokenUrl: TOKEN_URL7,
|
|
1551
2213
|
clientId,
|
|
1552
2214
|
clientSecret,
|
|
1553
2215
|
refreshToken: creds.refreshToken
|
|
@@ -1562,7 +2224,7 @@ function slack(opts) {
|
|
|
1562
2224
|
async test(source) {
|
|
1563
2225
|
try {
|
|
1564
2226
|
const accessToken = readBotToken(source.credentials);
|
|
1565
|
-
const res = await fetch(`${
|
|
2227
|
+
const res = await fetch(`${API4}/auth.test`, {
|
|
1566
2228
|
method: "POST",
|
|
1567
2229
|
headers: { authorization: `Bearer ${accessToken}` },
|
|
1568
2230
|
signal: AbortSignal.timeout(8e3)
|
|
@@ -1602,9 +2264,9 @@ async function slackGet(url, accessToken, dataSourceId) {
|
|
|
1602
2264
|
}
|
|
1603
2265
|
|
|
1604
2266
|
// src/connectors/adapters/notion-database.ts
|
|
1605
|
-
var
|
|
1606
|
-
var
|
|
1607
|
-
var
|
|
2267
|
+
var AUTH_URL8 = "https://api.notion.com/v1/oauth/authorize";
|
|
2268
|
+
var TOKEN_URL8 = "https://api.notion.com/v1/oauth/token";
|
|
2269
|
+
var API5 = "https://api.notion.com/v1";
|
|
1608
2270
|
var NOTION_VERSION = "2022-06-28";
|
|
1609
2271
|
function notionDatabase(opts) {
|
|
1610
2272
|
const { clientId, clientSecret } = opts;
|
|
@@ -1615,8 +2277,8 @@ function notionDatabase(opts) {
|
|
|
1615
2277
|
description: "Query a Notion database, create new pages, and update existing ones with optimistic concurrency via last_edited_time.",
|
|
1616
2278
|
auth: {
|
|
1617
2279
|
kind: "oauth2",
|
|
1618
|
-
authorizationUrl:
|
|
1619
|
-
tokenUrl:
|
|
2280
|
+
authorizationUrl: AUTH_URL8,
|
|
2281
|
+
tokenUrl: TOKEN_URL8,
|
|
1620
2282
|
// Notion does not use OAuth scopes — the workspace owner picks
|
|
1621
2283
|
// which pages/databases the integration sees during install. We
|
|
1622
2284
|
// declare an empty scope list so the consent screen renders cleanly.
|
|
@@ -1691,7 +2353,7 @@ function notionDatabase(opts) {
|
|
|
1691
2353
|
};
|
|
1692
2354
|
if (filter) body.filter = filter;
|
|
1693
2355
|
if (startCursor) body.start_cursor = startCursor;
|
|
1694
|
-
const res = await fetch(`${
|
|
2356
|
+
const res = await fetch(`${API5}/databases/${encodeURIComponent(databaseId)}/query`, {
|
|
1695
2357
|
method: "POST",
|
|
1696
2358
|
headers: notionHeaders(accessToken),
|
|
1697
2359
|
body: JSON.stringify(body),
|
|
@@ -1730,7 +2392,7 @@ function notionDatabase(opts) {
|
|
|
1730
2392
|
redirect_uri: input.redirectUri,
|
|
1731
2393
|
code_verifier: input.codeVerifier
|
|
1732
2394
|
});
|
|
1733
|
-
const res = await fetch(
|
|
2395
|
+
const res = await fetch(TOKEN_URL8, {
|
|
1734
2396
|
method: "POST",
|
|
1735
2397
|
headers: {
|
|
1736
2398
|
authorization: `Basic ${Buffer.from(`${clientId}:${clientSecret}`).toString("base64")}`,
|
|
@@ -1769,7 +2431,7 @@ function notionDatabase(opts) {
|
|
|
1769
2431
|
throw new Error("notion-database.refreshToken: missing refresh token");
|
|
1770
2432
|
}
|
|
1771
2433
|
const refreshed = await refreshAccessToken({
|
|
1772
|
-
tokenUrl:
|
|
2434
|
+
tokenUrl: TOKEN_URL8,
|
|
1773
2435
|
clientId,
|
|
1774
2436
|
clientSecret,
|
|
1775
2437
|
refreshToken: creds.refreshToken
|
|
@@ -1784,7 +2446,7 @@ function notionDatabase(opts) {
|
|
|
1784
2446
|
async test(source) {
|
|
1785
2447
|
try {
|
|
1786
2448
|
const accessToken = readToken(source.credentials);
|
|
1787
|
-
const res = await fetch(`${
|
|
2449
|
+
const res = await fetch(`${API5}/users/me`, {
|
|
1788
2450
|
headers: notionHeaders(accessToken),
|
|
1789
2451
|
signal: AbortSignal.timeout(8e3)
|
|
1790
2452
|
});
|
|
@@ -1801,7 +2463,7 @@ function notionDatabase(opts) {
|
|
|
1801
2463
|
async function createPage(inv, accessToken) {
|
|
1802
2464
|
const databaseId = readMetaString3(inv.source.metadata, "databaseId");
|
|
1803
2465
|
const { properties } = inv.args;
|
|
1804
|
-
const res = await fetch(`${
|
|
2466
|
+
const res = await fetch(`${API5}/pages`, {
|
|
1805
2467
|
method: "POST",
|
|
1806
2468
|
headers: {
|
|
1807
2469
|
...notionHeaders(accessToken),
|
|
@@ -1835,7 +2497,7 @@ async function createPage(inv, accessToken) {
|
|
|
1835
2497
|
async function updatePage(inv, accessToken) {
|
|
1836
2498
|
const { pageId, properties, expectedLastEditedTime } = inv.args;
|
|
1837
2499
|
if (expectedLastEditedTime) {
|
|
1838
|
-
const headRes = await fetch(`${
|
|
2500
|
+
const headRes = await fetch(`${API5}/pages/${encodeURIComponent(pageId)}`, {
|
|
1839
2501
|
headers: notionHeaders(accessToken),
|
|
1840
2502
|
signal: AbortSignal.timeout(1e4)
|
|
1841
2503
|
});
|
|
@@ -1855,7 +2517,7 @@ async function updatePage(inv, accessToken) {
|
|
|
1855
2517
|
);
|
|
1856
2518
|
}
|
|
1857
2519
|
}
|
|
1858
|
-
const res = await fetch(`${
|
|
2520
|
+
const res = await fetch(`${API5}/pages/${encodeURIComponent(pageId)}`, {
|
|
1859
2521
|
method: "PATCH",
|
|
1860
2522
|
headers: notionHeaders(accessToken),
|
|
1861
2523
|
body: JSON.stringify({ properties }),
|
|
@@ -1901,6 +2563,304 @@ function readMetaString3(meta, key) {
|
|
|
1901
2563
|
return v;
|
|
1902
2564
|
}
|
|
1903
2565
|
|
|
2566
|
+
// src/connectors/adapters/docuseal.ts
|
|
2567
|
+
import { createHmac, timingSafeEqual } from "crypto";
|
|
2568
|
+
var DEFAULT_BASE = "https://api.docuseal.com";
|
|
2569
|
+
function readDocuSealCredentials(creds) {
|
|
2570
|
+
if (creds.kind === "api-key" && typeof creds.apiKey === "string" && creds.apiKey.length > 0) {
|
|
2571
|
+
return { apiKey: creds.apiKey };
|
|
2572
|
+
}
|
|
2573
|
+
if (creds.kind === "custom" && creds.values && typeof creds.values.apiKey === "string" && creds.values.apiKey.length > 0) {
|
|
2574
|
+
const webhookSecret = typeof creds.values.webhookSecret === "string" ? creds.values.webhookSecret : void 0;
|
|
2575
|
+
return { apiKey: creds.values.apiKey, webhookSecret };
|
|
2576
|
+
}
|
|
2577
|
+
throw new Error("docuseal: expected api-key credentials (apiKey + optional webhookSecret)");
|
|
2578
|
+
}
|
|
2579
|
+
function docuseal(opts = {}) {
|
|
2580
|
+
const baseUrl = (opts.baseUrl ?? DEFAULT_BASE).replace(/\/$/, "");
|
|
2581
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
2582
|
+
const adapter = {
|
|
2583
|
+
manifest: {
|
|
2584
|
+
kind: "docuseal",
|
|
2585
|
+
displayName: "DocuSeal",
|
|
2586
|
+
description: "Send documents for e-signature via DocuSeal, poll submission status, void in-flight submissions, and react to push events when submitters sign.",
|
|
2587
|
+
auth: {
|
|
2588
|
+
kind: "api-key",
|
|
2589
|
+
hint: "Paste a DocuSeal personal API key (settings \u2192 API). Optional webhook secret enables push-driven workflows."
|
|
2590
|
+
},
|
|
2591
|
+
category: "doc",
|
|
2592
|
+
defaultConsistencyModel: "authoritative",
|
|
2593
|
+
capabilities: [
|
|
2594
|
+
{
|
|
2595
|
+
name: "get_submission",
|
|
2596
|
+
class: "read",
|
|
2597
|
+
description: "Fetch the current status of a DocuSeal submission and each submitter.",
|
|
2598
|
+
parameters: {
|
|
2599
|
+
type: "object",
|
|
2600
|
+
properties: { submissionId: { type: "string" } },
|
|
2601
|
+
required: ["submissionId"]
|
|
2602
|
+
}
|
|
2603
|
+
},
|
|
2604
|
+
{
|
|
2605
|
+
name: "create_submission",
|
|
2606
|
+
class: "mutation",
|
|
2607
|
+
description: "Send a template for signature to one or more submitters. external_id is the idempotency key; retries return the original submission.",
|
|
2608
|
+
cas: "native-idempotency",
|
|
2609
|
+
externalEffect: true,
|
|
2610
|
+
parameters: {
|
|
2611
|
+
type: "object",
|
|
2612
|
+
properties: {
|
|
2613
|
+
templateId: { type: "string" },
|
|
2614
|
+
submitters: {
|
|
2615
|
+
type: "array",
|
|
2616
|
+
items: {
|
|
2617
|
+
type: "object",
|
|
2618
|
+
properties: {
|
|
2619
|
+
email: { type: "string" },
|
|
2620
|
+
name: { type: "string" },
|
|
2621
|
+
role: { type: "string" },
|
|
2622
|
+
values: { type: "object", additionalProperties: true }
|
|
2623
|
+
},
|
|
2624
|
+
required: ["email"]
|
|
2625
|
+
}
|
|
2626
|
+
},
|
|
2627
|
+
sendEmail: { type: "boolean", default: true },
|
|
2628
|
+
message: { type: "string" }
|
|
2629
|
+
},
|
|
2630
|
+
required: ["templateId", "submitters"]
|
|
2631
|
+
}
|
|
2632
|
+
},
|
|
2633
|
+
{
|
|
2634
|
+
name: "void_submission",
|
|
2635
|
+
class: "mutation",
|
|
2636
|
+
description: "Cancel an in-flight submission. CAS via updated_at \u2014 concurrent voids return ResourceContention.",
|
|
2637
|
+
cas: "etag-if-match",
|
|
2638
|
+
externalEffect: true,
|
|
2639
|
+
parameters: {
|
|
2640
|
+
type: "object",
|
|
2641
|
+
properties: {
|
|
2642
|
+
submissionId: { type: "string" },
|
|
2643
|
+
reason: { type: "string" }
|
|
2644
|
+
},
|
|
2645
|
+
required: ["submissionId"]
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
]
|
|
2649
|
+
},
|
|
2650
|
+
async executeRead(inv) {
|
|
2651
|
+
if (inv.capabilityName !== "get_submission") {
|
|
2652
|
+
throw new Error(`docuseal: unknown read capability ${inv.capabilityName}`);
|
|
2653
|
+
}
|
|
2654
|
+
const { apiKey } = readDocuSealCredentials(inv.source.credentials);
|
|
2655
|
+
const { submissionId } = inv.args;
|
|
2656
|
+
const res = await fetch(`${baseUrl}/submissions/${encodeURIComponent(submissionId)}`, {
|
|
2657
|
+
headers: { "X-Auth-Token": apiKey, accept: "application/json" },
|
|
2658
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2659
|
+
});
|
|
2660
|
+
if (res.status === 401) throw new CredentialsExpired("DocuSeal rejected API key (401)", inv.source.id);
|
|
2661
|
+
if (res.status === 404) {
|
|
2662
|
+
throw new Error(`docuseal get_submission: submission ${submissionId} not found`);
|
|
2663
|
+
}
|
|
2664
|
+
if (!res.ok) {
|
|
2665
|
+
const text = await res.text().catch(() => "");
|
|
2666
|
+
throw new Error(`docuseal get_submission ${res.status}: ${text.slice(0, 200)}`);
|
|
2667
|
+
}
|
|
2668
|
+
const json = await res.json();
|
|
2669
|
+
return {
|
|
2670
|
+
data: normalizeSubmission(json),
|
|
2671
|
+
etag: json.updated_at,
|
|
2672
|
+
fetchedAt: Date.now()
|
|
2673
|
+
};
|
|
2674
|
+
},
|
|
2675
|
+
async executeMutation(inv) {
|
|
2676
|
+
const { apiKey } = readDocuSealCredentials(inv.source.credentials);
|
|
2677
|
+
if (inv.capabilityName === "create_submission") return createSubmission(inv, apiKey, baseUrl, timeoutMs);
|
|
2678
|
+
if (inv.capabilityName === "void_submission") return voidSubmission(inv, apiKey, baseUrl, timeoutMs);
|
|
2679
|
+
throw new Error(`docuseal: unknown mutation capability ${inv.capabilityName}`);
|
|
2680
|
+
},
|
|
2681
|
+
verifySignature({ rawBody, headers, source }) {
|
|
2682
|
+
const creds = (() => {
|
|
2683
|
+
try {
|
|
2684
|
+
return readDocuSealCredentials(source.credentials);
|
|
2685
|
+
} catch {
|
|
2686
|
+
return null;
|
|
2687
|
+
}
|
|
2688
|
+
})();
|
|
2689
|
+
if (!creds?.webhookSecret) return { valid: false, reason: "missing_webhook_secret" };
|
|
2690
|
+
const sig = firstHeader(headers, "x-docuseal-signature");
|
|
2691
|
+
if (!sig) return { valid: false, reason: "missing_signature_header" };
|
|
2692
|
+
const expected = createHmac("sha256", creds.webhookSecret).update(rawBody).digest("hex");
|
|
2693
|
+
const a = Buffer.from(sig.toLowerCase(), "utf-8");
|
|
2694
|
+
const b = Buffer.from(expected, "utf-8");
|
|
2695
|
+
if (a.length !== b.length) return { valid: false, reason: "invalid_signature" };
|
|
2696
|
+
return timingSafeEqual(a, b) ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
2697
|
+
},
|
|
2698
|
+
async handleInboundEvent({ rawBody }) {
|
|
2699
|
+
let parsed;
|
|
2700
|
+
try {
|
|
2701
|
+
parsed = JSON.parse(rawBody);
|
|
2702
|
+
} catch {
|
|
2703
|
+
return { events: [], response: { status: 400, body: { error: "invalid_json" } } };
|
|
2704
|
+
}
|
|
2705
|
+
if (!parsed || typeof parsed !== "object") {
|
|
2706
|
+
return { events: [], response: { status: 400, body: { error: "invalid_payload" } } };
|
|
2707
|
+
}
|
|
2708
|
+
const evt = parsed;
|
|
2709
|
+
const eventType = typeof evt.event_type === "string" ? `docuseal.${evt.event_type}` : "docuseal.unknown";
|
|
2710
|
+
const providerEventId = typeof evt.event_id === "string" ? evt.event_id : void 0;
|
|
2711
|
+
const events = [
|
|
2712
|
+
{
|
|
2713
|
+
eventType,
|
|
2714
|
+
providerEventId,
|
|
2715
|
+
payload: evt
|
|
2716
|
+
}
|
|
2717
|
+
];
|
|
2718
|
+
return { events };
|
|
2719
|
+
},
|
|
2720
|
+
async test(source) {
|
|
2721
|
+
try {
|
|
2722
|
+
const { apiKey } = readDocuSealCredentials(source.credentials);
|
|
2723
|
+
const res = await fetch(`${baseUrl}/templates?limit=1`, {
|
|
2724
|
+
headers: { "X-Auth-Token": apiKey },
|
|
2725
|
+
signal: AbortSignal.timeout(8e3)
|
|
2726
|
+
});
|
|
2727
|
+
if (res.status === 401) return { ok: false, reason: "DocuSeal rejected API key (401) \u2014 reconnect required" };
|
|
2728
|
+
if (!res.ok) return { ok: false, reason: `DocuSeal returned ${res.status}` };
|
|
2729
|
+
return { ok: true };
|
|
2730
|
+
} catch (err) {
|
|
2731
|
+
return { ok: false, reason: err instanceof Error ? err.message : String(err) };
|
|
2732
|
+
}
|
|
2733
|
+
}
|
|
2734
|
+
};
|
|
2735
|
+
return adapter;
|
|
2736
|
+
}
|
|
2737
|
+
function normalizeSubmission(s) {
|
|
2738
|
+
return {
|
|
2739
|
+
submissionId: String(s.id),
|
|
2740
|
+
status: s.status ?? "pending",
|
|
2741
|
+
updatedAt: s.updated_at,
|
|
2742
|
+
createdAt: s.created_at,
|
|
2743
|
+
completedAt: s.completed_at ?? void 0,
|
|
2744
|
+
auditLogUrl: s.audit_log_url,
|
|
2745
|
+
combinedDocumentUrl: s.combined_document_url,
|
|
2746
|
+
submitters: (s.submitters ?? []).map((sub) => ({
|
|
2747
|
+
email: sub.email,
|
|
2748
|
+
name: sub.name,
|
|
2749
|
+
role: sub.role,
|
|
2750
|
+
slug: sub.slug,
|
|
2751
|
+
status: sub.status ?? "awaiting",
|
|
2752
|
+
completedAt: sub.completed_at ?? void 0,
|
|
2753
|
+
url: sub.embed_src
|
|
2754
|
+
}))
|
|
2755
|
+
};
|
|
2756
|
+
}
|
|
2757
|
+
async function createSubmission(inv, apiKey, baseUrl, timeoutMs) {
|
|
2758
|
+
const { templateId, submitters, sendEmail, message } = inv.args;
|
|
2759
|
+
const body = {
|
|
2760
|
+
template_id: templateId,
|
|
2761
|
+
external_id: inv.idempotencyKey,
|
|
2762
|
+
send_email: sendEmail ?? true,
|
|
2763
|
+
message,
|
|
2764
|
+
submitters: submitters.map((s) => ({
|
|
2765
|
+
email: s.email,
|
|
2766
|
+
name: s.name,
|
|
2767
|
+
role: s.role,
|
|
2768
|
+
values: s.values
|
|
2769
|
+
}))
|
|
2770
|
+
};
|
|
2771
|
+
const res = await fetch(`${baseUrl}/submissions`, {
|
|
2772
|
+
method: "POST",
|
|
2773
|
+
headers: {
|
|
2774
|
+
"X-Auth-Token": apiKey,
|
|
2775
|
+
"content-type": "application/json",
|
|
2776
|
+
accept: "application/json"
|
|
2777
|
+
},
|
|
2778
|
+
body: JSON.stringify(body),
|
|
2779
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2780
|
+
});
|
|
2781
|
+
if (res.status === 401) throw new CredentialsExpired("DocuSeal rejected API key (401)", inv.source.id);
|
|
2782
|
+
if (res.status === 429) {
|
|
2783
|
+
const retryAfter = Number(res.headers.get("retry-after") ?? "5");
|
|
2784
|
+
return {
|
|
2785
|
+
status: "rate-limited",
|
|
2786
|
+
retryAfterMs: Number.isFinite(retryAfter) ? retryAfter * 1e3 : 5e3,
|
|
2787
|
+
message: "DocuSeal rate-limited create_submission"
|
|
2788
|
+
};
|
|
2789
|
+
}
|
|
2790
|
+
if (res.status === 409) {
|
|
2791
|
+
const conflictJson = await res.json().catch(() => ({}));
|
|
2792
|
+
const original = conflictJson.submission ?? conflictJson;
|
|
2793
|
+
return {
|
|
2794
|
+
status: "committed",
|
|
2795
|
+
data: normalizeSubmission(original),
|
|
2796
|
+
etagAfter: original.updated_at,
|
|
2797
|
+
committedAt: Date.now(),
|
|
2798
|
+
idempotentReplay: true
|
|
2799
|
+
};
|
|
2800
|
+
}
|
|
2801
|
+
if (!res.ok) {
|
|
2802
|
+
const text = await res.text().catch(() => "");
|
|
2803
|
+
throw new Error(`docuseal create_submission ${res.status}: ${text.slice(0, 200)}`);
|
|
2804
|
+
}
|
|
2805
|
+
const created = await res.json();
|
|
2806
|
+
const sub = Array.isArray(created) ? created[0] : created;
|
|
2807
|
+
if (!sub) {
|
|
2808
|
+
throw new Error("docuseal create_submission: empty response body");
|
|
2809
|
+
}
|
|
2810
|
+
return {
|
|
2811
|
+
status: "committed",
|
|
2812
|
+
data: normalizeSubmission(sub),
|
|
2813
|
+
etagAfter: sub.updated_at,
|
|
2814
|
+
committedAt: Date.now(),
|
|
2815
|
+
idempotentReplay: false
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
async function voidSubmission(inv, apiKey, baseUrl, timeoutMs) {
|
|
2819
|
+
const { submissionId, reason } = inv.args;
|
|
2820
|
+
const headers = {
|
|
2821
|
+
"X-Auth-Token": apiKey,
|
|
2822
|
+
accept: "application/json"
|
|
2823
|
+
};
|
|
2824
|
+
if (inv.expectedEtag) headers["if-match"] = inv.expectedEtag;
|
|
2825
|
+
if (reason) headers["x-docuseal-void-reason"] = reason;
|
|
2826
|
+
const res = await fetch(`${baseUrl}/submissions/${encodeURIComponent(submissionId)}`, {
|
|
2827
|
+
method: "DELETE",
|
|
2828
|
+
headers,
|
|
2829
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
2830
|
+
});
|
|
2831
|
+
if (res.status === 401) throw new CredentialsExpired("DocuSeal rejected API key (401)", inv.source.id);
|
|
2832
|
+
if (res.status === 404) {
|
|
2833
|
+
throw new Error(`docuseal void_submission: submission ${submissionId} not found`);
|
|
2834
|
+
}
|
|
2835
|
+
if (res.status === 412) {
|
|
2836
|
+
throw new ResourceContention(`docuseal void_submission: submission ${submissionId} updated since last read`);
|
|
2837
|
+
}
|
|
2838
|
+
if (res.status === 429) {
|
|
2839
|
+
const retryAfter = Number(res.headers.get("retry-after") ?? "5");
|
|
2840
|
+
return {
|
|
2841
|
+
status: "rate-limited",
|
|
2842
|
+
retryAfterMs: Number.isFinite(retryAfter) ? retryAfter * 1e3 : 5e3,
|
|
2843
|
+
message: "DocuSeal rate-limited void_submission"
|
|
2844
|
+
};
|
|
2845
|
+
}
|
|
2846
|
+
if (!res.ok) {
|
|
2847
|
+
const text = await res.text().catch(() => "");
|
|
2848
|
+
throw new Error(`docuseal void_submission ${res.status}: ${text.slice(0, 200)}`);
|
|
2849
|
+
}
|
|
2850
|
+
const json = await res.json().catch(() => ({}));
|
|
2851
|
+
return {
|
|
2852
|
+
status: "committed",
|
|
2853
|
+
data: {
|
|
2854
|
+
submissionId,
|
|
2855
|
+
status: "voided",
|
|
2856
|
+
voidedAt: json.updated_at ?? (/* @__PURE__ */ new Date()).toISOString()
|
|
2857
|
+
},
|
|
2858
|
+
etagAfter: json.updated_at,
|
|
2859
|
+
committedAt: Date.now(),
|
|
2860
|
+
idempotentReplay: false
|
|
2861
|
+
};
|
|
2862
|
+
}
|
|
2863
|
+
|
|
1904
2864
|
// src/connectors/adapters/declarative-rest.ts
|
|
1905
2865
|
function declarativeRestConnector(spec) {
|
|
1906
2866
|
const capabilities = spec.capabilities.map(operationToCapability);
|
|
@@ -2100,7 +3060,7 @@ async function safeErrorText(res) {
|
|
|
2100
3060
|
}
|
|
2101
3061
|
|
|
2102
3062
|
// src/connectors/adapters/twilio-sms.ts
|
|
2103
|
-
var
|
|
3063
|
+
var API6 = "https://api.twilio.com/2010-04-01";
|
|
2104
3064
|
var LOOKUP_API = "https://lookups.twilio.com/v1";
|
|
2105
3065
|
var twilioSmsConnector = {
|
|
2106
3066
|
manifest: {
|
|
@@ -2192,7 +3152,7 @@ var twilioSmsConnector = {
|
|
|
2192
3152
|
params.set("PageSize", String(Math.min(Math.max(1, limit ?? 20), 100)));
|
|
2193
3153
|
if (to) params.set("To", to);
|
|
2194
3154
|
if (from) params.set("From", from);
|
|
2195
|
-
const url = `${
|
|
3155
|
+
const url = `${API6}/Accounts/${encodeURIComponent(auth.accountSid)}/Messages.json?${params.toString()}`;
|
|
2196
3156
|
const res = await fetch(url, {
|
|
2197
3157
|
headers: { authorization: basicAuth(auth) },
|
|
2198
3158
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -2218,7 +3178,7 @@ var twilioSmsConnector = {
|
|
|
2218
3178
|
const { to, body, from } = inv.args;
|
|
2219
3179
|
const fromNumber = from ?? readMetaString4(inv.source.metadata, "fromNumber");
|
|
2220
3180
|
const formBody = new URLSearchParams({ To: to, From: fromNumber, Body: body });
|
|
2221
|
-
const url = `${
|
|
3181
|
+
const url = `${API6}/Accounts/${encodeURIComponent(auth.accountSid)}/Messages.json`;
|
|
2222
3182
|
const res = await fetch(url, {
|
|
2223
3183
|
method: "POST",
|
|
2224
3184
|
headers: {
|
|
@@ -2248,7 +3208,7 @@ var twilioSmsConnector = {
|
|
|
2248
3208
|
async test(source) {
|
|
2249
3209
|
try {
|
|
2250
3210
|
const auth = parseAuth(source.credentials);
|
|
2251
|
-
const res = await fetch(`${
|
|
3211
|
+
const res = await fetch(`${API6}/Accounts/${encodeURIComponent(auth.accountSid)}.json`, {
|
|
2252
3212
|
headers: { authorization: basicAuth(auth) },
|
|
2253
3213
|
signal: AbortSignal.timeout(8e3)
|
|
2254
3214
|
});
|
|
@@ -2293,15 +3253,15 @@ function readMetaString4(meta, key) {
|
|
|
2293
3253
|
}
|
|
2294
3254
|
|
|
2295
3255
|
// src/connectors/adapters/stripe-pack.ts
|
|
2296
|
-
var
|
|
3256
|
+
var API7 = "https://api.stripe.com/v1";
|
|
2297
3257
|
var stripePackConnector = {
|
|
2298
3258
|
manifest: {
|
|
2299
3259
|
kind: "stripe-pack",
|
|
2300
|
-
displayName: "Stripe (customers, invoices, checkout)",
|
|
2301
|
-
description: "Look up Stripe customers, draft invoices,
|
|
3260
|
+
displayName: "Stripe (customers, invoices, checkout, subscriptions)",
|
|
3261
|
+
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
3262
|
auth: {
|
|
2303
3263
|
kind: "api-key",
|
|
2304
|
-
hint: "Paste a Stripe restricted key (rk_live_\u2026) with read
|
|
3264
|
+
hint: "Paste a Stripe restricted key (rk_live_\u2026) with read on customers + subscriptions and write on invoices + checkout + subscriptions + billing portal."
|
|
2305
3265
|
},
|
|
2306
3266
|
category: "commerce",
|
|
2307
3267
|
defaultConsistencyModel: "authoritative",
|
|
@@ -2316,6 +3276,20 @@ var stripePackConnector = {
|
|
|
2316
3276
|
required: ["email"]
|
|
2317
3277
|
}
|
|
2318
3278
|
},
|
|
3279
|
+
{
|
|
3280
|
+
name: "list_subscriptions",
|
|
3281
|
+
class: "read",
|
|
3282
|
+
description: "List a customer's subscriptions. Optionally filter by status ('active', 'past_due', 'canceled', 'all').",
|
|
3283
|
+
parameters: {
|
|
3284
|
+
type: "object",
|
|
3285
|
+
properties: {
|
|
3286
|
+
customerId: { type: "string" },
|
|
3287
|
+
status: { type: "string", enum: ["active", "past_due", "unpaid", "canceled", "incomplete", "trialing", "all"], default: "all" },
|
|
3288
|
+
limit: { type: "integer", minimum: 1, maximum: 100, default: 10 }
|
|
3289
|
+
},
|
|
3290
|
+
required: ["customerId"]
|
|
3291
|
+
}
|
|
3292
|
+
},
|
|
2319
3293
|
{
|
|
2320
3294
|
name: "create_invoice",
|
|
2321
3295
|
class: "mutation",
|
|
@@ -2371,16 +3345,47 @@ var stripePackConnector = {
|
|
|
2371
3345
|
},
|
|
2372
3346
|
required: ["successUrl", "cancelUrl", "lineItems"]
|
|
2373
3347
|
}
|
|
3348
|
+
},
|
|
3349
|
+
{
|
|
3350
|
+
name: "cancel_subscription",
|
|
3351
|
+
class: "mutation",
|
|
3352
|
+
description: "Cancel a subscription. Default cancels immediately; pass atPeriodEnd=true to schedule cancellation for the end of the current billing period.",
|
|
3353
|
+
cas: "native-idempotency",
|
|
3354
|
+
externalEffect: true,
|
|
3355
|
+
parameters: {
|
|
3356
|
+
type: "object",
|
|
3357
|
+
properties: {
|
|
3358
|
+
subscriptionId: { type: "string" },
|
|
3359
|
+
atPeriodEnd: { type: "boolean", default: false }
|
|
3360
|
+
},
|
|
3361
|
+
required: ["subscriptionId"]
|
|
3362
|
+
}
|
|
3363
|
+
},
|
|
3364
|
+
{
|
|
3365
|
+
name: "create_billing_portal_session",
|
|
3366
|
+
class: "mutation",
|
|
3367
|
+
description: "Create a Stripe billing portal session and return its URL. Hand-off for the customer to self-serve cancel/upgrade/update-card.",
|
|
3368
|
+
cas: "native-idempotency",
|
|
3369
|
+
externalEffect: true,
|
|
3370
|
+
parameters: {
|
|
3371
|
+
type: "object",
|
|
3372
|
+
properties: {
|
|
3373
|
+
customerId: { type: "string" },
|
|
3374
|
+
returnUrl: { type: "string" }
|
|
3375
|
+
},
|
|
3376
|
+
required: ["customerId", "returnUrl"]
|
|
3377
|
+
}
|
|
2374
3378
|
}
|
|
2375
3379
|
]
|
|
2376
3380
|
},
|
|
2377
3381
|
async executeRead(inv) {
|
|
3382
|
+
const apiKey = readApiKey(inv.source.credentials);
|
|
3383
|
+
if (inv.capabilityName === "list_subscriptions") return listSubscriptions(inv, apiKey);
|
|
2378
3384
|
if (inv.capabilityName !== "find_customer") {
|
|
2379
3385
|
throw new Error(`stripe-pack: unknown read capability ${inv.capabilityName}`);
|
|
2380
3386
|
}
|
|
2381
|
-
const apiKey = readApiKey(inv.source.credentials);
|
|
2382
3387
|
const { email } = inv.args;
|
|
2383
|
-
const url = `${
|
|
3388
|
+
const url = `${API7}/customers/search?query=${encodeURIComponent(`email:'${email.toLowerCase()}'`)}&limit=1`;
|
|
2384
3389
|
const res = await fetch(url, {
|
|
2385
3390
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
2386
3391
|
signal: AbortSignal.timeout(1e4)
|
|
@@ -2403,12 +3408,14 @@ var stripePackConnector = {
|
|
|
2403
3408
|
const apiKey = readApiKey(inv.source.credentials);
|
|
2404
3409
|
if (inv.capabilityName === "create_invoice") return createInvoice(inv, apiKey);
|
|
2405
3410
|
if (inv.capabilityName === "create_checkout_session") return createCheckoutSession(inv, apiKey);
|
|
3411
|
+
if (inv.capabilityName === "cancel_subscription") return cancelSubscription(inv, apiKey);
|
|
3412
|
+
if (inv.capabilityName === "create_billing_portal_session") return createBillingPortalSession(inv, apiKey);
|
|
2406
3413
|
throw new Error(`stripe-pack: unknown mutation capability ${inv.capabilityName}`);
|
|
2407
3414
|
},
|
|
2408
3415
|
async test(source) {
|
|
2409
3416
|
try {
|
|
2410
3417
|
const apiKey = readApiKey(source.credentials);
|
|
2411
|
-
const res = await fetch(`${
|
|
3418
|
+
const res = await fetch(`${API7}/account`, {
|
|
2412
3419
|
headers: { authorization: `Bearer ${apiKey}` },
|
|
2413
3420
|
signal: AbortSignal.timeout(8e3)
|
|
2414
3421
|
});
|
|
@@ -2434,7 +3441,7 @@ async function createInvoice(inv, apiKey) {
|
|
|
2434
3441
|
quantity: String(it.quantity ?? 1)
|
|
2435
3442
|
});
|
|
2436
3443
|
if (it.description) body.set("description", it.description);
|
|
2437
|
-
const res = await fetch(`${
|
|
3444
|
+
const res = await fetch(`${API7}/invoiceitems`, {
|
|
2438
3445
|
method: "POST",
|
|
2439
3446
|
headers: {
|
|
2440
3447
|
authorization: `Bearer ${apiKey}`,
|
|
@@ -2459,7 +3466,7 @@ async function createInvoice(inv, apiKey) {
|
|
|
2459
3466
|
collection_method: "send_invoice",
|
|
2460
3467
|
days_until_due: "14"
|
|
2461
3468
|
});
|
|
2462
|
-
const invRes = await fetch(`${
|
|
3469
|
+
const invRes = await fetch(`${API7}/invoices`, {
|
|
2463
3470
|
method: "POST",
|
|
2464
3471
|
headers: {
|
|
2465
3472
|
authorization: `Bearer ${apiKey}`,
|
|
@@ -2497,7 +3504,7 @@ async function createCheckoutSession(inv, apiKey) {
|
|
|
2497
3504
|
body.set(`line_items[${i}][price]`, it.price);
|
|
2498
3505
|
body.set(`line_items[${i}][quantity]`, String(it.quantity ?? 1));
|
|
2499
3506
|
});
|
|
2500
|
-
const res = await fetch(`${
|
|
3507
|
+
const res = await fetch(`${API7}/checkout/sessions`, {
|
|
2501
3508
|
method: "POST",
|
|
2502
3509
|
headers: {
|
|
2503
3510
|
authorization: `Bearer ${apiKey}`,
|
|
@@ -2523,6 +3530,118 @@ async function createCheckoutSession(inv, apiKey) {
|
|
|
2523
3530
|
idempotentReplay: false
|
|
2524
3531
|
};
|
|
2525
3532
|
}
|
|
3533
|
+
async function listSubscriptions(inv, apiKey) {
|
|
3534
|
+
const { customerId, status, limit } = inv.args;
|
|
3535
|
+
const params = new URLSearchParams({ customer: customerId, limit: String(limit ?? 10) });
|
|
3536
|
+
if (status && status !== "all") params.set("status", status);
|
|
3537
|
+
else params.set("status", "all");
|
|
3538
|
+
const res = await fetch(`${API7}/subscriptions?${params.toString()}`, {
|
|
3539
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
3540
|
+
signal: AbortSignal.timeout(1e4)
|
|
3541
|
+
});
|
|
3542
|
+
if (res.status === 401) {
|
|
3543
|
+
throw new CredentialsExpired("Stripe rejected API key (401)", inv.source.id);
|
|
3544
|
+
}
|
|
3545
|
+
if (!res.ok) {
|
|
3546
|
+
const text = await res.text().catch(() => "");
|
|
3547
|
+
throw new Error(`stripe-pack list_subscriptions ${res.status}: ${text.slice(0, 200)}`);
|
|
3548
|
+
}
|
|
3549
|
+
const json = await res.json();
|
|
3550
|
+
const subscriptions = (json.data ?? []).map((sub) => ({
|
|
3551
|
+
id: sub.id,
|
|
3552
|
+
status: sub.status,
|
|
3553
|
+
currentPeriodEnd: sub.current_period_end,
|
|
3554
|
+
cancelAtPeriodEnd: sub.cancel_at_period_end ?? false,
|
|
3555
|
+
items: (sub.items?.data ?? []).map((it) => ({
|
|
3556
|
+
priceId: it.price?.id,
|
|
3557
|
+
productId: it.price?.product,
|
|
3558
|
+
unitAmount: it.price?.unit_amount,
|
|
3559
|
+
currency: it.price?.currency,
|
|
3560
|
+
interval: it.price?.recurring?.interval
|
|
3561
|
+
}))
|
|
3562
|
+
}));
|
|
3563
|
+
return {
|
|
3564
|
+
data: { subscriptions },
|
|
3565
|
+
fetchedAt: Date.now()
|
|
3566
|
+
};
|
|
3567
|
+
}
|
|
3568
|
+
async function cancelSubscription(inv, apiKey) {
|
|
3569
|
+
const { subscriptionId, atPeriodEnd } = inv.args;
|
|
3570
|
+
let res;
|
|
3571
|
+
if (atPeriodEnd) {
|
|
3572
|
+
const body = new URLSearchParams({ cancel_at_period_end: "true" });
|
|
3573
|
+
res = await fetch(`${API7}/subscriptions/${encodeURIComponent(subscriptionId)}`, {
|
|
3574
|
+
method: "POST",
|
|
3575
|
+
headers: {
|
|
3576
|
+
authorization: `Bearer ${apiKey}`,
|
|
3577
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
3578
|
+
"idempotency-key": inv.idempotencyKey
|
|
3579
|
+
},
|
|
3580
|
+
body,
|
|
3581
|
+
signal: AbortSignal.timeout(15e3)
|
|
3582
|
+
});
|
|
3583
|
+
} else {
|
|
3584
|
+
res = await fetch(`${API7}/subscriptions/${encodeURIComponent(subscriptionId)}`, {
|
|
3585
|
+
method: "DELETE",
|
|
3586
|
+
headers: {
|
|
3587
|
+
authorization: `Bearer ${apiKey}`,
|
|
3588
|
+
"idempotency-key": inv.idempotencyKey
|
|
3589
|
+
},
|
|
3590
|
+
signal: AbortSignal.timeout(15e3)
|
|
3591
|
+
});
|
|
3592
|
+
}
|
|
3593
|
+
if (res.status === 401) throw new CredentialsExpired("Stripe rejected API key (401)", inv.source.id);
|
|
3594
|
+
if (res.status === 404) throw new Error(`stripe-pack cancel_subscription: subscription ${subscriptionId} not found`);
|
|
3595
|
+
if (res.status === 409) {
|
|
3596
|
+
throw new ResourceContention("Stripe subscription conflict \u2014 retry rejected by idempotency check");
|
|
3597
|
+
}
|
|
3598
|
+
if (!res.ok) {
|
|
3599
|
+
const text = await res.text().catch(() => "");
|
|
3600
|
+
throw new Error(`stripe-pack cancel_subscription ${res.status}: ${text.slice(0, 200)}`);
|
|
3601
|
+
}
|
|
3602
|
+
const updated = await res.json();
|
|
3603
|
+
return {
|
|
3604
|
+
status: "committed",
|
|
3605
|
+
data: {
|
|
3606
|
+
id: updated.id,
|
|
3607
|
+
status: updated.status,
|
|
3608
|
+
cancelAtPeriodEnd: updated.cancel_at_period_end ?? false,
|
|
3609
|
+
canceledAt: updated.canceled_at,
|
|
3610
|
+
currentPeriodEnd: updated.current_period_end
|
|
3611
|
+
},
|
|
3612
|
+
committedAt: Date.now(),
|
|
3613
|
+
idempotentReplay: false
|
|
3614
|
+
};
|
|
3615
|
+
}
|
|
3616
|
+
async function createBillingPortalSession(inv, apiKey) {
|
|
3617
|
+
const { customerId, returnUrl } = inv.args;
|
|
3618
|
+
const body = new URLSearchParams({ customer: customerId, return_url: returnUrl });
|
|
3619
|
+
const res = await fetch(`${API7}/billing_portal/sessions`, {
|
|
3620
|
+
method: "POST",
|
|
3621
|
+
headers: {
|
|
3622
|
+
authorization: `Bearer ${apiKey}`,
|
|
3623
|
+
"content-type": "application/x-www-form-urlencoded",
|
|
3624
|
+
"idempotency-key": inv.idempotencyKey
|
|
3625
|
+
},
|
|
3626
|
+
body,
|
|
3627
|
+
signal: AbortSignal.timeout(15e3)
|
|
3628
|
+
});
|
|
3629
|
+
if (res.status === 401) throw new CredentialsExpired("Stripe rejected API key (401)", inv.source.id);
|
|
3630
|
+
if (res.status === 409) {
|
|
3631
|
+
throw new ResourceContention("Stripe billing portal session conflict \u2014 retry rejected by idempotency check");
|
|
3632
|
+
}
|
|
3633
|
+
if (!res.ok) {
|
|
3634
|
+
const text = await res.text().catch(() => "");
|
|
3635
|
+
throw new Error(`stripe-pack create_billing_portal_session ${res.status}: ${text.slice(0, 200)}`);
|
|
3636
|
+
}
|
|
3637
|
+
const created = await res.json();
|
|
3638
|
+
return {
|
|
3639
|
+
status: "committed",
|
|
3640
|
+
data: { sessionId: created.id, url: created.url, returnUrl: created.return_url },
|
|
3641
|
+
committedAt: Date.now(),
|
|
3642
|
+
idempotentReplay: false
|
|
3643
|
+
};
|
|
3644
|
+
}
|
|
2526
3645
|
function readApiKey(creds) {
|
|
2527
3646
|
if (creds.kind !== "api-key" || typeof creds.apiKey !== "string" || creds.apiKey.length === 0) {
|
|
2528
3647
|
throw new Error("stripe-pack: expected api-key credentials");
|
|
@@ -2531,7 +3650,7 @@ function readApiKey(creds) {
|
|
|
2531
3650
|
}
|
|
2532
3651
|
|
|
2533
3652
|
// src/connectors/adapters/webhook.ts
|
|
2534
|
-
import { createHmac } from "crypto";
|
|
3653
|
+
import { createHmac as createHmac2 } from "crypto";
|
|
2535
3654
|
var webhookConnector = {
|
|
2536
3655
|
manifest: {
|
|
2537
3656
|
kind: "webhook",
|
|
@@ -2645,96 +3764,12 @@ function signHeaders(creds, body, idempotencyKey) {
|
|
|
2645
3764
|
"x-phony-idempotency-key": idempotencyKey
|
|
2646
3765
|
};
|
|
2647
3766
|
if (creds.kind === "hmac" && typeof creds.secret === "string" && creds.secret.length > 0) {
|
|
2648
|
-
const sig =
|
|
3767
|
+
const sig = createHmac2("sha256", creds.secret).update(`${ts}.${body}`).digest("hex");
|
|
2649
3768
|
headers["x-phony-signature"] = `sha256=${sig}`;
|
|
2650
3769
|
}
|
|
2651
3770
|
return headers;
|
|
2652
3771
|
}
|
|
2653
3772
|
|
|
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
3773
|
// src/connectors/adapters/stripe-webhook-receiver.ts
|
|
2739
3774
|
var stripeWebhookReceiverConnector = {
|
|
2740
3775
|
manifest: {
|
|
@@ -3195,29 +4230,21 @@ var salesforceConnector = declarativeRestConnector({
|
|
|
3195
4230
|
});
|
|
3196
4231
|
|
|
3197
4232
|
export {
|
|
3198
|
-
ResourceContention,
|
|
3199
|
-
CredentialsExpired,
|
|
3200
|
-
validateConnectorManifest,
|
|
3201
|
-
assertValidConnectorManifest,
|
|
3202
4233
|
InMemoryOAuthFlowStore,
|
|
3203
4234
|
startOAuthFlow,
|
|
3204
4235
|
consumePendingFlow,
|
|
3205
4236
|
exchangeAuthorizationCode,
|
|
3206
4237
|
refreshAccessToken,
|
|
3207
4238
|
_resetPendingFlowsForTests,
|
|
3208
|
-
DEFAULT_SIGNATURE_TOLERANCE_SECONDS,
|
|
3209
|
-
parseStripeSignatureHeader,
|
|
3210
|
-
verifyStripeSignature,
|
|
3211
|
-
verifySlackSignature,
|
|
3212
|
-
verifyHmacSignature,
|
|
3213
|
-
verifyTwilioSignature,
|
|
3214
|
-
firstHeader,
|
|
3215
4239
|
googleCalendar,
|
|
4240
|
+
googleDrive,
|
|
3216
4241
|
googleSheets,
|
|
4242
|
+
gmail,
|
|
3217
4243
|
microsoftCalendar,
|
|
3218
4244
|
hubspot,
|
|
3219
4245
|
slack,
|
|
3220
4246
|
notionDatabase,
|
|
4247
|
+
docuseal,
|
|
3221
4248
|
declarativeRestConnector,
|
|
3222
4249
|
twilioSmsConnector,
|
|
3223
4250
|
stripePackConnector,
|
|
@@ -3230,4 +4257,4 @@ export {
|
|
|
3230
4257
|
asanaConnector,
|
|
3231
4258
|
salesforceConnector
|
|
3232
4259
|
};
|
|
3233
|
-
//# sourceMappingURL=chunk-
|
|
4260
|
+
//# sourceMappingURL=chunk-JU25UDN2.js.map
|