@wfgraph/plugins 3.1.1 → 4.0.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 +23 -6
- package/dist/client-B0M2Cle4.js +115 -0
- package/dist/{client-BEcFnbOB.js → client-B0dhD5OA.js} +1 -2
- package/dist/{client-DDDCNLId.js → client-BQmwEhSH.js} +1 -2
- package/dist/{client-LRwmIiPU.js → client-C32OdgmT.js} +5 -6
- package/dist/client-D3ALurAR.js +167 -0
- package/dist/config-options-BgSb_XqZ.js +101 -0
- package/dist/{errors-C4i6-DQE.js → errors-DQPf9k4I.js} +31 -24
- package/dist/index.d.ts +185 -53
- package/dist/index.js +1887 -447
- package/dist/omit-undefined-CnDMZgmp.js +6 -0
- package/dist/{test-B8I6nxMN.js → test-B5WBOt4r.js} +7 -6
- package/dist/{test-DN4ZApY4.js → test-Beetxizr.js} +2 -3
- package/dist/test-CTNffdZM.js +50 -0
- package/dist/{test-DWVpiQNl.js → test-CTWGAckT.js} +2 -3
- package/dist/{test-Dsrib2EJ.js → test-CiIt-3_x.js} +2 -3
- package/dist/{test-8wDSAiFQ.js → test-x2qD6zdE.js} +2 -3
- package/dist/ui.d.ts +11 -19
- package/dist/ui.js +119 -73
- package/package.json +15 -11
- package/skills/wfgraph-plugins/SKILL.md +106 -0
- package/dist/client-C2NIQxhb.js +0 -64
package/README.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# @wfgraph/plugins
|
|
2
2
|
|
|
3
|
-
The
|
|
4
|
-
[Workflow Graph](https://github.com/alandotcom/wfgraph): Clerk, Linear, Resend, Slack, and
|
|
3
|
+
The six built-in integrations for
|
|
4
|
+
[Workflow Graph](https://github.com/alandotcom/wfgraph): Clerk, Linear, PostHog, Resend, Slack, and
|
|
5
5
|
Twilio.
|
|
6
6
|
|
|
7
7
|
```bash
|
|
@@ -16,14 +16,31 @@ import { createWfGraphApp } from "@wfgraph/core";
|
|
|
16
16
|
import { builtInIntegrations } from "@wfgraph/plugins";
|
|
17
17
|
|
|
18
18
|
const wfgraph = await createWfGraphApp({
|
|
19
|
-
extensions: {
|
|
19
|
+
extensions: {
|
|
20
|
+
integrations: builtInIntegrations({
|
|
21
|
+
slack: {
|
|
22
|
+
oauthClient: {
|
|
23
|
+
clientId: process.env.SLACK_CLIENT_ID,
|
|
24
|
+
clientSecret: process.env.SLACK_CLIENT_SECRET,
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
}) /* ...events and actions */,
|
|
28
|
+
},
|
|
20
29
|
// ...the rest
|
|
21
30
|
});
|
|
22
31
|
```
|
|
23
32
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
33
|
+
Resend and PostHog include OAuth through a public client metadata document. Slack enables
|
|
34
|
+
OAuth when you pass a registered Slack app client. All three retain manual credential
|
|
35
|
+
entry. OAuth callback URLs also require `publicUrl` on `createWfGraphApp`.
|
|
36
|
+
Resend's webhook Events use the same `publicUrl` to copy a Connection-addressed
|
|
37
|
+
intake URL; add the signing secret (`RESEND_WEBHOOK_SECRET`) on that Connection.
|
|
38
|
+
|
|
39
|
+
Clerk, Linear, PostHog, Resend, and Twilio are also exported as values for a host that wants
|
|
40
|
+
some of the six rather than all of them. Slack is a factory because it accepts OAuth
|
|
41
|
+
client configuration. Selecting individual exports narrows what reaches
|
|
42
|
+
`createWfGraphApp`, not what the process loads, because this entry imports all six
|
|
43
|
+
integrations.
|
|
27
44
|
|
|
28
45
|
`@wfgraph/core` is a peer dependency: an integration is built against it and handed back to it,
|
|
29
46
|
so the two must resolve to one copy. They are released together and always share a version.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { callExternal, parsePayload } from "@wfgraph/core/plugin";
|
|
2
|
+
import { Schema } from "effect";
|
|
3
|
+
/**
|
|
4
|
+
* PostHog's error body, the same shape on both endpoints.
|
|
5
|
+
*
|
|
6
|
+
* Every field is optional because the handler depends on none of them: they
|
|
7
|
+
* make the message a person reads, and a build answering something else should
|
|
8
|
+
* still report its status rather than fail the decode.
|
|
9
|
+
*/
|
|
10
|
+
const posthogErrorSchema = Schema.Struct({
|
|
11
|
+
type: Schema.optionalKey(Schema.String),
|
|
12
|
+
code: Schema.optionalKey(Schema.String),
|
|
13
|
+
detail: Schema.optionalKey(Schema.String),
|
|
14
|
+
attr: Schema.optionalKey(Schema.NullishOr(Schema.String))
|
|
15
|
+
});
|
|
16
|
+
/**
|
|
17
|
+
* What the capture endpoint answers.
|
|
18
|
+
*
|
|
19
|
+
* Cloud answers `{"status": 1}` and older self-hosted builds answer
|
|
20
|
+
* `{"status": "Ok"}`, so both stand. The key is optional because nothing
|
|
21
|
+
* downstream reads it -- ingestion is async, and a capture that was accepted
|
|
22
|
+
* says nothing about whether the event was stored.
|
|
23
|
+
*/
|
|
24
|
+
const captureResponseSchema = Schema.Struct({ status: Schema.optionalKey(Schema.Union([Schema.Finite, Schema.String])) });
|
|
25
|
+
/**
|
|
26
|
+
* The flags body is only checked for arrival. The connection test reads
|
|
27
|
+
* `result.ok`, so recording unused flag maps would describe a shape nobody
|
|
28
|
+
* consults.
|
|
29
|
+
*/
|
|
30
|
+
const flagsResponseSchema = Schema.Unknown;
|
|
31
|
+
/**
|
|
32
|
+
* What PostHog said, in one sentence a person reads.
|
|
33
|
+
*
|
|
34
|
+
* A refusal carries PostHog's own `detail` when its error body is the documented
|
|
35
|
+
* shape and the bare status when it is not, which is what something standing in
|
|
36
|
+
* front of the API answers with.
|
|
37
|
+
*/
|
|
38
|
+
function describePostHogFailure(error) {
|
|
39
|
+
if (error._tag === "ExternalUnreachable") return error.message;
|
|
40
|
+
if (error._tag === "ExternalUnreadable") return `PostHog answered ${error.status} with an unrecognized body`;
|
|
41
|
+
return readPostHogError(error.payload)?.detail ?? describeCaptureStatus(error.status) ?? `HTTP ${error.status}`;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* What a refusal carrying no readable body means.
|
|
45
|
+
*
|
|
46
|
+
* Only the capture endpoint lands here, because a flags refusal always carries a
|
|
47
|
+
* `detail`. These are the two statuses it answers a misconfigured node with: a
|
|
48
|
+
* key it will not take, and an event it could not read.
|
|
49
|
+
*/
|
|
50
|
+
function describeCaptureStatus(status) {
|
|
51
|
+
if (status === 401) return "PostHog refused the project API key (HTTP 401)";
|
|
52
|
+
if (status === 400) return "PostHog could not read the event (HTTP 400)";
|
|
53
|
+
}
|
|
54
|
+
/** PostHog's error body, for a caller that reports more than the message. */
|
|
55
|
+
function readPostHogError(payload) {
|
|
56
|
+
return parsePayload(payload, posthogErrorSchema);
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* The host with any trailing slash taken off, so joining a path cannot produce
|
|
60
|
+
* a double slash. A blank host is the US cloud, which is where a project lives
|
|
61
|
+
* unless someone chose otherwise.
|
|
62
|
+
*/
|
|
63
|
+
function resolvePostHogHost(host) {
|
|
64
|
+
return (host?.trim() || "https://us.i.posthog.com").replace(/\/+$/, "");
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Capture one event.
|
|
68
|
+
*
|
|
69
|
+
* Marked `safeToRepeat` on a deterministic body: every field the dedup sort key
|
|
70
|
+
* reads is fixed by the caller before the first attempt, so a retry sends bytes
|
|
71
|
+
* identical to the ones that may already have arrived. PostHog collapses the
|
|
72
|
+
* pair during a background merge. That is eventual rather than immediate, and
|
|
73
|
+
* the trade is deliberate -- for an analytics event a duplicate that merges away
|
|
74
|
+
* beats a hole in a funnel nobody can reconstruct.
|
|
75
|
+
*/
|
|
76
|
+
function captureEvent(connection, event) {
|
|
77
|
+
return callExternal({
|
|
78
|
+
system: "PostHog",
|
|
79
|
+
url: `${connection.host}/i/v0/e/`,
|
|
80
|
+
method: "POST",
|
|
81
|
+
headers: { "content-type": "application/json" },
|
|
82
|
+
body: {
|
|
83
|
+
kind: "json",
|
|
84
|
+
value: {
|
|
85
|
+
api_key: connection.projectApiKey,
|
|
86
|
+
...event
|
|
87
|
+
}
|
|
88
|
+
},
|
|
89
|
+
schema: captureResponseSchema,
|
|
90
|
+
safeToRepeat: true
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Evaluate flags for one person, which is the cheapest call that tells a bad
|
|
95
|
+
* project key from a good one. Capture cannot: ingestion is async, so it answers
|
|
96
|
+
* `{"status": 1}` whatever key it was given.
|
|
97
|
+
*/
|
|
98
|
+
function evaluateFlags(connection, distinctId) {
|
|
99
|
+
return callExternal({
|
|
100
|
+
system: "PostHog",
|
|
101
|
+
url: `${connection.host}/flags?v=2`,
|
|
102
|
+
method: "POST",
|
|
103
|
+
headers: { "content-type": "application/json" },
|
|
104
|
+
body: {
|
|
105
|
+
kind: "json",
|
|
106
|
+
value: {
|
|
107
|
+
api_key: connection.projectApiKey,
|
|
108
|
+
distinct_id: distinctId
|
|
109
|
+
}
|
|
110
|
+
},
|
|
111
|
+
schema: flagsResponseSchema,
|
|
112
|
+
safeToRepeat: true
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
export { resolvePostHogHost as a, readPostHogError as i, describePostHogFailure as n, evaluateFlags as r, captureEvent as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getErrorMessage } from "@wfgraph/core/plugin";
|
|
2
2
|
import { createClerkClient } from "@clerk/backend";
|
|
3
|
-
|
|
3
|
+
// src/clerk/client.ts
|
|
4
4
|
function createClerkBackendClient(secretKey) {
|
|
5
5
|
return createClerkClient({ secretKey });
|
|
6
6
|
}
|
|
@@ -30,5 +30,4 @@ function getClerkApiErrorMessage(error) {
|
|
|
30
30
|
}
|
|
31
31
|
return getErrorMessage(error);
|
|
32
32
|
}
|
|
33
|
-
//#endregion
|
|
34
33
|
export { getClerkApiErrorMessage as n, toClerkApiUser as r, createClerkBackendClient as t };
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { callExternal, parsePayload } from "@wfgraph/core/plugin";
|
|
2
2
|
import { Schema } from "effect";
|
|
3
|
-
|
|
3
|
+
// src/slack/client.ts
|
|
4
4
|
const SLACK_API_BASE = "https://slack.com/api";
|
|
5
5
|
/**
|
|
6
6
|
* Slack answers every call with this envelope, on success and failure alike.
|
|
@@ -54,5 +54,4 @@ function callSlack(token, method, schema, options = {}) {
|
|
|
54
54
|
safeToRepeat: options.safeToRepeat
|
|
55
55
|
});
|
|
56
56
|
}
|
|
57
|
-
//#endregion
|
|
58
57
|
export { describeSlackFailure as n, readSlackError as r, callSlack as t };
|
|
@@ -1,8 +1,8 @@
|
|
|
1
|
+
import { t as omitUndefined } from "./omit-undefined-CnDMZgmp.js";
|
|
1
2
|
import { callExternal, parsePayload } from "@wfgraph/core/plugin";
|
|
2
|
-
import { omitBy } from "es-toolkit/object";
|
|
3
|
-
import { isNil } from "es-toolkit/predicate";
|
|
4
3
|
import { Schema } from "effect";
|
|
5
|
-
|
|
4
|
+
import { Base64 } from "effect/encoding";
|
|
5
|
+
// src/twilio/client.ts
|
|
6
6
|
const TWILIO_API_BASE = "https://api.twilio.com/2010-04-01";
|
|
7
7
|
/** Twilio's error body, the same shape on every endpoint. */
|
|
8
8
|
const twilioErrorSchema = Schema.Struct({
|
|
@@ -36,7 +36,7 @@ function readTwilioError(payload) {
|
|
|
36
36
|
return parsePayload(payload, twilioErrorSchema);
|
|
37
37
|
}
|
|
38
38
|
function toBasicAuth(accountSid, authToken) {
|
|
39
|
-
return `Basic ${
|
|
39
|
+
return `Basic ${Base64.encode(`${accountSid}:${authToken}`)}`;
|
|
40
40
|
}
|
|
41
41
|
function requestTwilio(credentials, path, schema, init) {
|
|
42
42
|
return callExternal({
|
|
@@ -58,7 +58,7 @@ function requestTwilio(credentials, path, schema, init) {
|
|
|
58
58
|
*/
|
|
59
59
|
function createTwilioMessage(credentials, parameters) {
|
|
60
60
|
const { MediaUrl, ...scalars } = parameters;
|
|
61
|
-
const body = new URLSearchParams(
|
|
61
|
+
const body = new URLSearchParams(omitUndefined(scalars));
|
|
62
62
|
for (const mediaUrl of MediaUrl ?? []) body.append("MediaUrl", mediaUrl);
|
|
63
63
|
return requestTwilio(credentials, `/Accounts/${encodeURIComponent(credentials.accountSid)}/Messages.json`, twilioMessageSchema, {
|
|
64
64
|
method: "POST",
|
|
@@ -69,5 +69,4 @@ function createTwilioMessage(credentials, parameters) {
|
|
|
69
69
|
function fetchTwilioAccount(credentials) {
|
|
70
70
|
return requestTwilio(credentials, `/Accounts/${encodeURIComponent(credentials.accountSid)}.json`, Schema.Struct({ sid: Schema.String }), { method: "GET" });
|
|
71
71
|
}
|
|
72
|
-
//#endregion
|
|
73
72
|
export { readTwilioError as i, describeTwilioFailure as n, fetchTwilioAccount as r, createTwilioMessage as t };
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { callExternal, parsePayload } from "@wfgraph/core/plugin";
|
|
2
|
+
import { Effect, Schema } from "effect";
|
|
3
|
+
// src/resend/client.ts
|
|
4
|
+
const RESEND_API_BASE = "https://api.resend.com";
|
|
5
|
+
/** Resend's error body. `name` is the machine-readable slug. */
|
|
6
|
+
const resendErrorSchema = Schema.Struct({
|
|
7
|
+
statusCode: Schema.optionalKey(Schema.Finite),
|
|
8
|
+
name: Schema.optionalKey(Schema.String),
|
|
9
|
+
message: Schema.optionalKey(Schema.String)
|
|
10
|
+
});
|
|
11
|
+
const sentEmailSchema = Schema.Struct({ id: Schema.String });
|
|
12
|
+
/** The fields the Find Email action reads from Resend's retrieve response. */
|
|
13
|
+
const resendEmailSchema = Schema.Struct({
|
|
14
|
+
id: Schema.String,
|
|
15
|
+
message_id: Schema.String,
|
|
16
|
+
from: Schema.String,
|
|
17
|
+
to: Schema.Array(Schema.String),
|
|
18
|
+
cc: Schema.NullOr(Schema.Array(Schema.String)),
|
|
19
|
+
bcc: Schema.NullOr(Schema.Array(Schema.String)),
|
|
20
|
+
reply_to: Schema.NullOr(Schema.Array(Schema.String)),
|
|
21
|
+
subject: Schema.String,
|
|
22
|
+
html: Schema.NullOr(Schema.String),
|
|
23
|
+
text: Schema.NullOr(Schema.String),
|
|
24
|
+
created_at: Schema.DateFromString,
|
|
25
|
+
last_event: Schema.String,
|
|
26
|
+
scheduled_at: Schema.NullOr(Schema.DateFromString),
|
|
27
|
+
tags: Schema.optionalKey(Schema.Array(Schema.Struct({
|
|
28
|
+
name: Schema.String,
|
|
29
|
+
value: Schema.String
|
|
30
|
+
})))
|
|
31
|
+
});
|
|
32
|
+
/**
|
|
33
|
+
* Resend's error body, for a caller that reports more than the message.
|
|
34
|
+
*
|
|
35
|
+
* The connection test is that caller: a send-only key answers
|
|
36
|
+
* `restricted_api_key` on the domains endpoint, which confirms the key works,
|
|
37
|
+
* and only the slug says so.
|
|
38
|
+
*/
|
|
39
|
+
function readResendError(payload) {
|
|
40
|
+
return parsePayload(payload, resendErrorSchema);
|
|
41
|
+
}
|
|
42
|
+
function classifyResendFailure(error) {
|
|
43
|
+
if (error._tag === "ExternalUnreachable") return "unreachable";
|
|
44
|
+
const body = error._tag === "ExternalRejected" ? readResendError(error.payload) : void 0;
|
|
45
|
+
const status = body?.statusCode ?? error.status;
|
|
46
|
+
if (body?.name === "invalid_permission" && status === 403) return "insufficient_scope";
|
|
47
|
+
if (body?.name === "restricted_api_key") return status === 403 ? "key_unusable" : "send_only_key";
|
|
48
|
+
if (body?.name === "suspended_api_key" || body?.name === "email_above_quota") return "key_unusable";
|
|
49
|
+
return "refused";
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* What Resend said, in one sentence a person reads.
|
|
53
|
+
*
|
|
54
|
+
* A refusal carries Resend's own message when its error body is the documented
|
|
55
|
+
* shape and the bare status when it is not. A 2xx whose body is not the
|
|
56
|
+
* documented resource says so, because reporting success there would tell the
|
|
57
|
+
* run an email went out and leave nothing to look it up by.
|
|
58
|
+
*/
|
|
59
|
+
function describeResendFailure(error) {
|
|
60
|
+
if (error._tag === "ExternalUnreachable") return error.message;
|
|
61
|
+
if (error._tag === "ExternalUnreadable") return `Resend answered ${error.status} with an unrecognized body`;
|
|
62
|
+
return readResendError(error.payload)?.message ?? `HTTP ${error.status}`;
|
|
63
|
+
}
|
|
64
|
+
function requestResend(apiKey, path, schema, init) {
|
|
65
|
+
return callExternal({
|
|
66
|
+
system: "Resend",
|
|
67
|
+
url: `${RESEND_API_BASE}${path}`,
|
|
68
|
+
method: init.method,
|
|
69
|
+
headers: { authorization: `Bearer ${apiKey}` },
|
|
70
|
+
body: init.jsonBody === void 0 ? void 0 : {
|
|
71
|
+
kind: "json",
|
|
72
|
+
value: init.jsonBody
|
|
73
|
+
},
|
|
74
|
+
idempotencyKey: init.idempotencyKey,
|
|
75
|
+
schema
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Resend's template shapes, described as the wire sends them rather than as the
|
|
80
|
+
* SDK types them. Only what a caller reads is required; the rest is tolerant,
|
|
81
|
+
* because a field this plugin ignores must not fail the decode.
|
|
82
|
+
*/
|
|
83
|
+
const resendTemplateSummarySchema = Schema.Struct({
|
|
84
|
+
id: Schema.String,
|
|
85
|
+
name: Schema.String,
|
|
86
|
+
status: Schema.optionalKey(Schema.NullOr(Schema.String))
|
|
87
|
+
});
|
|
88
|
+
const resendTemplateListSchema = Schema.Struct({
|
|
89
|
+
data: Schema.Array(resendTemplateSummarySchema),
|
|
90
|
+
has_more: Schema.optionalKey(Schema.NullOr(Schema.Boolean))
|
|
91
|
+
});
|
|
92
|
+
const resendTemplateVariableSchema = Schema.Struct({
|
|
93
|
+
key: Schema.String,
|
|
94
|
+
type: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
95
|
+
fallback_value: Schema.optionalKey(Schema.NullOr(Schema.String))
|
|
96
|
+
});
|
|
97
|
+
/** Only the retrieve endpoint carries `variables`; the list endpoint does not. */
|
|
98
|
+
const resendTemplateSchema = Schema.Struct({
|
|
99
|
+
id: Schema.String,
|
|
100
|
+
name: Schema.String,
|
|
101
|
+
status: Schema.optionalKey(Schema.NullOr(Schema.String)),
|
|
102
|
+
variables: Schema.optionalKey(Schema.NullOr(Schema.Array(resendTemplateVariableSchema)))
|
|
103
|
+
});
|
|
104
|
+
/** Resend's own maximum, so a page never asks for more than it will answer. */
|
|
105
|
+
const TEMPLATE_PAGE_SIZE = 100;
|
|
106
|
+
/**
|
|
107
|
+
* How many pages a listing follows before it gives up.
|
|
108
|
+
*
|
|
109
|
+
* Each page carries `callExternal`'s own per-attempt timeout and retry schedule,
|
|
110
|
+
* so a page is worth tens of seconds in the worst case and this is the number a
|
|
111
|
+
* config panel can wait on. Reaching the bound is reported rather than passed
|
|
112
|
+
* off as the whole list: a dropdown quietly missing the template someone wants,
|
|
113
|
+
* with no way to type it either, is worse than saying the list is too long.
|
|
114
|
+
*/
|
|
115
|
+
const TEMPLATE_PAGE_LIMIT = 3;
|
|
116
|
+
function listResendTemplatePage(apiKey, after) {
|
|
117
|
+
const query = new URLSearchParams({ limit: String(TEMPLATE_PAGE_SIZE) });
|
|
118
|
+
if (after) query.set("after", after);
|
|
119
|
+
return requestResend(apiKey, `/templates?${query.toString()}`, resendTemplateListSchema, { method: "GET" });
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Every template the account holds, or a refusal when there are more than the
|
|
123
|
+
* bound above will fetch.
|
|
124
|
+
*/
|
|
125
|
+
function listResendTemplates(apiKey) {
|
|
126
|
+
return Effect.gen(function* () {
|
|
127
|
+
const templates = [];
|
|
128
|
+
let after;
|
|
129
|
+
for (let page = 0; page < TEMPLATE_PAGE_LIMIT; page += 1) {
|
|
130
|
+
const answer = yield* listResendTemplatePage(apiKey, after);
|
|
131
|
+
templates.push(...answer.data);
|
|
132
|
+
const last = answer.data.at(-1);
|
|
133
|
+
if (!answer.has_more || !last) return {
|
|
134
|
+
templates,
|
|
135
|
+
reachedPageLimit: false
|
|
136
|
+
};
|
|
137
|
+
after = last.id;
|
|
138
|
+
}
|
|
139
|
+
return {
|
|
140
|
+
templates,
|
|
141
|
+
reachedPageLimit: true
|
|
142
|
+
};
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
/** One template, with the variables only this endpoint carries. */
|
|
146
|
+
function getResendTemplate(apiKey, idOrAlias) {
|
|
147
|
+
return requestResend(apiKey, `/templates/${encodeURIComponent(idOrAlias)}`, resendTemplateSchema, { method: "GET" });
|
|
148
|
+
}
|
|
149
|
+
function sendResendEmail(apiKey, payload, idempotencyKey) {
|
|
150
|
+
return requestResend(apiKey, "/emails", sentEmailSchema, {
|
|
151
|
+
method: "POST",
|
|
152
|
+
jsonBody: payload,
|
|
153
|
+
idempotencyKey
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
/** Retrieve one sent email by its Resend email ID. */
|
|
157
|
+
function getResendEmail(apiKey, emailId) {
|
|
158
|
+
return requestResend(apiKey, `/emails/${encodeURIComponent(emailId)}`, resendEmailSchema, { method: "GET" });
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Listing domains is a read-only call any valid key can make, which makes it the
|
|
162
|
+
* check that says whether a key works without sending anything.
|
|
163
|
+
*/
|
|
164
|
+
function listResendDomains(apiKey) {
|
|
165
|
+
return requestResend(apiKey, "/domains", Schema.Unknown, { method: "GET" });
|
|
166
|
+
}
|
|
167
|
+
export { listResendDomains as a, sendResendEmail as c, getResendTemplate as i, describeResendFailure as n, listResendTemplates as o, getResendEmail as r, readResendError as s, classifyResendFailure as t };
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { i as getResendTemplate, n as describeResendFailure, o as listResendTemplates, t as classifyResendFailure } from "./client-D3ALurAR.js";
|
|
2
|
+
import { t as omitUndefined } from "./omit-undefined-CnDMZgmp.js";
|
|
3
|
+
import { callExternalAsync } from "@wfgraph/core/plugin";
|
|
4
|
+
// src/resend/config-options.ts
|
|
5
|
+
/**
|
|
6
|
+
* What the Send Email node's template fields are filled with.
|
|
7
|
+
*
|
|
8
|
+
* A Promise at its edge for the reason `test.ts` is one: core calls these over a
|
|
9
|
+
* Promise, so this is where the effect is run and the transport provided.
|
|
10
|
+
*
|
|
11
|
+
* Which Resend refusal means what is `client.ts`'s to say, because the
|
|
12
|
+
* connection test asks the same question of the same account and the two must
|
|
13
|
+
* not drift.
|
|
14
|
+
*/
|
|
15
|
+
/** Resend's own maximum, so a template can declare no more inputs than it has. */
|
|
16
|
+
const MAX_TEMPLATE_VARIABLES = 50;
|
|
17
|
+
const NO_API_KEY = {
|
|
18
|
+
status: "unavailable",
|
|
19
|
+
reason: "not_permitted",
|
|
20
|
+
message: "This Resend connection has no API key. Reconnect it."
|
|
21
|
+
};
|
|
22
|
+
/**
|
|
23
|
+
* What a refusal means for this field.
|
|
24
|
+
*
|
|
25
|
+
* `not_permitted` is the only reason the editor offers as something to act on,
|
|
26
|
+
* so it is spent on the two cases where acting on the connection is the fix.
|
|
27
|
+
*/
|
|
28
|
+
function describeUnavailable(failure) {
|
|
29
|
+
switch (classifyResendFailure(failure)) {
|
|
30
|
+
case "unreachable": return {
|
|
31
|
+
status: "unavailable",
|
|
32
|
+
reason: "unreachable",
|
|
33
|
+
message: describeResendFailure(failure)
|
|
34
|
+
};
|
|
35
|
+
case "send_only_key":
|
|
36
|
+
case "insufficient_scope": return {
|
|
37
|
+
status: "unavailable",
|
|
38
|
+
reason: "not_permitted",
|
|
39
|
+
message: "This Resend connection can only send email. Reconnect it and choose full access to pick a template."
|
|
40
|
+
};
|
|
41
|
+
case "key_unusable": return {
|
|
42
|
+
status: "unavailable",
|
|
43
|
+
reason: "not_permitted",
|
|
44
|
+
message: `This Resend connection is no longer usable: ${describeResendFailure(failure)}`
|
|
45
|
+
};
|
|
46
|
+
default: return {
|
|
47
|
+
status: "unavailable",
|
|
48
|
+
reason: "refused",
|
|
49
|
+
message: describeResendFailure(failure)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** Every template the account holds, drafts included and labelled as such. */
|
|
54
|
+
async function resendTemplateOptions(credentials) {
|
|
55
|
+
const apiKey = credentials.RESEND_API_KEY;
|
|
56
|
+
if (!apiKey) return NO_API_KEY;
|
|
57
|
+
const result = await callExternalAsync(listResendTemplates(apiKey), (error) => error);
|
|
58
|
+
if (!result.ok) return describeUnavailable(result.failure);
|
|
59
|
+
if (result.data.reachedPageLimit) return {
|
|
60
|
+
status: "unavailable",
|
|
61
|
+
reason: "refused",
|
|
62
|
+
message: "This account holds more Resend templates than this list can load. Enter the template id instead."
|
|
63
|
+
};
|
|
64
|
+
return {
|
|
65
|
+
status: "options",
|
|
66
|
+
options: result.data.templates.map((template) => ({
|
|
67
|
+
value: template.id,
|
|
68
|
+
label: template.status === "draft" ? `${template.name} (draft)` : template.name
|
|
69
|
+
}))
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* One input per variable the chosen template declares.
|
|
74
|
+
*
|
|
75
|
+
* The id is sent as the builder gave it. Resend's retrieve endpoint takes an
|
|
76
|
+
* alias as readily as an id, `encodeURIComponent` is what makes the path safe,
|
|
77
|
+
* and Resend's own "Template not found" is a better sentence than a shape check
|
|
78
|
+
* that would refuse the aliases people actually write.
|
|
79
|
+
*/
|
|
80
|
+
async function resendTemplateVariableFields(credentials, request) {
|
|
81
|
+
const apiKey = credentials.RESEND_API_KEY;
|
|
82
|
+
const templateId = request.parameters.emailTemplateId;
|
|
83
|
+
if (!templateId) return {
|
|
84
|
+
status: "fields",
|
|
85
|
+
fields: []
|
|
86
|
+
};
|
|
87
|
+
if (!apiKey) return NO_API_KEY;
|
|
88
|
+
const result = await callExternalAsync(getResendTemplate(apiKey, templateId), (error) => error);
|
|
89
|
+
if (!result.ok) return describeUnavailable(result.failure);
|
|
90
|
+
return {
|
|
91
|
+
status: "fields",
|
|
92
|
+
fields: (result.data.variables ?? []).slice(0, MAX_TEMPLATE_VARIABLES).map((variable) => omitUndefined({
|
|
93
|
+
key: variable.key,
|
|
94
|
+
label: variable.key,
|
|
95
|
+
required: variable.fallback_value == null ? true : void 0,
|
|
96
|
+
defaultValue: variable.fallback_value ?? void 0,
|
|
97
|
+
type: variable.type === "number" ? "number" : void 0
|
|
98
|
+
}))
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
export { resendTemplateOptions, resendTemplateVariableFields };
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { getErrorMessage } from "@wfgraph/core/plugin";
|
|
2
2
|
import { Option, Schema } from "effect";
|
|
3
3
|
import { LinearError, parseLinearError } from "@linear/sdk";
|
|
4
|
-
|
|
4
|
+
// src/linear/errors.ts
|
|
5
5
|
/**
|
|
6
6
|
* The error payload Linear's `parseLinearError` reads: the GraphQL request that
|
|
7
7
|
* failed, the response that came back with its status and GraphQL errors, and a
|
|
@@ -12,38 +12,46 @@ import { LinearError, parseLinearError } from "@linear/sdk";
|
|
|
12
12
|
* (`response.data`, `response.headers`) and the validated value is handed on to
|
|
13
13
|
* Linear whole. The named fields are the ones Linear reads unguarded: a `message`
|
|
14
14
|
* that is not a string, or a `response.errors` that is not a list of objects,
|
|
15
|
-
* makes Linear's own parsing throw.
|
|
16
|
-
*
|
|
17
|
-
* back; the default strips them.
|
|
15
|
+
* makes Linear's own parsing throw. Each object uses `Schema.StructWithRest` so
|
|
16
|
+
* the unnamed fields survive at every level; a closed struct would strip them.
|
|
18
17
|
*
|
|
19
18
|
* Fields are described as the API sends them. Linear's LinearErrorRaw types both
|
|
20
19
|
* the per-error `message` and `extensions.type` as its LinearErrorType enum, while
|
|
21
20
|
* the wire carries readable text ("Authentication required") and a lowercase
|
|
22
21
|
* phrase ("authentication error") that Linear maps back to the enum itself.
|
|
23
22
|
*/
|
|
24
|
-
const
|
|
23
|
+
const unknownFields = [Schema.Record(Schema.String, Schema.Unknown)];
|
|
24
|
+
const linearErrorExtensionSchema = Schema.StructWithRest(Schema.Struct({
|
|
25
|
+
type: Schema.optionalKey(Schema.String),
|
|
26
|
+
userError: Schema.optionalKey(Schema.Boolean),
|
|
27
|
+
userPresentableMessage: Schema.optionalKey(Schema.String)
|
|
28
|
+
}), unknownFields);
|
|
29
|
+
const linearGraphqlErrorSchema = Schema.StructWithRest(Schema.Struct({
|
|
25
30
|
message: Schema.optionalKey(Schema.String),
|
|
26
31
|
path: Schema.optionalKey(Schema.Array(Schema.String)),
|
|
27
|
-
extensions: Schema.optionalKey(
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
});
|
|
33
|
-
const
|
|
32
|
+
extensions: Schema.optionalKey(linearErrorExtensionSchema)
|
|
33
|
+
}), unknownFields);
|
|
34
|
+
const linearRequestSchema = Schema.StructWithRest(Schema.Struct({
|
|
35
|
+
query: Schema.optionalKey(Schema.String),
|
|
36
|
+
variables: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown))
|
|
37
|
+
}), unknownFields);
|
|
38
|
+
const linearResponseSchema = Schema.StructWithRest(Schema.Struct({
|
|
39
|
+
status: Schema.optionalKey(Schema.Finite),
|
|
40
|
+
error: Schema.optionalKey(Schema.String),
|
|
41
|
+
errors: Schema.optionalKey(Schema.Array(linearGraphqlErrorSchema))
|
|
42
|
+
}), unknownFields);
|
|
43
|
+
const linearErrorRawSchema = Schema.StructWithRest(Schema.Struct({
|
|
34
44
|
name: Schema.optionalKey(Schema.String),
|
|
35
45
|
message: Schema.optionalKey(Schema.String),
|
|
36
|
-
request: Schema.optionalKey(
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
});
|
|
46
|
-
const readLinearErrorRaw = Schema.decodeUnknownOption(linearErrorRawSchema, { onExcessProperty: "preserve" });
|
|
46
|
+
request: Schema.optionalKey(linearRequestSchema),
|
|
47
|
+
response: Schema.optionalKey(linearResponseSchema)
|
|
48
|
+
}), unknownFields);
|
|
49
|
+
const decodeLinearErrorRaw = Schema.decodeUnknownOption(linearErrorRawSchema);
|
|
50
|
+
function readLinearErrorRaw(input) {
|
|
51
|
+
if (typeof input !== "object" || input === null) return decodeLinearErrorRaw(input);
|
|
52
|
+
const ownProperties = Object.fromEntries(Object.getOwnPropertyNames(input).map((key) => [key, Reflect.get(input, key)]));
|
|
53
|
+
return decodeLinearErrorRaw(ownProperties);
|
|
54
|
+
}
|
|
47
55
|
/**
|
|
48
56
|
* Normalizes anything thrown while talking to Linear into a LinearError, which is
|
|
49
57
|
* what the steps read an error type and a message from. The SDK wraps its own
|
|
@@ -73,5 +81,4 @@ function describeLinearFailure(error) {
|
|
|
73
81
|
const linearError = toLinearError(error);
|
|
74
82
|
return linearError.errors?.[0]?.message || linearError.message || getErrorMessage(error);
|
|
75
83
|
}
|
|
76
|
-
//#endregion
|
|
77
84
|
export { toLinearError as n, describeLinearFailure as t };
|