@agnocon/piece-buttondown 0.0.5

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.
Files changed (67) hide show
  1. package/LICENSE.MIT-AP +24 -0
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +31 -0
  5. package/dist/index.js.map +1 -0
  6. package/dist/lib/actions/create-subscriber.d.ts +17 -0
  7. package/dist/lib/actions/create-subscriber.d.ts.map +1 -0
  8. package/dist/lib/actions/create-subscriber.js +155 -0
  9. package/dist/lib/actions/create-subscriber.js.map +1 -0
  10. package/dist/lib/actions/custom-api-call.d.ts +13 -0
  11. package/dist/lib/actions/custom-api-call.d.ts.map +1 -0
  12. package/dist/lib/actions/custom-api-call.js +19 -0
  13. package/dist/lib/actions/custom-api-call.js.map +1 -0
  14. package/dist/lib/actions/list-subscribers.d.ts +11 -0
  15. package/dist/lib/actions/list-subscribers.d.ts.map +1 -0
  16. package/dist/lib/actions/list-subscribers.js +115 -0
  17. package/dist/lib/actions/list-subscribers.js.map +1 -0
  18. package/dist/lib/actions/send-email.d.ts +16 -0
  19. package/dist/lib/actions/send-email.d.ts.map +1 -0
  20. package/dist/lib/actions/send-email.js +147 -0
  21. package/dist/lib/actions/send-email.js.map +1 -0
  22. package/dist/lib/common/auth.d.ts +2 -0
  23. package/dist/lib/common/auth.d.ts.map +1 -0
  24. package/dist/lib/common/auth.js +46 -0
  25. package/dist/lib/common/auth.js.map +1 -0
  26. package/dist/lib/common/client.d.ts +20 -0
  27. package/dist/lib/common/client.d.ts.map +1 -0
  28. package/dist/lib/common/client.js +41 -0
  29. package/dist/lib/common/client.js.map +1 -0
  30. package/dist/lib/common/options.d.ts +22 -0
  31. package/dist/lib/common/options.d.ts.map +1 -0
  32. package/dist/lib/common/options.js +58 -0
  33. package/dist/lib/common/options.js.map +1 -0
  34. package/dist/lib/common/types.d.ts +81 -0
  35. package/dist/lib/common/types.d.ts.map +1 -0
  36. package/dist/lib/common/types.js +3 -0
  37. package/dist/lib/common/types.js.map +1 -0
  38. package/dist/lib/common/webhook.d.ts +36 -0
  39. package/dist/lib/common/webhook.d.ts.map +1 -0
  40. package/dist/lib/common/webhook.js +80 -0
  41. package/dist/lib/common/webhook.js.map +1 -0
  42. package/dist/lib/triggers/email-sent.d.ts +14 -0
  43. package/dist/lib/triggers/email-sent.d.ts.map +1 -0
  44. package/dist/lib/triggers/email-sent.js +43 -0
  45. package/dist/lib/triggers/email-sent.js.map +1 -0
  46. package/dist/lib/triggers/new-subscriber.d.ts +14 -0
  47. package/dist/lib/triggers/new-subscriber.d.ts.map +1 -0
  48. package/dist/lib/triggers/new-subscriber.js +43 -0
  49. package/dist/lib/triggers/new-subscriber.js.map +1 -0
  50. package/dist/lib/triggers/subscriber-confirmed.d.ts +14 -0
  51. package/dist/lib/triggers/subscriber-confirmed.d.ts.map +1 -0
  52. package/dist/lib/triggers/subscriber-confirmed.js +43 -0
  53. package/dist/lib/triggers/subscriber-confirmed.js.map +1 -0
  54. package/package.json +46 -0
  55. package/src/index.ts +26 -0
  56. package/src/lib/actions/create-subscriber.ts +171 -0
  57. package/src/lib/actions/custom-api-call.ts +11 -0
  58. package/src/lib/actions/list-subscribers.ts +125 -0
  59. package/src/lib/actions/send-email.ts +162 -0
  60. package/src/lib/common/auth.ts +58 -0
  61. package/src/lib/common/client.ts +83 -0
  62. package/src/lib/common/options.ts +65 -0
  63. package/src/lib/common/types.ts +141 -0
  64. package/src/lib/common/webhook.ts +105 -0
  65. package/src/lib/triggers/email-sent.ts +39 -0
  66. package/src/lib/triggers/new-subscriber.ts +39 -0
  67. package/src/lib/triggers/subscriber-confirmed.ts +39 -0
@@ -0,0 +1,43 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.buttondownSubscriberConfirmed = void 0;
4
+ const tslib_1 = require("tslib");
5
+ // Vendored from Activepieces packages/pieces/community/buttondown/src/lib/triggers/subscriber-confirmed.ts (MIT).
6
+ // Generated by packages/scripts/vendor-ap.mjs - do not edit by hand.
7
+ const pieces_common_1 = require("@agnocon/pieces-common");
8
+ const client_1 = require("../common/client");
9
+ const webhook_1 = require("../common/webhook");
10
+ exports.buttondownSubscriberConfirmed = (0, webhook_1.createButtondownWebhookTrigger)({
11
+ name: 'buttondown_subscriber_confirmed',
12
+ displayName: 'Subscriber Confirmed',
13
+ description: 'Triggers when a subscriber confirms their email.',
14
+ aiMetadata: {
15
+ description: 'Fires when a subscriber completes double opt-in and confirms their email address in Buttondown. Represents a subscriber transitioning to confirmed status; the payload is enriched with the full subscriber details.',
16
+ },
17
+ eventType: 'subscriber.confirmed',
18
+ sampleData: {
19
+ event_type: 'subscriber.confirmed',
20
+ data: {
21
+ subscriber: 'sub_123456789',
22
+ },
23
+ },
24
+ enrich: (_a) => tslib_1.__awaiter(void 0, [_a], void 0, function* ({ apiKey, payload }) {
25
+ var _b;
26
+ const subscriberData = (_b = payload.data) === null || _b === void 0 ? void 0 : _b['subscriber'];
27
+ let subscriber;
28
+ if (typeof subscriberData === 'string') {
29
+ subscriber = yield (0, client_1.buttondownRequest)({
30
+ auth: apiKey,
31
+ method: pieces_common_1.HttpMethod.GET,
32
+ path: `/subscribers/${encodeURIComponent(subscriberData)}`,
33
+ });
34
+ }
35
+ else if (subscriberData && typeof subscriberData === 'object') {
36
+ subscriber = subscriberData;
37
+ }
38
+ return {
39
+ subscriber,
40
+ };
41
+ }),
42
+ });
43
+ //# sourceMappingURL=subscriber-confirmed.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"subscriber-confirmed.js","sourceRoot":"","sources":["../../../src/lib/triggers/subscriber-confirmed.ts"],"names":[],"mappings":";;;;AAAA,kHAAkH;AAClH,qEAAqE;AACrE,0DAAoD;AACpD,6CAAqD;AACrD,+CAAmE;AAGtD,QAAA,6BAA6B,GAAG,IAAA,wCAA8B,EAAC;IAC1E,IAAI,EAAE,iCAAiC;IACvC,WAAW,EAAE,sBAAsB;IACnC,WAAW,EAAE,kDAAkD;IAC/D,UAAU,EAAE;QACV,WAAW,EACT,sNAAsN;KACzN;IACD,SAAS,EAAE,sBAAsB;IACjC,UAAU,EAAE;QACV,UAAU,EAAE,sBAAsB;QAClC,IAAI,EAAE;YACJ,UAAU,EAAE,eAAe;SAC5B;KACF;IACD,MAAM,EAAE,KAA4B,EAAE,oDAAvB,EAAE,MAAM,EAAE,OAAO,EAAE;;QAChC,MAAM,cAAc,GAAG,MAAA,OAAO,CAAC,IAAI,0CAAG,YAAY,CAAC,CAAC;QACpD,IAAI,UAA4C,CAAC;QAEjD,IAAI,OAAO,cAAc,KAAK,QAAQ,EAAE,CAAC;YACvC,UAAU,GAAG,MAAM,IAAA,0BAAiB,EAAuB;gBACzD,IAAI,EAAE,MAAM;gBACZ,MAAM,EAAE,0BAAU,CAAC,GAAG;gBACtB,IAAI,EAAE,gBAAgB,kBAAkB,CAAC,cAAc,CAAC,EAAE;aAC3D,CAAC,CAAC;QACL,CAAC;aAAM,IAAI,cAAc,IAAI,OAAO,cAAc,KAAK,QAAQ,EAAE,CAAC;YAChE,UAAU,GAAG,cAAsC,CAAC;QACtD,CAAC;QAED,OAAO;YACL,UAAU;SACX,CAAC;IACJ,CAAC,CAAA;CACF,CAAC,CAAC"}
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@agnocon/piece-buttondown",
3
+ "version": "0.0.5",
4
+ "description": "AgnoCon buttondown connector",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./dist/index.d.ts",
10
+ "require": "./dist/index.js",
11
+ "default": "./dist/index.js"
12
+ }
13
+ },
14
+ "files": [
15
+ "dist",
16
+ "src",
17
+ "LICENSE.MIT-AP"
18
+ ],
19
+ "scripts": {
20
+ "build": "tsc -p tsconfig.json",
21
+ "vendor": "node ../scripts/vendor-ap.mjs piece-buttondown"
22
+ },
23
+ "dependencies": {
24
+ "@agnocon/core-utils": "0.2.0",
25
+ "@agnocon/piece-types": "0.3.1",
26
+ "@agnocon/pieces-common": "0.12.8",
27
+ "@agnocon/pieces-framework": "0.36.0",
28
+ "tslib": "2.6.2"
29
+ },
30
+ "devDependencies": {
31
+ "@types/node": "20.17.6",
32
+ "typescript": "5.5.4"
33
+ },
34
+ "license": "MIT",
35
+ "agnocon": {
36
+ "originVersion": "0.0.5"
37
+ },
38
+ "publishConfig": {
39
+ "access": "restricted",
40
+ "tag": "latest"
41
+ },
42
+ "keywords": [
43
+ "agnocon",
44
+ "connector"
45
+ ]
46
+ }
package/src/index.ts ADDED
@@ -0,0 +1,26 @@
1
+ import { createPiece } from '@agnocon/pieces-framework';
2
+ import { PieceCategory } from '@agnocon/pieces-framework';
3
+ import { buttondownAuth } from './lib/common/auth';
4
+ import { createSubscriber } from './lib/actions/create-subscriber';
5
+ import { listSubscribers } from './lib/actions/list-subscribers';
6
+ import { sendEmail } from './lib/actions/send-email';
7
+ import { customApiCall } from './lib/actions/custom-api-call';
8
+ import { buttondownNewSubscriber } from './lib/triggers/new-subscriber';
9
+ import { buttondownSubscriberConfirmed } from './lib/triggers/subscriber-confirmed';
10
+ import { buttondownEmailSent } from './lib/triggers/email-sent';
11
+
12
+ export const buttondown = createPiece({
13
+ displayName: 'Buttondown',
14
+ description: 'Automate your Buttondown newsletter workflows.',
15
+ auth: buttondownAuth,
16
+ minimumSupportedRelease: '0.36.1',
17
+ logoUrl: 'https://cdn.activepieces.com/pieces/buttondown.png',
18
+ authors: ['jacksbox-cassandra'],
19
+ categories: [PieceCategory.MARKETING, PieceCategory.COMMUNICATION],
20
+ actions: [createSubscriber, listSubscribers, sendEmail, customApiCall],
21
+ triggers: [
22
+ buttondownNewSubscriber,
23
+ buttondownSubscriberConfirmed,
24
+ buttondownEmailSent,
25
+ ],
26
+ });
@@ -0,0 +1,171 @@
1
+ import { HttpMethod } from '@agnocon/pieces-common';
2
+ import { createAction, Property } from '@agnocon/pieces-framework';
3
+ import { buttondownAuth } from '../common/auth';
4
+ import { buttondownRequest } from '../common/client';
5
+ import { subscriberTypeOptions } from '../common/options';
6
+ import {
7
+ ButtondownSubscriber,
8
+ ButtondownSubscriberInput,
9
+ ButtondownSubscriberType,
10
+ } from '../common/types';
11
+
12
+ export const createSubscriber = createAction({
13
+ auth: buttondownAuth,
14
+ name: 'createSubscriber',
15
+ displayName: 'Create Subscriber',
16
+ description: 'Create a new subscriber in Buttondown.',
17
+ audience: 'both',
18
+ aiMetadata: {
19
+ description:
20
+ 'Adds a subscriber to a Buttondown newsletter by email address, with optional tags, metadata, UTM attribution, and referral data. Use "regular" subscriber type to skip double opt-in. Not idempotent: each call creates a subscriber; control duplicate-email handling via collision behavior (default keeps the existing subscriber, or overwrite/merge tags).',
21
+ idempotent: false,
22
+ },
23
+ props: {
24
+ email: Property.ShortText({
25
+ displayName: 'Email Address',
26
+ required: true,
27
+ description: 'The email address of the subscriber.',
28
+ }),
29
+ type: Property.StaticDropdown({
30
+ displayName: 'Subscriber Type',
31
+ description:
32
+ 'Set the subscriber status. Use "regular" to bypass double opt-in.',
33
+ required: false,
34
+ options: {
35
+ options: subscriberTypeOptions,
36
+ },
37
+ }),
38
+ notes: Property.LongText({
39
+ displayName: 'Internal Notes',
40
+ required: false,
41
+ }),
42
+ tags: Property.Array({
43
+ displayName: 'Tags',
44
+ description: 'Add one or more tags to assign to the subscriber.',
45
+ required: false,
46
+ properties: {
47
+ tag: Property.ShortText({
48
+ displayName: 'Tag',
49
+ required: true,
50
+ }),
51
+ },
52
+ }),
53
+ metadata: Property.Json({
54
+ displayName: 'Metadata',
55
+ description: 'Custom metadata to store on the subscriber (JSON object).',
56
+ required: false,
57
+ }),
58
+ referrerUrl: Property.ShortText({
59
+ displayName: 'Referrer URL',
60
+ required: false,
61
+ }),
62
+ utmSource: Property.ShortText({
63
+ displayName: 'UTM Source',
64
+ required: false,
65
+ }),
66
+ utmMedium: Property.ShortText({
67
+ displayName: 'UTM Medium',
68
+ required: false,
69
+ }),
70
+ utmCampaign: Property.ShortText({
71
+ displayName: 'UTM Campaign',
72
+ required: false,
73
+ }),
74
+ referringSubscriberId: Property.ShortText({
75
+ displayName: 'Referring Subscriber ID',
76
+ description: 'ID of the subscriber who referred this contact.',
77
+ required: false,
78
+ }),
79
+ ipAddress: Property.ShortText({
80
+ displayName: 'IP Address',
81
+ description:
82
+ 'The original IP address of the subscriber. Helps Buttondown evaluate spam.',
83
+ required: false,
84
+ }),
85
+ collisionBehavior: Property.StaticDropdown({
86
+ displayName: 'Collision Behavior',
87
+ description:
88
+ 'Control how Buttondown handles duplicate email addresses. Leave empty to keep the existing subscriber.',
89
+ required: false,
90
+ options: {
91
+ options: [
92
+ { label: 'Overwrite existing subscriber', value: 'overwrite' },
93
+ { label: 'Merge tags with existing subscriber', value: 'add' },
94
+ ],
95
+ },
96
+ }),
97
+ bypassFirewall: Property.Checkbox({
98
+ displayName: 'Bypass Firewall',
99
+ description:
100
+ 'Set to true when the request originates from a trusted internal system without the subscriber\'s IP address.',
101
+ required: false,
102
+ defaultValue: false,
103
+ }),
104
+ },
105
+ async run({ auth, propsValue }) {
106
+ const payload: ButtondownSubscriberInput = {
107
+ email_address: propsValue.email,
108
+ };
109
+
110
+ if (propsValue.notes) {
111
+ payload.notes = propsValue.notes;
112
+ }
113
+
114
+ const tags = (propsValue.tags as Array<{ tag?: string }> | undefined)
115
+ ?.map((entry) => entry?.tag?.trim())
116
+ .filter((tag): tag is string => !!tag);
117
+ if (tags && tags.length > 0) {
118
+ payload.tags = tags;
119
+ }
120
+
121
+ if (propsValue.metadata) {
122
+ if (typeof propsValue.metadata !== 'object' || Array.isArray(propsValue.metadata)) {
123
+ throw new Error('Metadata must be a JSON object.');
124
+ }
125
+ payload.metadata = propsValue.metadata as Record<string, unknown>;
126
+ }
127
+
128
+ if (propsValue.referrerUrl) {
129
+ payload.referrer_url = propsValue.referrerUrl;
130
+ }
131
+
132
+ if (propsValue.utmSource) {
133
+ payload.utm_source = propsValue.utmSource;
134
+ }
135
+
136
+ if (propsValue.utmMedium) {
137
+ payload.utm_medium = propsValue.utmMedium;
138
+ }
139
+
140
+ if (propsValue.utmCampaign) {
141
+ payload.utm_campaign = propsValue.utmCampaign;
142
+ }
143
+
144
+ if (propsValue.referringSubscriberId) {
145
+ payload.referring_subscriber_id = propsValue.referringSubscriberId;
146
+ }
147
+
148
+ if (propsValue.type) {
149
+ payload.type = propsValue.type as ButtondownSubscriberType;
150
+ }
151
+
152
+ if (propsValue.ipAddress) {
153
+ payload.ip_address = propsValue.ipAddress;
154
+ }
155
+
156
+ const headers: Record<string, string | undefined> = {
157
+ 'X-Buttondown-Collision-Behavior': propsValue.collisionBehavior as string | undefined,
158
+ 'X-Buttondown-Bypass-Firewall': propsValue.bypassFirewall ? 'true' : undefined,
159
+ };
160
+
161
+ const subscriber = await buttondownRequest<ButtondownSubscriber>({
162
+ auth: auth.secret_text,
163
+ method: HttpMethod.POST,
164
+ path: '/subscribers',
165
+ body: payload,
166
+ headers,
167
+ });
168
+
169
+ return subscriber;
170
+ },
171
+ });
@@ -0,0 +1,11 @@
1
+ import { createCustomApiCallAction } from '@agnocon/pieces-common';
2
+ import { buttondownAuth } from '../common/auth';
3
+ import { BUTTONDOWN_BASE_URL } from '../common/client';
4
+
5
+ export const customApiCall = createCustomApiCallAction({
6
+ auth: buttondownAuth,
7
+ baseUrl: () => BUTTONDOWN_BASE_URL,
8
+ authMapping: async (auth) => ({
9
+ Authorization: `Token ${auth.secret_text}`,
10
+ }),
11
+ });
@@ -0,0 +1,125 @@
1
+ import { HttpMethod } from '@agnocon/pieces-common';
2
+ import { createAction, Property } from '@agnocon/pieces-framework';
3
+ import { buttondownAuth } from '../common/auth';
4
+ import { buttondownRequest, ButtondownPagedResponse } from '../common/client';
5
+ import {
6
+ subscriberTypeOptions,
7
+ subscriberSourceOptions,
8
+ subscriberOrderingOptions,
9
+ } from '../common/options';
10
+ import { ButtondownSubscriber } from '../common/types';
11
+
12
+ export const listSubscribers = createAction({
13
+ auth: buttondownAuth,
14
+ name: 'listSubscribers',
15
+ displayName: 'List Subscribers',
16
+ description: 'Retrieve subscribers from Buttondown.',
17
+ audience: 'both',
18
+ aiMetadata: {
19
+ description:
20
+ 'Lists subscribers from a Buttondown newsletter, optionally filtered by type, source, tags, email substring, or referrer URL; with no filters it returns all subscribers. Use to look up or enumerate subscribers. Read-only and idempotent. Returns one page (default 100); pass the returned cursor to page through results.',
21
+ idempotent: true,
22
+ },
23
+ props: {
24
+ limit: Property.Number({
25
+ displayName: 'Page Size',
26
+ description: 'Maximum number of subscribers to return (1-1000).',
27
+ required: false,
28
+ defaultValue: 100,
29
+ }),
30
+ cursor: Property.ShortText({
31
+ displayName: 'Cursor',
32
+ description:
33
+ 'Provide the cursor value from a previous response to fetch the next page.',
34
+ required: false,
35
+ }),
36
+ type: Property.StaticDropdown({
37
+ displayName: 'Subscriber Type',
38
+ required: false,
39
+ options: {
40
+ options: subscriberTypeOptions,
41
+ },
42
+ }),
43
+ source: Property.StaticDropdown({
44
+ displayName: 'Source',
45
+ required: false,
46
+ options: {
47
+ options: subscriberSourceOptions,
48
+ },
49
+ }),
50
+ tags: Property.Array({
51
+ displayName: 'Tags',
52
+ description: 'Filter by subscribers who have at least one of these tags.',
53
+ required: false,
54
+ properties: {
55
+ tag: Property.ShortText({
56
+ displayName: 'Tag',
57
+ required: true,
58
+ }),
59
+ },
60
+ }),
61
+ emailAddress: Property.ShortText({
62
+ displayName: 'Email Contains',
63
+ description: 'Return subscribers whose email address contains this value.',
64
+ required: false,
65
+ }),
66
+ referrerUrl: Property.ShortText({
67
+ displayName: 'Referrer URL Contains',
68
+ required: false,
69
+ }),
70
+ ordering: Property.StaticDropdown({
71
+ displayName: 'Ordering',
72
+ required: false,
73
+ options: {
74
+ options: subscriberOrderingOptions,
75
+ },
76
+ }),
77
+ },
78
+ async run({ auth, propsValue }) {
79
+ const query: Record<string, string | number | boolean | (string | number | boolean)[] | undefined> = {};
80
+
81
+ if (propsValue.limit) {
82
+ query['page_size'] = propsValue.limit;
83
+ }
84
+
85
+ if (propsValue.cursor) {
86
+ query['cursor'] = propsValue.cursor;
87
+ }
88
+
89
+ if (propsValue.type) {
90
+ query['type'] = [propsValue.type];
91
+ }
92
+
93
+ if (propsValue.source) {
94
+ query['source'] = [propsValue.source];
95
+ }
96
+
97
+ const tags = (propsValue.tags as Array<{ tag?: string }> | undefined)
98
+ ?.map((entry) => entry.tag?.trim())
99
+ .filter((tag): tag is string => !!tag);
100
+ if (tags && tags.length > 0) {
101
+ query['tag'] = tags;
102
+ }
103
+
104
+ if (propsValue.emailAddress) {
105
+ query['email_address'] = propsValue.emailAddress;
106
+ }
107
+
108
+ if (propsValue.referrerUrl) {
109
+ query['referrer_url'] = propsValue.referrerUrl;
110
+ }
111
+
112
+ if (propsValue.ordering) {
113
+ query['ordering'] = propsValue.ordering;
114
+ }
115
+
116
+ const response = await buttondownRequest<ButtondownPagedResponse<ButtondownSubscriber>>({
117
+ auth: auth.secret_text,
118
+ method: HttpMethod.GET,
119
+ path: '/subscribers',
120
+ query,
121
+ });
122
+
123
+ return response;
124
+ },
125
+ });
@@ -0,0 +1,162 @@
1
+ import { HttpMethod } from '@agnocon/pieces-common';
2
+ import { createAction, Property } from '@agnocon/pieces-framework';
3
+ import { buttondownAuth } from '../common/auth';
4
+ import { buttondownRequest } from '../common/client';
5
+ import { emailStatusOptions, emailTypeOptions } from '../common/options';
6
+ import {
7
+ ButtondownEmail,
8
+ ButtondownEmailInput,
9
+ ButtondownEmailStatus,
10
+ ButtondownEmailType,
11
+ } from '../common/types';
12
+
13
+ export const sendEmail = createAction({
14
+ auth: buttondownAuth,
15
+ name: 'sendEmail',
16
+ displayName: 'Send Email',
17
+ description: 'Send an email to your Buttondown subscribers.',
18
+ audience: 'both',
19
+ aiMetadata: {
20
+ description:
21
+ 'Creates an email in Buttondown from a subject and Markdown/HTML body, then handles it per the chosen delivery status (e.g. send now vs. schedule for a future publish date, or save as draft). Use to broadcast a newsletter or queue one for later. A publish date is required when status is scheduled. Not idempotent: each call creates and may dispatch a new email.',
22
+ idempotent: false,
23
+ },
24
+ props: {
25
+ subject: Property.ShortText({
26
+ displayName: 'Subject',
27
+ required: true,
28
+ }),
29
+ body: Property.LongText({
30
+ displayName: 'Body',
31
+ description:
32
+ 'Write your email content in Markdown or HTML. Buttondown will detect the format automatically.',
33
+ required: true,
34
+ }),
35
+ status: Property.StaticDropdown({
36
+ displayName: 'Delivery Status',
37
+ description: 'Choose how Buttondown should treat this email after creation.',
38
+ required: false,
39
+ defaultValue: 'about_to_send',
40
+ options: {
41
+ options: emailStatusOptions,
42
+ },
43
+ }),
44
+ publishDate: Property.DateTime({
45
+ displayName: 'Publish Date',
46
+ description: 'Required when scheduling an email. Use ISO 8601 format.',
47
+ required: false,
48
+ }),
49
+ emailType: Property.StaticDropdown({
50
+ displayName: 'Email Type',
51
+ required: false,
52
+ defaultValue: 'public',
53
+ options: {
54
+ options: emailTypeOptions,
55
+ },
56
+ }),
57
+ description: Property.LongText({
58
+ displayName: 'Description',
59
+ required: false,
60
+ }),
61
+ canonicalUrl: Property.ShortText({
62
+ displayName: 'Canonical URL',
63
+ required: false,
64
+ }),
65
+ image: Property.ShortText({
66
+ displayName: 'Primary Image URL',
67
+ required: false,
68
+ }),
69
+ slug: Property.ShortText({
70
+ displayName: 'Slug',
71
+ description: 'Override the automatic slug used for the archive URL.',
72
+ required: false,
73
+ }),
74
+ metadata: Property.Json({
75
+ displayName: 'Metadata',
76
+ description: 'Custom metadata to attach to the email (JSON object).',
77
+ required: false,
78
+ }),
79
+ attachments: Property.Array({
80
+ displayName: 'Attachment IDs',
81
+ description: 'IDs of attachments uploaded via the Buttondown API.',
82
+ required: false,
83
+ properties: {
84
+ id: Property.ShortText({
85
+ displayName: 'Attachment ID',
86
+ required: true,
87
+ }),
88
+ },
89
+ }),
90
+ triggerBilling: Property.Checkbox({
91
+ displayName: 'Trigger Pay-per-email Billing',
92
+ description:
93
+ 'Enable when this email should count against pay-per-email billing for paid subscribers.',
94
+ required: false,
95
+ defaultValue: false,
96
+ }),
97
+ },
98
+ async run({ auth, propsValue }) {
99
+ const status = (propsValue.status as ButtondownEmailStatus | undefined) ?? 'about_to_send';
100
+
101
+ if (status === 'scheduled' && !propsValue.publishDate) {
102
+ throw new Error('Publish date is required when scheduling an email.');
103
+ }
104
+
105
+ const payload: ButtondownEmailInput = {
106
+ subject: propsValue.subject,
107
+ body: propsValue.body,
108
+ status,
109
+ };
110
+
111
+ if (propsValue.publishDate) {
112
+ payload.publish_date = propsValue.publishDate;
113
+ }
114
+
115
+ if (propsValue.emailType) {
116
+ payload.email_type = propsValue.emailType as ButtondownEmailType;
117
+ }
118
+
119
+ if (propsValue.description) {
120
+ payload.description = propsValue.description;
121
+ }
122
+
123
+ if (propsValue.canonicalUrl) {
124
+ payload.canonical_url = propsValue.canonicalUrl;
125
+ }
126
+
127
+ if (propsValue.image) {
128
+ payload.image = propsValue.image;
129
+ }
130
+
131
+ if (propsValue.slug) {
132
+ payload.slug = propsValue.slug;
133
+ }
134
+
135
+ if (propsValue.metadata) {
136
+ if (typeof propsValue.metadata !== 'object' || Array.isArray(propsValue.metadata)) {
137
+ throw new Error('Metadata must be provided as a JSON object.');
138
+ }
139
+ payload.metadata = propsValue.metadata as Record<string, unknown>;
140
+ }
141
+
142
+ const attachments = (propsValue.attachments as Array<{ id?: string }> | undefined)
143
+ ?.map((entry) => entry.id?.trim())
144
+ .filter((id): id is string => !!id);
145
+ if (attachments && attachments.length > 0) {
146
+ payload.attachments = attachments;
147
+ }
148
+
149
+ if (propsValue.triggerBilling) {
150
+ payload.should_trigger_pay_per_email_billing = true;
151
+ }
152
+
153
+ const email = await buttondownRequest<ButtondownEmail>({
154
+ auth: auth.secret_text,
155
+ method: HttpMethod.POST,
156
+ path: '/emails',
157
+ body: payload,
158
+ });
159
+
160
+ return email;
161
+ },
162
+ });
@@ -0,0 +1,58 @@
1
+ import { HttpMethod, httpClient } from '@agnocon/pieces-common';
2
+ import { PieceAuth } from '@agnocon/pieces-framework';
3
+ import { BUTTONDOWN_BASE_URL } from './client';
4
+
5
+ interface ButtondownPingResponse {
6
+ newsletter?: string;
7
+ }
8
+
9
+ interface ButtondownError {
10
+ detail?: string;
11
+ error?: string;
12
+ message?: string;
13
+ }
14
+
15
+ export const buttondownAuth = PieceAuth.SecretText({
16
+ displayName: 'API Key',
17
+ description:
18
+ 'Create an API key from https://buttondown.com/keys and paste it here.',
19
+ required: true,
20
+ validate: async ({ auth }) => {
21
+ if (!auth) {
22
+ return {
23
+ valid: false,
24
+ error: 'API key is required.',
25
+ };
26
+ }
27
+
28
+ try {
29
+ await httpClient.sendRequest<ButtondownPingResponse>({
30
+ method: HttpMethod.GET,
31
+ url: `${BUTTONDOWN_BASE_URL}/newsletters`,
32
+ headers: {
33
+ Authorization: `Token ${auth}`,
34
+ },
35
+ });
36
+ return {
37
+ valid: true,
38
+ };
39
+ } catch (unknownError: unknown) {
40
+ const error = unknownError as {
41
+ response?: { status?: number; body?: ButtondownError };
42
+ message?: string;
43
+ };
44
+ const status = error.response?.status;
45
+ const body = error.response?.body ?? {};
46
+ const message =
47
+ body.detail ??
48
+ body.error ??
49
+ body.message ??
50
+ (status === 401 ? 'Invalid API key.' : 'Failed to verify the API key.');
51
+
52
+ return {
53
+ valid: false,
54
+ error: message,
55
+ };
56
+ }
57
+ },
58
+ });