@tangle-network/agent-integrations 0.2.0 → 0.3.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 +31 -1
- package/dist/index.d.ts +187 -3
- package/dist/index.js +482 -84
- package/dist/index.js.map +1 -1
- package/docs/execution-layer-launch-plan.md +220 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { createHmac as
|
|
2
|
+
import { createHmac as createHmac3, randomUUID as randomUUID2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
3
3
|
|
|
4
4
|
// src/connectors/types.ts
|
|
5
5
|
var ResourceContention = class extends Error {
|
|
@@ -2525,43 +2525,6 @@ function signHeaders(creds, body, idempotencyKey) {
|
|
|
2525
2525
|
}
|
|
2526
2526
|
|
|
2527
2527
|
// src/connectors/adapters/stripe-webhook-receiver.ts
|
|
2528
|
-
import { createHmac as createHmac3, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
2529
|
-
var SIGNATURE_TOLERANCE_SECONDS = 5 * 60;
|
|
2530
|
-
function parseStripeHeader(header) {
|
|
2531
|
-
const t = { sigs: [] };
|
|
2532
|
-
for (const part of header.split(",")) {
|
|
2533
|
-
const idx = part.indexOf("=");
|
|
2534
|
-
if (idx < 0) continue;
|
|
2535
|
-
const key = part.slice(0, idx).trim();
|
|
2536
|
-
const val = part.slice(idx + 1).trim();
|
|
2537
|
-
if (key === "t") {
|
|
2538
|
-
const n = Number(val);
|
|
2539
|
-
if (Number.isFinite(n)) t.ts = n;
|
|
2540
|
-
} else if (key === "v1") {
|
|
2541
|
-
t.sigs.push(val);
|
|
2542
|
-
}
|
|
2543
|
-
}
|
|
2544
|
-
if (t.ts === void 0 || t.sigs.length === 0) return null;
|
|
2545
|
-
return { t: t.ts, sigs: t.sigs };
|
|
2546
|
-
}
|
|
2547
|
-
function verifyStripeSignature2(rawBody, header, secret, now) {
|
|
2548
|
-
const parsed = parseStripeHeader(header);
|
|
2549
|
-
if (!parsed) return false;
|
|
2550
|
-
if (Math.abs(now - parsed.t) > SIGNATURE_TOLERANCE_SECONDS) return false;
|
|
2551
|
-
const expected = createHmac3("sha256", secret).update(`${parsed.t}.${rawBody}`).digest("hex");
|
|
2552
|
-
const expectedBuf = Buffer.from(expected, "utf8");
|
|
2553
|
-
for (const sig of parsed.sigs) {
|
|
2554
|
-
const sigBuf = Buffer.from(sig, "utf8");
|
|
2555
|
-
if (sigBuf.length !== expectedBuf.length) continue;
|
|
2556
|
-
if (timingSafeEqual2(sigBuf, expectedBuf)) return true;
|
|
2557
|
-
}
|
|
2558
|
-
return false;
|
|
2559
|
-
}
|
|
2560
|
-
function firstHeader2(h, name) {
|
|
2561
|
-
const v = h[name] ?? h[name.toLowerCase()];
|
|
2562
|
-
if (Array.isArray(v)) return v[0];
|
|
2563
|
-
return typeof v === "string" ? v : void 0;
|
|
2564
|
-
}
|
|
2565
2528
|
var stripeWebhookReceiverConnector = {
|
|
2566
2529
|
manifest: {
|
|
2567
2530
|
kind: "stripe",
|
|
@@ -2569,23 +2532,17 @@ var stripeWebhookReceiverConnector = {
|
|
|
2569
2532
|
description: "Receive Stripe webhook events from your own Stripe account. Paste your endpoint signing secret (whsec_*) at connect time; we'll verify every push and feed events to your agent's runtime.",
|
|
2570
2533
|
auth: { kind: "hmac" },
|
|
2571
2534
|
category: "commerce",
|
|
2572
|
-
// Inbound-only
|
|
2573
|
-
//
|
|
2574
|
-
//
|
|
2535
|
+
// Inbound-only. Stripe events are advisory in this incarnation — the
|
|
2536
|
+
// agent reacts to them but doesn't compete for writes against the same
|
|
2537
|
+
// resource.
|
|
2575
2538
|
defaultConsistencyModel: "advisory",
|
|
2576
2539
|
capabilities: []
|
|
2577
2540
|
},
|
|
2578
|
-
async executeRead(_inv) {
|
|
2579
|
-
throw new Error("not_implemented: stripe outbound read is scaffolded \u2014 use stripe-customers connector when shipped");
|
|
2580
|
-
},
|
|
2581
|
-
async executeMutation(_inv) {
|
|
2582
|
-
throw new Error("not_implemented: stripe outbound mutation is scaffolded \u2014 use stripe-customers connector when shipped");
|
|
2583
|
-
},
|
|
2584
2541
|
verifySignature({ rawBody, headers, source }) {
|
|
2585
2542
|
if (source.credentials.kind !== "hmac") return { valid: false, reason: "missing_hmac_secret" };
|
|
2586
|
-
const sig =
|
|
2543
|
+
const sig = firstHeader(headers, "stripe-signature");
|
|
2587
2544
|
if (!sig) return { valid: false, reason: "missing_stripe_signature_header" };
|
|
2588
|
-
const ok =
|
|
2545
|
+
const ok = verifyStripeSignature(rawBody, sig, source.credentials.secret);
|
|
2589
2546
|
return ok ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
2590
2547
|
},
|
|
2591
2548
|
async handleInboundEvent({ rawBody }) {
|
|
@@ -2619,24 +2576,6 @@ var stripeWebhookReceiverConnector = {
|
|
|
2619
2576
|
};
|
|
2620
2577
|
|
|
2621
2578
|
// src/connectors/adapters/slack-events.ts
|
|
2622
|
-
import { createHmac as createHmac4, timingSafeEqual as timingSafeEqual3 } from "crypto";
|
|
2623
|
-
var SIGNATURE_TOLERANCE_SECONDS2 = 5 * 60;
|
|
2624
|
-
function firstHeader3(h, name) {
|
|
2625
|
-
const v = h[name] ?? h[name.toLowerCase()];
|
|
2626
|
-
if (Array.isArray(v)) return v[0];
|
|
2627
|
-
return typeof v === "string" ? v : void 0;
|
|
2628
|
-
}
|
|
2629
|
-
function verifySlackSignature2(rawBody, signatureHeader, timestampHeader, secret, now) {
|
|
2630
|
-
if (!signatureHeader.startsWith("v0=")) return false;
|
|
2631
|
-
const ts = Number(timestampHeader);
|
|
2632
|
-
if (!Number.isFinite(ts)) return false;
|
|
2633
|
-
if (Math.abs(now - ts) > SIGNATURE_TOLERANCE_SECONDS2) return false;
|
|
2634
|
-
const expected = "v0=" + createHmac4("sha256", secret).update(`v0:${ts}:${rawBody}`).digest("hex");
|
|
2635
|
-
const expectedBuf = Buffer.from(expected, "utf8");
|
|
2636
|
-
const sigBuf = Buffer.from(signatureHeader, "utf8");
|
|
2637
|
-
if (sigBuf.length !== expectedBuf.length) return false;
|
|
2638
|
-
return timingSafeEqual3(sigBuf, expectedBuf);
|
|
2639
|
-
}
|
|
2640
2579
|
var slackEventsConnector = {
|
|
2641
2580
|
manifest: {
|
|
2642
2581
|
// NOTE: `slack` is owned by the OAuth bot connector in slack.ts (post_message,
|
|
@@ -2649,23 +2588,17 @@ var slackEventsConnector = {
|
|
|
2649
2588
|
description: "Receive workspace events (messages, reactions, app mentions, \u2026) from Slack's Events API. Outbound bot messaging will land in a follow-up.",
|
|
2650
2589
|
auth: { kind: "hmac" },
|
|
2651
2590
|
category: "comms",
|
|
2652
|
-
// Inbound-only
|
|
2653
|
-
//
|
|
2591
|
+
// Inbound-only. Events are advisory in this incarnation — agents observe
|
|
2592
|
+
// and react, no CAS.
|
|
2654
2593
|
defaultConsistencyModel: "advisory",
|
|
2655
2594
|
capabilities: []
|
|
2656
2595
|
},
|
|
2657
|
-
async executeRead(_inv) {
|
|
2658
|
-
throw new Error("not_implemented: slack outbound read is scaffolded");
|
|
2659
|
-
},
|
|
2660
|
-
async executeMutation(_inv) {
|
|
2661
|
-
throw new Error("not_implemented: slack outbound mutation is scaffolded");
|
|
2662
|
-
},
|
|
2663
2596
|
verifySignature({ rawBody, headers, source }) {
|
|
2664
2597
|
if (source.credentials.kind !== "hmac") return { valid: false, reason: "missing_hmac_secret" };
|
|
2665
|
-
const sig =
|
|
2666
|
-
const ts =
|
|
2598
|
+
const sig = firstHeader(headers, "x-slack-signature");
|
|
2599
|
+
const ts = firstHeader(headers, "x-slack-request-timestamp");
|
|
2667
2600
|
if (!sig || !ts) return { valid: false, reason: "missing_slack_headers" };
|
|
2668
|
-
const ok =
|
|
2601
|
+
const ok = verifySlackSignature(rawBody, sig, ts, source.credentials.secret);
|
|
2669
2602
|
return ok ? { valid: true } : { valid: false, reason: "invalid_signature" };
|
|
2670
2603
|
},
|
|
2671
2604
|
async handleInboundEvent({ rawBody }) {
|
|
@@ -2707,6 +2640,434 @@ var slackEventsConnector = {
|
|
|
2707
2640
|
}
|
|
2708
2641
|
};
|
|
2709
2642
|
|
|
2643
|
+
// src/catalog.ts
|
|
2644
|
+
var riskRank = {
|
|
2645
|
+
read: 0,
|
|
2646
|
+
write: 1,
|
|
2647
|
+
destructive: 2
|
|
2648
|
+
};
|
|
2649
|
+
function integrationToolName(providerId, connectorId, actionId) {
|
|
2650
|
+
return `int_${encodeToolPart(providerId)}_${encodeToolPart(connectorId)}_${encodeToolPart(actionId)}`;
|
|
2651
|
+
}
|
|
2652
|
+
function parseIntegrationToolName(name) {
|
|
2653
|
+
const parts = name.split("_");
|
|
2654
|
+
if (parts.length !== 4 || parts[0] !== "int") {
|
|
2655
|
+
throw new Error(`Invalid integration tool name: ${name}`);
|
|
2656
|
+
}
|
|
2657
|
+
return {
|
|
2658
|
+
providerId: decodeToolPart(parts[1]),
|
|
2659
|
+
connectorId: decodeToolPart(parts[2]),
|
|
2660
|
+
actionId: decodeToolPart(parts[3])
|
|
2661
|
+
};
|
|
2662
|
+
}
|
|
2663
|
+
function buildIntegrationToolCatalog(connectors) {
|
|
2664
|
+
const tools = [];
|
|
2665
|
+
for (const connector of connectors) {
|
|
2666
|
+
for (const action of connector.actions) {
|
|
2667
|
+
const tags = unique([
|
|
2668
|
+
connector.id,
|
|
2669
|
+
connector.providerId,
|
|
2670
|
+
connector.title,
|
|
2671
|
+
connector.category,
|
|
2672
|
+
action.id,
|
|
2673
|
+
action.title,
|
|
2674
|
+
action.risk,
|
|
2675
|
+
action.dataClass,
|
|
2676
|
+
...connector.scopes ?? [],
|
|
2677
|
+
...action.requiredScopes ?? []
|
|
2678
|
+
].flatMap(tokenize));
|
|
2679
|
+
tools.push({
|
|
2680
|
+
name: integrationToolName(connector.providerId, connector.id, action.id),
|
|
2681
|
+
title: `${connector.title}: ${action.title}`,
|
|
2682
|
+
description: action.description ?? `${action.risk} action ${action.id} on ${connector.title}`,
|
|
2683
|
+
providerId: connector.providerId,
|
|
2684
|
+
connectorId: connector.id,
|
|
2685
|
+
connectorTitle: connector.title,
|
|
2686
|
+
category: connector.category,
|
|
2687
|
+
action,
|
|
2688
|
+
risk: action.risk,
|
|
2689
|
+
dataClass: action.dataClass,
|
|
2690
|
+
requiredScopes: action.requiredScopes,
|
|
2691
|
+
inputSchema: action.inputSchema,
|
|
2692
|
+
outputSchema: action.outputSchema,
|
|
2693
|
+
tags
|
|
2694
|
+
});
|
|
2695
|
+
}
|
|
2696
|
+
}
|
|
2697
|
+
return tools;
|
|
2698
|
+
}
|
|
2699
|
+
function searchIntegrationTools(catalog, query, filters = {}) {
|
|
2700
|
+
const terms = tokenize(query);
|
|
2701
|
+
const filtered = catalog.filter((tool) => {
|
|
2702
|
+
if (filters.providerId && tool.providerId !== filters.providerId) return false;
|
|
2703
|
+
if (filters.connectorId && tool.connectorId !== filters.connectorId) return false;
|
|
2704
|
+
if (filters.category && tool.category !== filters.category) return false;
|
|
2705
|
+
if (filters.dataClass && tool.dataClass !== filters.dataClass) return false;
|
|
2706
|
+
if (filters.maxRisk && riskRank[tool.risk] > riskRank[filters.maxRisk]) return false;
|
|
2707
|
+
return true;
|
|
2708
|
+
});
|
|
2709
|
+
const scored = filtered.map((tool) => scoreTool(tool, terms));
|
|
2710
|
+
return scored.filter((result) => terms.length === 0 || result.score > 0).sort((a, b) => b.score - a.score || a.tool.name.localeCompare(b.tool.name)).slice(0, filters.limit ?? 20);
|
|
2711
|
+
}
|
|
2712
|
+
function toMcpTools(tools) {
|
|
2713
|
+
return tools.map((tool) => ({
|
|
2714
|
+
name: tool.name,
|
|
2715
|
+
description: `${tool.title}. ${tool.description}`,
|
|
2716
|
+
inputSchema: tool.inputSchema ?? {
|
|
2717
|
+
type: "object",
|
|
2718
|
+
additionalProperties: true,
|
|
2719
|
+
properties: {}
|
|
2720
|
+
}
|
|
2721
|
+
}));
|
|
2722
|
+
}
|
|
2723
|
+
function scoreTool(tool, terms) {
|
|
2724
|
+
if (terms.length === 0) return { tool, score: 1, matched: [] };
|
|
2725
|
+
const haystack = new Set(tool.tags);
|
|
2726
|
+
const matched = [];
|
|
2727
|
+
let score = 0;
|
|
2728
|
+
for (const term of terms) {
|
|
2729
|
+
if (haystack.has(term)) {
|
|
2730
|
+
matched.push(term);
|
|
2731
|
+
score += 4;
|
|
2732
|
+
continue;
|
|
2733
|
+
}
|
|
2734
|
+
if (tool.tags.some((tag) => tag.includes(term))) {
|
|
2735
|
+
matched.push(term);
|
|
2736
|
+
score += 1;
|
|
2737
|
+
}
|
|
2738
|
+
}
|
|
2739
|
+
if (tool.risk === "read") score += 0.25;
|
|
2740
|
+
return { tool, score, matched: unique(matched) };
|
|
2741
|
+
}
|
|
2742
|
+
function tokenize(value) {
|
|
2743
|
+
return value.toLowerCase().split(/[^a-z0-9]+/g).map((part) => part.trim()).filter(Boolean);
|
|
2744
|
+
}
|
|
2745
|
+
function encodeToolPart(value) {
|
|
2746
|
+
return Buffer.from(value, "utf8").toString("base64url");
|
|
2747
|
+
}
|
|
2748
|
+
function decodeToolPart(value) {
|
|
2749
|
+
return Buffer.from(value, "base64url").toString("utf8");
|
|
2750
|
+
}
|
|
2751
|
+
function unique(values) {
|
|
2752
|
+
return [...new Set(values)];
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
// src/policy.ts
|
|
2756
|
+
import { randomUUID } from "crypto";
|
|
2757
|
+
var StaticIntegrationPolicyEngine = class {
|
|
2758
|
+
rules;
|
|
2759
|
+
defaultReadEffect;
|
|
2760
|
+
defaultWriteEffect;
|
|
2761
|
+
defaultDestructiveEffect;
|
|
2762
|
+
now;
|
|
2763
|
+
constructor(options = {}) {
|
|
2764
|
+
this.rules = options.rules ?? [];
|
|
2765
|
+
this.defaultReadEffect = options.defaultReadEffect ?? "allow";
|
|
2766
|
+
this.defaultWriteEffect = options.defaultWriteEffect ?? "require_approval";
|
|
2767
|
+
this.defaultDestructiveEffect = options.defaultDestructiveEffect ?? "deny";
|
|
2768
|
+
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2769
|
+
}
|
|
2770
|
+
decide(ctx) {
|
|
2771
|
+
const action = ctx.action;
|
|
2772
|
+
if (!action) return { decision: "deny", reason: "Integration action is missing from connector catalog." };
|
|
2773
|
+
const matched = this.rules.find((rule) => ruleMatches(rule, ctx));
|
|
2774
|
+
const effect = matched?.effect ?? this.defaultEffect(action.risk);
|
|
2775
|
+
const reason = matched?.reason ?? defaultReason(effect, action.risk);
|
|
2776
|
+
if (effect === "allow") return { decision: "allow", reason, metadata: matched ? { ruleId: matched.id } : void 0 };
|
|
2777
|
+
if (effect === "deny") return { decision: "deny", reason, metadata: matched ? { ruleId: matched.id } : void 0 };
|
|
2778
|
+
return {
|
|
2779
|
+
decision: "require_approval",
|
|
2780
|
+
reason,
|
|
2781
|
+
approval: buildApprovalRequest(ctx, reason, this.now()),
|
|
2782
|
+
metadata: matched ? { ruleId: matched.id } : void 0
|
|
2783
|
+
};
|
|
2784
|
+
}
|
|
2785
|
+
defaultEffect(risk) {
|
|
2786
|
+
if (risk === "read") return this.defaultReadEffect;
|
|
2787
|
+
if (risk === "write") return this.defaultWriteEffect;
|
|
2788
|
+
return this.defaultDestructiveEffect;
|
|
2789
|
+
}
|
|
2790
|
+
};
|
|
2791
|
+
function createDefaultIntegrationPolicyEngine(options = {}) {
|
|
2792
|
+
return new StaticIntegrationPolicyEngine(options);
|
|
2793
|
+
}
|
|
2794
|
+
function buildApprovalRequest(ctx, reason, requestedAt) {
|
|
2795
|
+
if (!ctx.action) {
|
|
2796
|
+
throw new Error("Cannot build approval request without an action descriptor.");
|
|
2797
|
+
}
|
|
2798
|
+
return {
|
|
2799
|
+
id: `approval_${randomUUID()}`,
|
|
2800
|
+
connectionId: ctx.connection.id,
|
|
2801
|
+
providerId: ctx.connection.providerId,
|
|
2802
|
+
connectorId: ctx.connection.connectorId,
|
|
2803
|
+
action: ctx.request.action,
|
|
2804
|
+
actor: { type: ctx.subject.type, id: ctx.subject.id },
|
|
2805
|
+
risk: ctx.action.risk,
|
|
2806
|
+
dataClass: ctx.action.dataClass,
|
|
2807
|
+
reason,
|
|
2808
|
+
requestedAt: requestedAt.toISOString(),
|
|
2809
|
+
inputPreview: previewInput(ctx.request.input)
|
|
2810
|
+
};
|
|
2811
|
+
}
|
|
2812
|
+
function redactApprovalRequest(request) {
|
|
2813
|
+
return {
|
|
2814
|
+
...request,
|
|
2815
|
+
inputPreview: redactUnknown(request.inputPreview)
|
|
2816
|
+
};
|
|
2817
|
+
}
|
|
2818
|
+
function ruleMatches(rule, ctx) {
|
|
2819
|
+
if (!ctx.action) return false;
|
|
2820
|
+
if (rule.providerId && rule.providerId !== ctx.connection.providerId) return false;
|
|
2821
|
+
if (rule.connectorId && rule.connectorId !== ctx.connection.connectorId) return false;
|
|
2822
|
+
if (rule.action && rule.action !== ctx.request.action) return false;
|
|
2823
|
+
if (rule.risk && rule.risk !== ctx.action.risk) return false;
|
|
2824
|
+
if (rule.maxRisk && riskRank2(ctx.action.risk) > riskRank2(rule.maxRisk)) return false;
|
|
2825
|
+
if (rule.dataClass && rule.dataClass !== ctx.action.dataClass) return false;
|
|
2826
|
+
return true;
|
|
2827
|
+
}
|
|
2828
|
+
function riskRank2(risk) {
|
|
2829
|
+
if (risk === "read") return 0;
|
|
2830
|
+
if (risk === "write") return 1;
|
|
2831
|
+
return 2;
|
|
2832
|
+
}
|
|
2833
|
+
function defaultReason(effect, risk) {
|
|
2834
|
+
if (effect === "allow") return `${risk} integration action allowed by default policy.`;
|
|
2835
|
+
if (effect === "deny") return `${risk} integration action denied by default policy.`;
|
|
2836
|
+
return `${risk} integration action requires approval by default policy.`;
|
|
2837
|
+
}
|
|
2838
|
+
function previewInput(input) {
|
|
2839
|
+
return redactUnknown(input);
|
|
2840
|
+
}
|
|
2841
|
+
function redactUnknown(value) {
|
|
2842
|
+
if (Array.isArray(value)) return value.map(redactUnknown);
|
|
2843
|
+
if (!value || typeof value !== "object") return value;
|
|
2844
|
+
const out = {};
|
|
2845
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2846
|
+
if (/token|secret|password|authorization|api[_-]?key|credential/i.test(key)) {
|
|
2847
|
+
out[key] = "[REDACTED]";
|
|
2848
|
+
} else {
|
|
2849
|
+
out[key] = redactUnknown(child);
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
return out;
|
|
2853
|
+
}
|
|
2854
|
+
|
|
2855
|
+
// src/sandbox.ts
|
|
2856
|
+
function buildIntegrationInvocationEnvelope(input) {
|
|
2857
|
+
const parsed = parseIntegrationToolName(input.toolName);
|
|
2858
|
+
return {
|
|
2859
|
+
kind: "integration.invocation",
|
|
2860
|
+
capabilityToken: input.capabilityToken,
|
|
2861
|
+
toolName: input.toolName,
|
|
2862
|
+
action: parsed.actionId,
|
|
2863
|
+
input: input.args,
|
|
2864
|
+
idempotencyKey: input.idempotencyKey,
|
|
2865
|
+
dryRun: input.dryRun,
|
|
2866
|
+
metadata: input.metadata
|
|
2867
|
+
};
|
|
2868
|
+
}
|
|
2869
|
+
function invocationRequestFromEnvelope(envelope) {
|
|
2870
|
+
return {
|
|
2871
|
+
action: envelope.action,
|
|
2872
|
+
input: envelope.input,
|
|
2873
|
+
idempotencyKey: envelope.idempotencyKey,
|
|
2874
|
+
dryRun: envelope.dryRun,
|
|
2875
|
+
metadata: envelope.metadata
|
|
2876
|
+
};
|
|
2877
|
+
}
|
|
2878
|
+
function redactInvocationEnvelope(envelope) {
|
|
2879
|
+
return {
|
|
2880
|
+
...envelope,
|
|
2881
|
+
capabilityToken: "[REDACTED]",
|
|
2882
|
+
input: redactUnknown2(envelope.input)
|
|
2883
|
+
};
|
|
2884
|
+
}
|
|
2885
|
+
function redactCapability(capability) {
|
|
2886
|
+
return {
|
|
2887
|
+
...capability,
|
|
2888
|
+
metadata: redactUnknown2(capability.metadata)
|
|
2889
|
+
};
|
|
2890
|
+
}
|
|
2891
|
+
function normalizeIntegrationResult(result) {
|
|
2892
|
+
const output = result.output;
|
|
2893
|
+
if (!result.ok && output?.approvalRequired === true && output.approval) {
|
|
2894
|
+
return {
|
|
2895
|
+
status: "approval_required",
|
|
2896
|
+
action: result.action,
|
|
2897
|
+
approval: output.approval,
|
|
2898
|
+
metadata: result.metadata
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
if (!result.ok) {
|
|
2902
|
+
return {
|
|
2903
|
+
status: "failed",
|
|
2904
|
+
action: result.action,
|
|
2905
|
+
error: String(result.output ?? result.warnings?.[0] ?? "integration action failed"),
|
|
2906
|
+
metadata: result.metadata
|
|
2907
|
+
};
|
|
2908
|
+
}
|
|
2909
|
+
return {
|
|
2910
|
+
status: "ok",
|
|
2911
|
+
action: result.action,
|
|
2912
|
+
output: result.output,
|
|
2913
|
+
metadata: result.metadata
|
|
2914
|
+
};
|
|
2915
|
+
}
|
|
2916
|
+
function redactUnknown2(value) {
|
|
2917
|
+
if (Array.isArray(value)) return value.map(redactUnknown2);
|
|
2918
|
+
if (!value || typeof value !== "object") return value;
|
|
2919
|
+
const out = {};
|
|
2920
|
+
for (const [key, child] of Object.entries(value)) {
|
|
2921
|
+
if (/token|secret|password|authorization|api[_-]?key|credential/i.test(key)) {
|
|
2922
|
+
out[key] = "[REDACTED]";
|
|
2923
|
+
} else {
|
|
2924
|
+
out[key] = redactUnknown2(child);
|
|
2925
|
+
}
|
|
2926
|
+
}
|
|
2927
|
+
return out;
|
|
2928
|
+
}
|
|
2929
|
+
|
|
2930
|
+
// src/adapter-provider.ts
|
|
2931
|
+
function createConnectorAdapterProvider(options) {
|
|
2932
|
+
const providerId = options.id ?? "first-party";
|
|
2933
|
+
const now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2934
|
+
const adapters = /* @__PURE__ */ new Map();
|
|
2935
|
+
for (const adapter of options.adapters) {
|
|
2936
|
+
adapters.set(adapter.manifest.kind, adapter);
|
|
2937
|
+
}
|
|
2938
|
+
return {
|
|
2939
|
+
id: providerId,
|
|
2940
|
+
kind: options.kind ?? "first_party",
|
|
2941
|
+
listConnectors: () => [...adapters.values()].map((adapter) => manifestToConnector(providerId, adapter)),
|
|
2942
|
+
async invokeAction(connection, request) {
|
|
2943
|
+
const adapter = adapters.get(connection.connectorId);
|
|
2944
|
+
if (!adapter) {
|
|
2945
|
+
throw new IntegrationError(`Connector adapter ${connection.connectorId} not found.`, "connector_not_found");
|
|
2946
|
+
}
|
|
2947
|
+
const capability = adapter.manifest.capabilities.find((candidate) => candidate.name === request.action);
|
|
2948
|
+
if (!capability) {
|
|
2949
|
+
throw new IntegrationError(`Capability ${request.action} is not defined by ${connection.connectorId}.`, "action_not_found");
|
|
2950
|
+
}
|
|
2951
|
+
const source = await options.resolveDataSource(connection);
|
|
2952
|
+
const invocation = {
|
|
2953
|
+
source,
|
|
2954
|
+
capabilityName: request.action,
|
|
2955
|
+
args: toRecord(request.input),
|
|
2956
|
+
idempotencyKey: request.idempotencyKey ?? `idem_${connection.id}_${request.action}_${now().getTime()}`,
|
|
2957
|
+
expectedEtag: typeof request.metadata?.expectedEtag === "string" ? request.metadata.expectedEtag : void 0,
|
|
2958
|
+
callSessionId: typeof request.metadata?.callSessionId === "string" ? request.metadata.callSessionId : void 0
|
|
2959
|
+
};
|
|
2960
|
+
if (capability.class === "read") {
|
|
2961
|
+
if (!adapter.executeRead) {
|
|
2962
|
+
throw new IntegrationError(`Connector ${connection.connectorId} does not implement reads.`, "action_not_found");
|
|
2963
|
+
}
|
|
2964
|
+
const result = await adapter.executeRead(invocation);
|
|
2965
|
+
return readResultToAction(request, result);
|
|
2966
|
+
}
|
|
2967
|
+
if (capability.class === "mutation") {
|
|
2968
|
+
if (!adapter.executeMutation) {
|
|
2969
|
+
throw new IntegrationError(`Connector ${connection.connectorId} does not implement mutations.`, "action_not_found");
|
|
2970
|
+
}
|
|
2971
|
+
const result = await adapter.executeMutation(invocation);
|
|
2972
|
+
return mutationResultToAction(request, result);
|
|
2973
|
+
}
|
|
2974
|
+
throw new IntegrationError(`Capability ${request.action} is not invokable as an action.`, "action_not_found");
|
|
2975
|
+
}
|
|
2976
|
+
};
|
|
2977
|
+
}
|
|
2978
|
+
function manifestToConnector(providerId, adapter) {
|
|
2979
|
+
const manifest = adapter.manifest;
|
|
2980
|
+
return {
|
|
2981
|
+
id: manifest.kind,
|
|
2982
|
+
providerId,
|
|
2983
|
+
title: manifest.displayName,
|
|
2984
|
+
category: mapCategory(manifest.category),
|
|
2985
|
+
auth: mapAuth(manifest.auth.kind),
|
|
2986
|
+
scopes: manifest.auth.kind === "oauth2" ? manifest.auth.scopes : [],
|
|
2987
|
+
actions: manifest.capabilities.filter((capability) => capability.class === "read" || capability.class === "mutation").map((capability) => ({
|
|
2988
|
+
id: capability.name,
|
|
2989
|
+
title: titleFromName(capability.name),
|
|
2990
|
+
risk: capability.class === "read" ? "read" : capability.externalEffect ? "destructive" : "write",
|
|
2991
|
+
requiredScopes: capability.requiredScopes ?? [],
|
|
2992
|
+
dataClass: inferDataClass(manifest.category),
|
|
2993
|
+
description: capability.description,
|
|
2994
|
+
approvalRequired: capability.class === "mutation",
|
|
2995
|
+
inputSchema: capability.parameters
|
|
2996
|
+
}))
|
|
2997
|
+
};
|
|
2998
|
+
}
|
|
2999
|
+
function readResultToAction(request, result) {
|
|
3000
|
+
return {
|
|
3001
|
+
ok: true,
|
|
3002
|
+
action: request.action,
|
|
3003
|
+
output: result.data,
|
|
3004
|
+
metadata: {
|
|
3005
|
+
etag: result.etag,
|
|
3006
|
+
fetchedAt: result.fetchedAt
|
|
3007
|
+
}
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
function mutationResultToAction(request, result) {
|
|
3011
|
+
if (result.status === "committed") {
|
|
3012
|
+
return {
|
|
3013
|
+
ok: true,
|
|
3014
|
+
action: request.action,
|
|
3015
|
+
output: result.data,
|
|
3016
|
+
metadata: {
|
|
3017
|
+
etagAfter: result.etagAfter,
|
|
3018
|
+
committedAt: result.committedAt,
|
|
3019
|
+
idempotentReplay: result.idempotentReplay
|
|
3020
|
+
}
|
|
3021
|
+
};
|
|
3022
|
+
}
|
|
3023
|
+
if (result.status === "conflict") {
|
|
3024
|
+
return {
|
|
3025
|
+
ok: false,
|
|
3026
|
+
action: request.action,
|
|
3027
|
+
output: {
|
|
3028
|
+
conflict: true,
|
|
3029
|
+
message: result.message,
|
|
3030
|
+
alternatives: result.alternatives,
|
|
3031
|
+
currentState: result.currentState
|
|
3032
|
+
}
|
|
3033
|
+
};
|
|
3034
|
+
}
|
|
3035
|
+
return {
|
|
3036
|
+
ok: false,
|
|
3037
|
+
action: request.action,
|
|
3038
|
+
output: {
|
|
3039
|
+
rateLimited: true,
|
|
3040
|
+
retryAfterMs: result.retryAfterMs,
|
|
3041
|
+
message: result.message
|
|
3042
|
+
}
|
|
3043
|
+
};
|
|
3044
|
+
}
|
|
3045
|
+
function mapAuth(kind) {
|
|
3046
|
+
if (kind === "oauth2") return "oauth2";
|
|
3047
|
+
if (kind === "api-key") return "api_key";
|
|
3048
|
+
if (kind === "none") return "none";
|
|
3049
|
+
return "custom";
|
|
3050
|
+
}
|
|
3051
|
+
function mapCategory(category) {
|
|
3052
|
+
if (category === "comms") return "chat";
|
|
3053
|
+
if (category === "spreadsheet") return "database";
|
|
3054
|
+
if (category === "doc") return "docs";
|
|
3055
|
+
if (category === "commerce") return "workflow";
|
|
3056
|
+
return category === "other" ? "other" : category;
|
|
3057
|
+
}
|
|
3058
|
+
function inferDataClass(category) {
|
|
3059
|
+
if (category === "commerce") return "sensitive";
|
|
3060
|
+
if (category === "webhook") return "internal";
|
|
3061
|
+
return "private";
|
|
3062
|
+
}
|
|
3063
|
+
function titleFromName(name) {
|
|
3064
|
+
return name.split(/[._-]/g).filter(Boolean).map((part) => part.slice(0, 1).toUpperCase() + part.slice(1)).join(" ");
|
|
3065
|
+
}
|
|
3066
|
+
function toRecord(input) {
|
|
3067
|
+
if (input && typeof input === "object" && !Array.isArray(input)) return input;
|
|
3068
|
+
return {};
|
|
3069
|
+
}
|
|
3070
|
+
|
|
2710
3071
|
// src/index.ts
|
|
2711
3072
|
var IntegrationError = class extends Error {
|
|
2712
3073
|
constructor(message, code) {
|
|
@@ -2738,6 +3099,7 @@ var IntegrationHub = class {
|
|
|
2738
3099
|
store;
|
|
2739
3100
|
capabilitySecret;
|
|
2740
3101
|
guard;
|
|
3102
|
+
policy;
|
|
2741
3103
|
now;
|
|
2742
3104
|
constructor(options) {
|
|
2743
3105
|
if (!options.capabilitySecret) {
|
|
@@ -2747,6 +3109,7 @@ var IntegrationHub = class {
|
|
|
2747
3109
|
this.store = options.store;
|
|
2748
3110
|
this.capabilitySecret = options.capabilitySecret;
|
|
2749
3111
|
this.guard = options.guard;
|
|
3112
|
+
this.policy = options.policy;
|
|
2750
3113
|
this.now = options.now ?? (() => /* @__PURE__ */ new Date());
|
|
2751
3114
|
}
|
|
2752
3115
|
async listConnectors() {
|
|
@@ -2776,11 +3139,11 @@ var IntegrationHub = class {
|
|
|
2776
3139
|
assertScopes(connection, request.scopes);
|
|
2777
3140
|
const now = this.now();
|
|
2778
3141
|
const capability = {
|
|
2779
|
-
id: `cap_${
|
|
3142
|
+
id: `cap_${randomUUID2()}`,
|
|
2780
3143
|
subject: request.subject,
|
|
2781
3144
|
connectionId: request.connectionId,
|
|
2782
|
-
scopes:
|
|
2783
|
-
allowedActions:
|
|
3145
|
+
scopes: unique2(request.scopes),
|
|
3146
|
+
allowedActions: unique2(request.allowedActions),
|
|
2784
3147
|
issuedAt: now.toISOString(),
|
|
2785
3148
|
expiresAt: new Date(now.getTime() + request.ttlMs).toISOString(),
|
|
2786
3149
|
metadata: request.metadata
|
|
@@ -2808,6 +3171,25 @@ var IntegrationHub = class {
|
|
|
2808
3171
|
assertScopes(connection, action.requiredScopes);
|
|
2809
3172
|
assertScopes({ ...connection, grantedScopes: capability.scopes }, action.requiredScopes);
|
|
2810
3173
|
const fullRequest = { ...request, connectionId: connection.id };
|
|
3174
|
+
if (this.policy) {
|
|
3175
|
+
const decision = await this.policy.decide({
|
|
3176
|
+
connection,
|
|
3177
|
+
request: fullRequest,
|
|
3178
|
+
action,
|
|
3179
|
+
subject: capability.subject
|
|
3180
|
+
});
|
|
3181
|
+
if (decision.decision === "deny") {
|
|
3182
|
+
throw new IntegrationError(decision.reason, "policy_denied");
|
|
3183
|
+
}
|
|
3184
|
+
if (decision.decision === "require_approval") {
|
|
3185
|
+
return {
|
|
3186
|
+
ok: false,
|
|
3187
|
+
action: request.action,
|
|
3188
|
+
output: { approvalRequired: true, approval: decision.approval },
|
|
3189
|
+
metadata: { policyDecision: decision.decision, reason: decision.reason, ...decision.metadata }
|
|
3190
|
+
};
|
|
3191
|
+
}
|
|
3192
|
+
}
|
|
2811
3193
|
const proceed = () => Promise.resolve(provider.invokeAction(connection, fullRequest));
|
|
2812
3194
|
if (this.guard) {
|
|
2813
3195
|
return this.guard.invokeAction({ connection, request: fullRequest, action }, proceed);
|
|
@@ -2994,12 +3376,12 @@ function assertScopes(connection, requiredScopes) {
|
|
|
2994
3376
|
if (missing.length > 0) throw new IntegrationError(`Missing integration scopes: ${missing.join(", ")}`, "scope_denied");
|
|
2995
3377
|
}
|
|
2996
3378
|
function hmac(payload, secret) {
|
|
2997
|
-
return
|
|
3379
|
+
return createHmac3("sha256", secret).update(payload).digest("base64url");
|
|
2998
3380
|
}
|
|
2999
3381
|
function constantTimeEqual(a, b) {
|
|
3000
3382
|
const left = Buffer.from(a);
|
|
3001
3383
|
const right = Buffer.from(b);
|
|
3002
|
-
return left.length === right.length &&
|
|
3384
|
+
return left.length === right.length && timingSafeEqual2(left, right);
|
|
3003
3385
|
}
|
|
3004
3386
|
function base64UrlEncode(value) {
|
|
3005
3387
|
return Buffer.from(value, "utf8").toString("base64url");
|
|
@@ -3007,7 +3389,7 @@ function base64UrlEncode(value) {
|
|
|
3007
3389
|
function base64UrlDecode(value) {
|
|
3008
3390
|
return Buffer.from(value, "base64url").toString("utf8");
|
|
3009
3391
|
}
|
|
3010
|
-
function
|
|
3392
|
+
function unique2(values) {
|
|
3011
3393
|
return [...new Set(values)];
|
|
3012
3394
|
}
|
|
3013
3395
|
export {
|
|
@@ -3018,9 +3400,15 @@ export {
|
|
|
3018
3400
|
IntegrationError,
|
|
3019
3401
|
IntegrationHub,
|
|
3020
3402
|
ResourceContention,
|
|
3403
|
+
StaticIntegrationPolicyEngine,
|
|
3021
3404
|
_resetPendingFlowsForTests,
|
|
3022
3405
|
assertValidConnectorManifest,
|
|
3406
|
+
buildApprovalRequest,
|
|
3407
|
+
buildIntegrationInvocationEnvelope,
|
|
3408
|
+
buildIntegrationToolCatalog,
|
|
3023
3409
|
consumePendingFlow,
|
|
3410
|
+
createConnectorAdapterProvider,
|
|
3411
|
+
createDefaultIntegrationPolicyEngine,
|
|
3024
3412
|
createHttpIntegrationProvider,
|
|
3025
3413
|
createMockIntegrationProvider,
|
|
3026
3414
|
exchangeAuthorizationCode,
|
|
@@ -3028,17 +3416,27 @@ export {
|
|
|
3028
3416
|
googleCalendar,
|
|
3029
3417
|
googleSheets,
|
|
3030
3418
|
hubspot,
|
|
3419
|
+
integrationToolName,
|
|
3420
|
+
invocationRequestFromEnvelope,
|
|
3421
|
+
manifestToConnector,
|
|
3031
3422
|
microsoftCalendar,
|
|
3423
|
+
normalizeIntegrationResult,
|
|
3032
3424
|
notionDatabase,
|
|
3425
|
+
parseIntegrationToolName,
|
|
3033
3426
|
parseStripeSignatureHeader,
|
|
3427
|
+
redactApprovalRequest,
|
|
3428
|
+
redactCapability,
|
|
3429
|
+
redactInvocationEnvelope,
|
|
3034
3430
|
refreshAccessToken,
|
|
3035
3431
|
sanitizeConnection,
|
|
3432
|
+
searchIntegrationTools,
|
|
3036
3433
|
signCapability,
|
|
3037
3434
|
slack,
|
|
3038
3435
|
slackEventsConnector,
|
|
3039
3436
|
startOAuthFlow,
|
|
3040
3437
|
stripePackConnector,
|
|
3041
3438
|
stripeWebhookReceiverConnector,
|
|
3439
|
+
toMcpTools,
|
|
3042
3440
|
twilioSmsConnector,
|
|
3043
3441
|
validateConnectorManifest,
|
|
3044
3442
|
verifyCapabilityToken,
|