@wfgraph/plugins 3.1.0 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  # @wfgraph/plugins
2
2
 
3
- The five built-in integrations for
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: { integrations: builtInIntegrations /* ...events and actions */ },
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
- Each integration is also exported by name, for a host that wants some of the five rather than
25
- all of them. That narrows what reaches `createWfGraphApp` rather than what the process loads,
26
- since this entry imports all five as values.
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
- //#region src/clerk/client.ts
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
- //#region src/slack/client.ts
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
- //#region src/twilio/client.ts
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 ${Buffer.from(`${accountSid}:${authToken}`).toString("base64")}`;
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(omitBy(scalars, isNil));
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
- //#region src/linear/errors.ts
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. `onExcessProperty: "preserve"` in the decode
16
- * below is what keeps the unnamed fields, at every level, on the value that comes
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 linearGraphqlErrorSchema = Schema.Struct({
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(Schema.Struct({
28
- type: Schema.optionalKey(Schema.String),
29
- userError: Schema.optionalKey(Schema.Boolean),
30
- userPresentableMessage: Schema.optionalKey(Schema.String)
31
- }))
32
- });
33
- const linearErrorRawSchema = Schema.Struct({
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(Schema.Struct({
37
- query: Schema.optionalKey(Schema.String),
38
- variables: Schema.optionalKey(Schema.Record(Schema.String, Schema.Unknown))
39
- })),
40
- response: Schema.optionalKey(Schema.Struct({
41
- status: Schema.optionalKey(Schema.Finite),
42
- error: Schema.optionalKey(Schema.String),
43
- errors: Schema.optionalKey(Schema.Array(linearGraphqlErrorSchema))
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 };