n8n-nodes-postial 0.2.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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/credentials/SocialMintApi.credentials.d.ts +10 -0
- package/dist/credentials/SocialMintApi.credentials.js +42 -0
- package/dist/nodes/SocialMint/SocialMint.node.d.ts +11 -0
- package/dist/nodes/SocialMint/SocialMint.node.js +337 -0
- package/dist/nodes/SocialMint/SocialMint.node.json +21 -0
- package/dist/nodes/SocialMint/helpers.d.ts +21 -0
- package/dist/nodes/SocialMint/helpers.js +83 -0
- package/dist/nodes/SocialMint/socialmint.dark.svg +1 -0
- package/dist/nodes/SocialMint/socialmint.svg +1 -0
- package/dist/nodes/SocialMintTrigger/SocialMintTrigger.node.d.ts +12 -0
- package/dist/nodes/SocialMintTrigger/SocialMintTrigger.node.js +185 -0
- package/dist/nodes/SocialMintTrigger/SocialMintTrigger.node.json +21 -0
- package/package.json +60 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 productivity-boost.com Betriebs UG
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# Postial (formerly SocialMint) for n8n
|
|
2
|
+
|
|
3
|
+
Community nodes for [Postial](https://postial.co), by productivity-boost.com Betriebs UG. Create social posts and receive publishing and approval events in n8n.
|
|
4
|
+
|
|
5
|
+
Version 0.2.0. MIT licensed. Install the renamed package `n8n-nodes-postial`.
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
|
|
9
|
+
Open **Settings → Community Nodes → Install** in your self-hosted n8n instance and enter `n8n-nodes-postial`. Confirm installation, then search for **Postial** or **Postial Trigger** in the node picker. n8n Cloud discovery requires approval through n8n's community package verification process.
|
|
10
|
+
|
|
11
|
+
For local development, run `npm ci`, `npm run build`, and `npm run dev` in this repository. The official n8n node CLI provides the local development environment.
|
|
12
|
+
|
|
13
|
+
## Credentials
|
|
14
|
+
|
|
15
|
+
Create an API key in Postial under **Settings → API** using an Agency workspace. Create **Postial API** credentials in n8n:
|
|
16
|
+
|
|
17
|
+
- **Base URL:** `https://postial.co` (or your Postial installation, without `/api/v1`).
|
|
18
|
+
- **API Key:** your key. The credential test calls `GET /api/v1/me` with Bearer authentication.
|
|
19
|
+
- Grant `brands:read` for dropdowns and brand/channel lists, `posts:read` for post reads, and `posts:write` for mutations. Automatic webhook registration also needs `webhooks:manage`.
|
|
20
|
+
|
|
21
|
+
Never include API keys in workflow examples. Credentials are stored by n8n's credential system.
|
|
22
|
+
|
|
23
|
+
## First workflow
|
|
24
|
+
|
|
25
|
+
1. In Postial, use an active **Agency plan or Agency trial**. Create a brand, then open **Brands → your brand → Connect a channel** and connect at least one publishing account.
|
|
26
|
+
2. Under **Settings → API**, create a key with `posts:write`, `brands:read`, and `webhooks:manage`. Add `posts:read` if you will retrieve posts. Save it in n8n's Postial API credentials.
|
|
27
|
+
3. Create a workflow with **Manual Trigger → Postial**. Select **Post / Create**, your brand and connected channel, enter text, and leave **Draft** selected. Execute it and verify the returned post ID and the draft in Postial.
|
|
28
|
+
4. Create a second workflow with **Postial Trigger**, **Automatic** registration and **Approval Decided**. Select credentials and activate the workflow; its production webhook URL must be publicly reachable over HTTPS.
|
|
29
|
+
5. In the first workflow, enable **Requires Approval**, select **Scheduled Time**, and choose a future date. Execute it, open the returned `approval_url`, and request changes. The active second workflow receives `approval.decided`; inspect its execution output. Requesting changes keeps this test post from publishing. Remove the test post in Postial afterward.
|
|
30
|
+
|
|
31
|
+
Postial's webhook **Test** button sends `ping`. The trigger acknowledges it with HTTP 200 but deliberately emits no workflow item; use an approval decision to test visible output.
|
|
32
|
+
|
|
33
|
+
## Troubleshooting
|
|
34
|
+
|
|
35
|
+
- **401:** Check the Base URL and API key, and replace revoked keys. For trigger signature failures, check the signing secret and synchronize host clocks.
|
|
36
|
+
- **403:** Confirm an active Agency entitlement and the required scopes. Existing keys do not gain scopes automatically; create a replacement key with the needed scopes.
|
|
37
|
+
- **422:** Read the API validation message. Check brand/channel ownership, HTTPS media URLs (at most four), schedule and text constraints. Webhooks require public HTTPS; the workspace limit is ten endpoints, including inactive ones.
|
|
38
|
+
- **429:** The API allows 60 requests per minute per key. Respect `Retry-After`, reduce batch size or add a Wait node before retrying. The node does not automatically retry 429 responses. Preserve the custom key for retries across executions if duplicate creation must be prevented.
|
|
39
|
+
|
|
40
|
+
## Postial node
|
|
41
|
+
|
|
42
|
+
| Resource | Operations |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| Post | Create, Get, Get Many, Retry, Delete |
|
|
45
|
+
| Brand | Get Many |
|
|
46
|
+
| Channel | Get Many by brand |
|
|
47
|
+
|
|
48
|
+
**Create** accepts a brand, multiple channels, text, up to four media URLs (one per line), an optional link, draft/now/scheduled time, and an approval flag. Brand and channel dropdowns load from your workspace; expressions can supply IDs. Dates are sent as ISO timestamps. Draft is the safe default.
|
|
49
|
+
|
|
50
|
+
An automatic `Idempotency-Key` is SHA-256 over the workflow ID, execution ID, node ID, item index, run index (when supplied by n8n), and a canonical SHA-256 hash of the request body. Identical retries in the same run reuse the key; changed bodies and later runs receive different keys. **Custom Idempotency Key** accepts an expression to override this behavior, including across executions. Reuse a custom key only for identical requests; changed bodies with that key return 409. The API retains keys for 24 hours.
|
|
51
|
+
|
|
52
|
+
**Get Many** supports brand/status filters and Return All or Limit; post pagination follows `next_cursor`. Outputs are individual items with paired input references. Get returns post targets, publishing events, and approvals. Delete returns `{ "success": true }`; Postial only permits deletion of drafts or unstarted scheduled posts. Retry asks Postial to retry eligible targets and can fail if none are eligible.
|
|
53
|
+
|
|
54
|
+
API error messages appear in node errors. Use n8n's **On Error → Continue (using regular output)** to receive `{ "error": "..." }` per failed item and continue processing.
|
|
55
|
+
|
|
56
|
+
Example: Schedule Trigger → Postial (Post / Create). Select the brand and channels, enter `Weekly update`, choose Scheduled Time, and set a future date. Enable Requires Approval when review is needed.
|
|
57
|
+
|
|
58
|
+
The execution method uses n8n's authenticated HTTP helper for all network requests. Programmatic execution keeps pagination, input pairing, and per-item idempotency consistent across operations; credential authentication and testing use n8n's declarative definitions. There are no external runtime dependencies or runtime file/environment access.
|
|
59
|
+
|
|
60
|
+
## Postial Trigger
|
|
61
|
+
|
|
62
|
+
Choose one or more events: `post.published`, `post.failed`, `post.needs_review`, `approval.decided`.
|
|
63
|
+
|
|
64
|
+
**Automatic:** activation registers the n8n webhook URL through `POST /api/v1/webhooks` with `{url, events}`. The returned endpoint ID and one-time secret are saved in node-scoped workflow static data. Existence checks use `GET /api/v1/webhooks`, matching the saved ID or the exact webhook URL. Active matching registrations with a known secret are reused. Inactive registrations, changed URL/events, and lost secrets are replaced with DELETE followed by POST because the API does not support PATCH. A failed DELETE preserves recovery state and stops replacement; 404 is harmless. Multiple URL matches require removing duplicates in Postial. Use a dedicated URL per trigger. Deactivation uses `DELETE /api/v1/webhooks/{id}`. n8n must expose a public HTTPS webhook URL. Automatic mode requires a Postial version with webhook management API routes.
|
|
65
|
+
|
|
66
|
+
**Manual (Paste Secret):** register the displayed production webhook URL in Postial yourself and paste its signing secret into the node. No API credentials or management API are required. Deactivation does not remove manually managed endpoints; disable them in Postial yourself. For a manual test, register the test URL and use the matching secret while n8n is listening.
|
|
67
|
+
|
|
68
|
+
The node checks `X-Postial-Signature: t=<seconds>,v1=<hex>` using HMAC-SHA256 over the timestamp, a dot, and the **original request bytes**. It also accepts `X-SocialMint-Signature` as a compatibility fallback. It compares signatures in constant time and accepts timestamps within five minutes in either direction. Invalid signatures receive HTTP 401, valid selected events receive HTTP 200 and emit the event object. Valid unselected events are acknowledged without starting downstream work. Keep host clocks synchronized.
|
|
69
|
+
|
|
70
|
+
Delivery is at least once: deduplicate using the event `id` when downstream actions must happen only once. The five-minute check limits replay age, not repeated deliveries inside that window.
|
|
71
|
+
|
|
72
|
+
The automatic signing secret is in workflow static data; the manual secret is a password-masked node parameter. These are not n8n encrypted credentials. Restrict workflow/database/export access and remove secrets and static data before sharing exports.
|
|
73
|
+
|
|
74
|
+
Import [examples/approval-to-slack.json](examples/approval-to-slack.json), select Postial and Slack credentials, replace the Slack channel ID, then activate it. It sends the approval decision and post link to Slack. To send email instead, replace the Slack node with n8n's Send Email node and use `$json.data.decision` and `$json.data.post_url`.
|
|
75
|
+
|
|
76
|
+
## Validation
|
|
77
|
+
|
|
78
|
+
```sh
|
|
79
|
+
npm ci
|
|
80
|
+
npm run lint
|
|
81
|
+
npm run build
|
|
82
|
+
npm test
|
|
83
|
+
npm pack --dry-run
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The current official starter's `n8n-node lint` enables `eslint-plugin-n8n-nodes-base` plus the community/cloud rules. The strict default ESLint configuration is unchanged; the integration fixture is a template loaded only by the development runner. Unit tests exercise the compiled node and trigger with helper contexts; they need no running n8n server.
|
|
87
|
+
|
|
88
|
+
The smoke test needs a sibling SocialMint checkout with dependencies installed, a disposable local database, and a built SocialMint server. It creates an Agency fixture, makes real HTTP requests, verifies an actual outbox event over loopback, and removes the fixture in `finally`. It never publishes a social post or prints credentials.
|
|
89
|
+
|
|
90
|
+
Start SocialMint in a separate terminal (paths below match the maintainer workspace):
|
|
91
|
+
|
|
92
|
+
```sh
|
|
93
|
+
cd /home/flori/ventures2/socialmint/repo
|
|
94
|
+
set -a
|
|
95
|
+
source /home/flori/ventures2/socialmint/.secrets.env
|
|
96
|
+
export DATABASE_URL="${DATABASE_URL_LOCAL}?sslmode=require"
|
|
97
|
+
export AUTH_URL=http://localhost:3999 AUTH_TRUST_HOST=true
|
|
98
|
+
export NEXT_PUBLIC_APP_URL=http://localhost:3999 PORT=3999 HOSTNAME=127.0.0.1
|
|
99
|
+
set +a
|
|
100
|
+
npm run build && npm start
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
In this package's directory, load the same environment, then run `npm run smoke`. Set `SOCIALMINT_REPO` or `API_HTTP_URL` to override the sibling checkout and `http://127.0.0.1:3999` defaults. Stop the server after the test.
|
|
104
|
+
|
|
105
|
+
Production deliberately rejects loopback webhook destinations. The smoke runner therefore uses SocialMint's existing development-only `WEBHOOK_ALLOW_LOOPBACK=1` support in a separate process for dispatch and webhook management route tests, while post CRUD targets the built server. It does not weaken production URL validation. When webhook management routes are absent, the test uses the real service functions and explicitly prints `OPEN`; rerun after those routes land to complete HTTP management verification.
|
|
106
|
+
|
|
107
|
+
## Release
|
|
108
|
+
|
|
109
|
+
Trusted Publishing is configured for this repository. After checks, push the matching version tag (`git tag v0.1.1` followed by `git push origin v0.1.1`). The Publish GitHub Action runs `npm ci`, lint, build, and tests, then `npm publish --provenance --access public` using GitHub OIDC. Tags must match the package version. Do not publish from a local shell.
|
|
110
|
+
|
|
111
|
+
Before tagging, run all validation commands and the local smoke test, inspect `npm pack --dry-run`, and perform an n8n editor activation/deactivation test. After publication, verify npm provenance, run `npx @n8n/scan-community-package n8n-nodes-postial`, and submit for n8n verification.
|
|
112
|
+
|
|
113
|
+
References checked September 9, 2026: [official starter](https://github.com/n8n-io/n8n-nodes-starter), [verification guidelines](https://docs.n8n.io/connect/create-nodes/build-your-node/reference/verification-guidelines), [Postial API documentation](https://postial.co/docs/api). The guidelines require GitHub Actions publication with provenance, MIT licensing, English documentation, and no external runtime dependencies or file/environment access.
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from "n8n-workflow";
|
|
2
|
+
export declare class SocialMintApi implements ICredentialType {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
documentationUrl: string;
|
|
6
|
+
icon: "file:../nodes/SocialMint/socialmint.svg";
|
|
7
|
+
properties: INodeProperties[];
|
|
8
|
+
authenticate: IAuthenticateGeneric;
|
|
9
|
+
test: ICredentialTestRequest;
|
|
10
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SocialMintApi = void 0;
|
|
4
|
+
class SocialMintApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = "socialMintApi";
|
|
7
|
+
this.displayName = "Postial API";
|
|
8
|
+
this.documentationUrl = "https://postial.co/docs/api";
|
|
9
|
+
this.icon = "file:../nodes/SocialMint/socialmint.svg";
|
|
10
|
+
this.properties = [
|
|
11
|
+
{
|
|
12
|
+
displayName: "Base URL",
|
|
13
|
+
name: "baseUrl",
|
|
14
|
+
type: "string",
|
|
15
|
+
default: "https://postial.co",
|
|
16
|
+
required: true,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
displayName: "API Key",
|
|
20
|
+
name: "apiKey",
|
|
21
|
+
type: "string",
|
|
22
|
+
typeOptions: { password: true },
|
|
23
|
+
default: "",
|
|
24
|
+
required: true,
|
|
25
|
+
},
|
|
26
|
+
];
|
|
27
|
+
this.authenticate = {
|
|
28
|
+
type: "generic",
|
|
29
|
+
properties: {
|
|
30
|
+
headers: { Authorization: "=Bearer {{$credentials.apiKey}}" },
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
this.test = {
|
|
34
|
+
request: {
|
|
35
|
+
baseURL: "={{$credentials.baseUrl}}",
|
|
36
|
+
url: "/api/v1/me",
|
|
37
|
+
method: "GET",
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
exports.SocialMintApi = SocialMintApi;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from "n8n-workflow";
|
|
2
|
+
export declare class SocialMint implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
loadOptions: {
|
|
6
|
+
getBrands(this: ILoadOptionsFunctions): Promise<any>;
|
|
7
|
+
getChannels(this: ILoadOptionsFunctions): Promise<any>;
|
|
8
|
+
};
|
|
9
|
+
};
|
|
10
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SocialMint = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const helpers_1 = require("./helpers");
|
|
6
|
+
class SocialMint {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: "Postial",
|
|
10
|
+
name: "socialMint",
|
|
11
|
+
icon: { light: "file:socialmint.svg", dark: "file:socialmint.dark.svg" },
|
|
12
|
+
group: ["transform"],
|
|
13
|
+
version: 1,
|
|
14
|
+
usableAsTool: true,
|
|
15
|
+
subtitle: '={{$parameter["operation"] + ": " + $parameter["resource"]}}',
|
|
16
|
+
description: "Manage Postial posts, brands, and channels",
|
|
17
|
+
defaults: { name: "Postial" },
|
|
18
|
+
inputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
19
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
20
|
+
credentials: [{ name: "socialMintApi", required: true }],
|
|
21
|
+
properties: [
|
|
22
|
+
{
|
|
23
|
+
displayName: "Custom Idempotency Key",
|
|
24
|
+
name: "idempotencyKey",
|
|
25
|
+
type: "string",
|
|
26
|
+
default: "",
|
|
27
|
+
description: "Optional expression for a stable request key. Leave empty for automatic keys; reuse a custom key only for the same request body.",
|
|
28
|
+
displayOptions: { show: { resource: ["post"], operation: ["create"] } },
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
displayName: "Resource",
|
|
32
|
+
name: "resource",
|
|
33
|
+
type: "options",
|
|
34
|
+
noDataExpression: true,
|
|
35
|
+
default: "post",
|
|
36
|
+
options: [
|
|
37
|
+
{ name: "Brand", value: "brand" },
|
|
38
|
+
{ name: "Channel", value: "channel" },
|
|
39
|
+
{ name: "Post", value: "post" },
|
|
40
|
+
],
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
displayName: "Operation",
|
|
44
|
+
name: "operation",
|
|
45
|
+
type: "options",
|
|
46
|
+
noDataExpression: true,
|
|
47
|
+
displayOptions: { show: { resource: ["post"] } },
|
|
48
|
+
default: "create",
|
|
49
|
+
options: [
|
|
50
|
+
{ name: "Create", value: "create", action: "Create a post" },
|
|
51
|
+
{ name: "Delete", value: "delete", action: "Delete a post" },
|
|
52
|
+
{ name: "Get", value: "get", action: "Get a post" },
|
|
53
|
+
{ name: "Get Many", value: "getAll", action: "Get many posts" },
|
|
54
|
+
{ name: "Retry", value: "retry", action: "Retry a post" },
|
|
55
|
+
],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
displayName: "Operation",
|
|
59
|
+
name: "operation",
|
|
60
|
+
type: "options",
|
|
61
|
+
noDataExpression: true,
|
|
62
|
+
displayOptions: { show: { resource: ["brand", "channel"] } },
|
|
63
|
+
default: "getAll",
|
|
64
|
+
options: [
|
|
65
|
+
{
|
|
66
|
+
name: "Get Many",
|
|
67
|
+
value: "getAll",
|
|
68
|
+
action: "Get many brands or channels",
|
|
69
|
+
},
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
displayName: "Brand Name or ID",
|
|
74
|
+
name: "brandId",
|
|
75
|
+
type: "options",
|
|
76
|
+
typeOptions: { loadOptionsMethod: "getBrands" },
|
|
77
|
+
default: "",
|
|
78
|
+
required: true,
|
|
79
|
+
displayOptions: { show: { resource: ["post"], operation: ["create"] } },
|
|
80
|
+
description: 'Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
81
|
+
},
|
|
82
|
+
{
|
|
83
|
+
displayName: "Brand Name or ID",
|
|
84
|
+
name: "brandId",
|
|
85
|
+
type: "options",
|
|
86
|
+
typeOptions: { loadOptionsMethod: "getBrands" },
|
|
87
|
+
default: "",
|
|
88
|
+
required: true,
|
|
89
|
+
displayOptions: { show: { resource: ["channel"] } },
|
|
90
|
+
description: 'Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
91
|
+
},
|
|
92
|
+
{
|
|
93
|
+
displayName: "Channel Names or IDs",
|
|
94
|
+
name: "channelIds",
|
|
95
|
+
type: "multiOptions",
|
|
96
|
+
typeOptions: {
|
|
97
|
+
loadOptionsMethod: "getChannels",
|
|
98
|
+
loadOptionsDependsOn: ["brandId"],
|
|
99
|
+
},
|
|
100
|
+
default: [],
|
|
101
|
+
displayOptions: { show: { resource: ["post"], operation: ["create"] } },
|
|
102
|
+
description: 'Choose from the list, or specify IDs using an expression. Choose from the list, or specify IDs using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
displayName: "Text",
|
|
106
|
+
name: "text",
|
|
107
|
+
type: "string",
|
|
108
|
+
typeOptions: { rows: 4 },
|
|
109
|
+
default: "",
|
|
110
|
+
displayOptions: { show: { resource: ["post"], operation: ["create"] } },
|
|
111
|
+
},
|
|
112
|
+
{
|
|
113
|
+
displayName: "Media URLs",
|
|
114
|
+
name: "mediaUrls",
|
|
115
|
+
type: "string",
|
|
116
|
+
typeOptions: { rows: 3 },
|
|
117
|
+
default: "",
|
|
118
|
+
description: "Public media URLs, one per line (up to four)",
|
|
119
|
+
displayOptions: { show: { resource: ["post"], operation: ["create"] } },
|
|
120
|
+
},
|
|
121
|
+
{
|
|
122
|
+
displayName: "Link URL",
|
|
123
|
+
name: "linkUrl",
|
|
124
|
+
type: "string",
|
|
125
|
+
default: "",
|
|
126
|
+
displayOptions: { show: { resource: ["post"], operation: ["create"] } },
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
displayName: "Schedule",
|
|
130
|
+
name: "schedule",
|
|
131
|
+
type: "options",
|
|
132
|
+
default: "draft",
|
|
133
|
+
options: [
|
|
134
|
+
{ name: "Draft", value: "draft" },
|
|
135
|
+
{ name: "Now", value: "now" },
|
|
136
|
+
{ name: "Scheduled Time", value: "later" },
|
|
137
|
+
],
|
|
138
|
+
displayOptions: { show: { resource: ["post"], operation: ["create"] } },
|
|
139
|
+
},
|
|
140
|
+
{
|
|
141
|
+
displayName: "Scheduled At",
|
|
142
|
+
name: "scheduledAt",
|
|
143
|
+
type: "dateTime",
|
|
144
|
+
default: "",
|
|
145
|
+
displayOptions: {
|
|
146
|
+
show: {
|
|
147
|
+
resource: ["post"],
|
|
148
|
+
operation: ["create"],
|
|
149
|
+
schedule: ["later"],
|
|
150
|
+
},
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
{
|
|
154
|
+
displayName: "Requires Approval",
|
|
155
|
+
name: "requiresApproval",
|
|
156
|
+
type: "boolean",
|
|
157
|
+
default: false,
|
|
158
|
+
displayOptions: { show: { resource: ["post"], operation: ["create"] } },
|
|
159
|
+
description: "Whether the post requires approval before publishing",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
displayName: "Post ID",
|
|
163
|
+
name: "postId",
|
|
164
|
+
type: "string",
|
|
165
|
+
default: "",
|
|
166
|
+
required: true,
|
|
167
|
+
displayOptions: {
|
|
168
|
+
show: { resource: ["post"], operation: ["get", "delete", "retry"] },
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
{
|
|
172
|
+
displayName: "Return All",
|
|
173
|
+
name: "returnAll",
|
|
174
|
+
type: "boolean",
|
|
175
|
+
default: false,
|
|
176
|
+
description: "Whether to return all results or only up to a given limit",
|
|
177
|
+
displayOptions: { show: { resource: ["post"], operation: ["getAll"] } },
|
|
178
|
+
},
|
|
179
|
+
{
|
|
180
|
+
displayName: "Limit",
|
|
181
|
+
name: "limit",
|
|
182
|
+
type: "number",
|
|
183
|
+
default: 50,
|
|
184
|
+
typeOptions: { minValue: 1 },
|
|
185
|
+
description: "Max number of results to return",
|
|
186
|
+
displayOptions: {
|
|
187
|
+
show: {
|
|
188
|
+
resource: ["post"],
|
|
189
|
+
operation: ["getAll"],
|
|
190
|
+
returnAll: [false],
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
displayName: "Filters",
|
|
196
|
+
name: "filters",
|
|
197
|
+
type: "collection",
|
|
198
|
+
placeholder: "Add Filter",
|
|
199
|
+
default: {},
|
|
200
|
+
displayOptions: { show: { resource: ["post"], operation: ["getAll"] } },
|
|
201
|
+
options: [
|
|
202
|
+
{
|
|
203
|
+
displayName: "Brand Name or ID",
|
|
204
|
+
name: "brand_id",
|
|
205
|
+
type: "options",
|
|
206
|
+
typeOptions: { loadOptionsMethod: "getBrands" },
|
|
207
|
+
default: "",
|
|
208
|
+
description: 'Choose from the list, or specify an ID using an expression. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code/expressions/">expression</a>.',
|
|
209
|
+
},
|
|
210
|
+
{
|
|
211
|
+
displayName: "Status",
|
|
212
|
+
name: "status",
|
|
213
|
+
type: "string",
|
|
214
|
+
default: "",
|
|
215
|
+
description: "Post status, such as draft, scheduled, published, or failed",
|
|
216
|
+
},
|
|
217
|
+
],
|
|
218
|
+
},
|
|
219
|
+
],
|
|
220
|
+
};
|
|
221
|
+
this.methods = {
|
|
222
|
+
loadOptions: {
|
|
223
|
+
async getBrands() {
|
|
224
|
+
const result = await helpers_1.apiRequest.call(this, "GET", "/brands");
|
|
225
|
+
return result.data.map((b) => ({
|
|
226
|
+
name: b.name,
|
|
227
|
+
value: b.id,
|
|
228
|
+
}));
|
|
229
|
+
},
|
|
230
|
+
async getChannels() {
|
|
231
|
+
const brand = this.getCurrentNodeParameter("brandId");
|
|
232
|
+
if (!brand)
|
|
233
|
+
return [];
|
|
234
|
+
const result = await helpers_1.apiRequest.call(this, "GET", `/brands/${encodeURIComponent(String(brand))}/channels`);
|
|
235
|
+
return result.data.map((c) => ({
|
|
236
|
+
name: c.display_name || c.provider,
|
|
237
|
+
value: c.id,
|
|
238
|
+
}));
|
|
239
|
+
},
|
|
240
|
+
},
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
async execute() {
|
|
244
|
+
var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p, _q, _r, _s, _t, _u, _v, _w, _x, _y, _z;
|
|
245
|
+
const output = [];
|
|
246
|
+
for (let i = 0; i < this.getInputData().length; i++) {
|
|
247
|
+
try {
|
|
248
|
+
const resource = this.getNodeParameter("resource", i), operation = this.getNodeParameter("operation", i);
|
|
249
|
+
let result;
|
|
250
|
+
if (resource === "brand")
|
|
251
|
+
result = (await helpers_1.apiRequest.call(this, "GET", "/brands")).data;
|
|
252
|
+
else if (resource === "channel")
|
|
253
|
+
result = (await helpers_1.apiRequest.call(this, "GET", `/brands/${encodeURIComponent(this.getNodeParameter("brandId", i))}/channels`)).data;
|
|
254
|
+
else if (operation === "create") {
|
|
255
|
+
const req = (0, helpers_1.createPostRequest)({
|
|
256
|
+
brandId: this.getNodeParameter("brandId", i),
|
|
257
|
+
channelIds: this.getNodeParameter("channelIds", i),
|
|
258
|
+
text: this.getNodeParameter("text", i),
|
|
259
|
+
mediaUrls: this.getNodeParameter("mediaUrls", i),
|
|
260
|
+
linkUrl: this.getNodeParameter("linkUrl", i),
|
|
261
|
+
schedule: this.getNodeParameter("schedule", i),
|
|
262
|
+
scheduledAt: this.getNodeParameter("scheduledAt", i, ""),
|
|
263
|
+
requiresApproval: this.getNodeParameter("requiresApproval", i),
|
|
264
|
+
}, this.getExecutionId(), this.getNode().id, i, (_a = this.getWorkflow().id) !== null && _a !== void 0 ? _a : "", (_c = (_b = this.getExecuteData) === null || _b === void 0 ? void 0 : _b.call(this).runIndex) !== null && _c !== void 0 ? _c : 0, this.getNodeParameter("idempotencyKey", i, ""));
|
|
265
|
+
result = await helpers_1.apiRequest.call(this, req.method, req.path, req.body, undefined, req.headers);
|
|
266
|
+
}
|
|
267
|
+
else if (operation === "getAll") {
|
|
268
|
+
const all = this.getNodeParameter("returnAll", i), limit = all
|
|
269
|
+
? Infinity
|
|
270
|
+
: this.getNodeParameter("limit", i);
|
|
271
|
+
const filters = this.getNodeParameter("filters", i);
|
|
272
|
+
const qs = Object.fromEntries(Object.entries(filters).filter(([, v]) => v !== ""));
|
|
273
|
+
const rows = [];
|
|
274
|
+
let cursor;
|
|
275
|
+
do {
|
|
276
|
+
const page = await helpers_1.apiRequest.call(this, "GET", "/posts", undefined, {
|
|
277
|
+
...qs,
|
|
278
|
+
limit: Math.min(100, limit - rows.length),
|
|
279
|
+
...(cursor ? { cursor } : {}),
|
|
280
|
+
});
|
|
281
|
+
rows.push(...page.data);
|
|
282
|
+
cursor = page.next_cursor || undefined;
|
|
283
|
+
} while (cursor && rows.length < limit);
|
|
284
|
+
result = rows;
|
|
285
|
+
}
|
|
286
|
+
else if (["get", "retry", "delete"].includes(operation)) {
|
|
287
|
+
const path = "/posts/" +
|
|
288
|
+
encodeURIComponent(this.getNodeParameter("postId", i));
|
|
289
|
+
result = await helpers_1.apiRequest.call(this, operation === "get"
|
|
290
|
+
? "GET"
|
|
291
|
+
: operation === "delete"
|
|
292
|
+
? "DELETE"
|
|
293
|
+
: "POST", path + (operation === "retry" ? "/retry" : ""));
|
|
294
|
+
if (operation === "delete")
|
|
295
|
+
result = { success: true };
|
|
296
|
+
}
|
|
297
|
+
else
|
|
298
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Unsupported operation");
|
|
299
|
+
for (const json of Array.isArray(result) ? result : [result])
|
|
300
|
+
output.push({ json, pairedItem: { item: i } });
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
const apiError = (_j = (_h = (_f = (_e = (_d = error.response) === null || _d === void 0 ? void 0 : _d.body) === null || _e === void 0 ? void 0 : _e.error) !== null && _f !== void 0 ? _f : (_g = error.error) === null || _g === void 0 ? void 0 : _g.error) !== null && _h !== void 0 ? _h : error.error) !== null && _j !== void 0 ? _j : {};
|
|
304
|
+
const statusCode = Number((_p = (_m = (_k = error.statusCode) !== null && _k !== void 0 ? _k : (_l = error.response) === null || _l === void 0 ? void 0 : _l.status) !== null && _m !== void 0 ? _m : (_o = error.response) === null || _o === void 0 ? void 0 : _o.statusCode) !== null && _p !== void 0 ? _p : error.httpCode);
|
|
305
|
+
const retryAfterHeader = (_x = (_v = (_s = (_r = (_q = error.response) === null || _q === void 0 ? void 0 : _q.headers) === null || _r === void 0 ? void 0 : _r["retry-after"]) !== null && _s !== void 0 ? _s : (_u = (_t = error.response) === null || _t === void 0 ? void 0 : _t.headers) === null || _u === void 0 ? void 0 : _u["Retry-After"]) !== null && _v !== void 0 ? _v : (_w = error.headers) === null || _w === void 0 ? void 0 : _w["retry-after"]) !== null && _x !== void 0 ? _x : (_y = error.headers) === null || _y === void 0 ? void 0 : _y["Retry-After"];
|
|
306
|
+
const retryAfter = Number(retryAfterHeader);
|
|
307
|
+
const retryAfterSeconds = Number.isFinite(retryAfter)
|
|
308
|
+
? retryAfter
|
|
309
|
+
: undefined;
|
|
310
|
+
const code = apiError.code;
|
|
311
|
+
const apiMessage = (_z = apiError.message) !== null && _z !== void 0 ? _z : error.message;
|
|
312
|
+
const message = statusCode === 429 && code && retryAfterSeconds !== undefined
|
|
313
|
+
? `Postial API 429 ${code} — retry after ${retryAfterSeconds}s`
|
|
314
|
+
: apiMessage;
|
|
315
|
+
if (this.continueOnFail()) {
|
|
316
|
+
const json = { error: message };
|
|
317
|
+
if (Number.isFinite(statusCode))
|
|
318
|
+
json.statusCode = statusCode;
|
|
319
|
+
if (code)
|
|
320
|
+
json.code = code;
|
|
321
|
+
if (retryAfterSeconds !== undefined)
|
|
322
|
+
json.retryAfter = retryAfterSeconds;
|
|
323
|
+
output.push({ json, pairedItem: { item: i } });
|
|
324
|
+
}
|
|
325
|
+
else
|
|
326
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), error, {
|
|
327
|
+
message,
|
|
328
|
+
description: apiMessage,
|
|
329
|
+
httpCode: Number.isFinite(statusCode) ? String(statusCode) : undefined,
|
|
330
|
+
itemIndex: i,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
return [output];
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
exports.SocialMint = SocialMint;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"node": "n8n-nodes-postial.socialMint",
|
|
3
|
+
"nodeVersion": "1.0",
|
|
4
|
+
"codexVersion": "1.0",
|
|
5
|
+
"categories": [
|
|
6
|
+
"Marketing & Content"
|
|
7
|
+
],
|
|
8
|
+
"resources": {
|
|
9
|
+
"primaryDocumentation": [
|
|
10
|
+
{
|
|
11
|
+
"url": "https://postial.co/docs/api"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"alias": [
|
|
16
|
+
"Postial",
|
|
17
|
+
"social media",
|
|
18
|
+
"publishing",
|
|
19
|
+
"approval"
|
|
20
|
+
]
|
|
21
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { IDataObject, IExecuteFunctions, IHookFunctions, ILoadOptionsFunctions, IHttpRequestMethods } from "n8n-workflow";
|
|
2
|
+
export interface PostInput {
|
|
3
|
+
brandId: string;
|
|
4
|
+
channelIds: string[];
|
|
5
|
+
text: string;
|
|
6
|
+
mediaUrls?: string;
|
|
7
|
+
linkUrl?: string;
|
|
8
|
+
schedule?: string;
|
|
9
|
+
scheduledAt?: string;
|
|
10
|
+
requiresApproval?: boolean;
|
|
11
|
+
}
|
|
12
|
+
export declare function createPostRequest(input: PostInput, executionId: string, nodeName: string, item: number, workflowId?: string, runIndex?: number, customKey?: string): {
|
|
13
|
+
method: "POST";
|
|
14
|
+
path: string;
|
|
15
|
+
body: IDataObject;
|
|
16
|
+
headers: {
|
|
17
|
+
"Idempotency-Key": string;
|
|
18
|
+
};
|
|
19
|
+
};
|
|
20
|
+
export declare function verifySignature(raw: Buffer, header: string, secret: string, now?: number): boolean;
|
|
21
|
+
export declare function apiRequest(this: IExecuteFunctions | ILoadOptionsFunctions | IHookFunctions, method: IHttpRequestMethods, path: string, body?: IDataObject, qs?: IDataObject, headers?: IDataObject): Promise<any>;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createPostRequest = createPostRequest;
|
|
4
|
+
exports.verifySignature = verifySignature;
|
|
5
|
+
exports.apiRequest = apiRequest;
|
|
6
|
+
const crypto_1 = require("crypto");
|
|
7
|
+
function canonicalJson(value) {
|
|
8
|
+
if (Array.isArray(value))
|
|
9
|
+
return "[" + value.map(canonicalJson).join(",") + "]";
|
|
10
|
+
if (value !== null && typeof value === "object")
|
|
11
|
+
return ("{" +
|
|
12
|
+
Object.entries(value)
|
|
13
|
+
.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0))
|
|
14
|
+
.map(([key, entry]) => JSON.stringify(key) + ":" + canonicalJson(entry))
|
|
15
|
+
.join(",") +
|
|
16
|
+
"}");
|
|
17
|
+
return JSON.stringify(value);
|
|
18
|
+
}
|
|
19
|
+
function createPostRequest(input, executionId, nodeName, item, workflowId = "", runIndex = 0, customKey = "") {
|
|
20
|
+
var _a, _b, _c;
|
|
21
|
+
const body = {
|
|
22
|
+
brand_id: input.brandId,
|
|
23
|
+
channel_ids: input.channelIds,
|
|
24
|
+
body: input.text,
|
|
25
|
+
media_urls: ((_a = input.mediaUrls) !== null && _a !== void 0 ? _a : "")
|
|
26
|
+
.split("\n")
|
|
27
|
+
.map((s) => s.trim())
|
|
28
|
+
.filter(Boolean),
|
|
29
|
+
requires_approval: (_b = input.requiresApproval) !== null && _b !== void 0 ? _b : false,
|
|
30
|
+
};
|
|
31
|
+
if (input.linkUrl)
|
|
32
|
+
body.link_url = input.linkUrl;
|
|
33
|
+
if (input.schedule === "now")
|
|
34
|
+
body.scheduled_at = "now";
|
|
35
|
+
if (input.schedule === "later") {
|
|
36
|
+
const date = new Date((_c = input.scheduledAt) !== null && _c !== void 0 ? _c : "");
|
|
37
|
+
if (!Number.isFinite(date.getTime()))
|
|
38
|
+
throw new Error("Enter a valid scheduled date");
|
|
39
|
+
body.scheduled_at = date.toISOString();
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
method: "POST",
|
|
43
|
+
path: "/posts",
|
|
44
|
+
body,
|
|
45
|
+
headers: {
|
|
46
|
+
"Idempotency-Key": customKey ||
|
|
47
|
+
"n8n-" +
|
|
48
|
+
(0, crypto_1.createHash)("sha256")
|
|
49
|
+
.update(JSON.stringify([
|
|
50
|
+
workflowId,
|
|
51
|
+
executionId,
|
|
52
|
+
nodeName,
|
|
53
|
+
item,
|
|
54
|
+
runIndex,
|
|
55
|
+
(0, crypto_1.createHash)("sha256").update(canonicalJson(body)).digest("hex"),
|
|
56
|
+
]))
|
|
57
|
+
.digest("hex"),
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
function verifySignature(raw, header, secret, now = Date.now()) {
|
|
62
|
+
if (!secret)
|
|
63
|
+
return false;
|
|
64
|
+
const match = /^t=(\d{1,12}),v1=([a-f0-9]{64})$/.exec(header);
|
|
65
|
+
if (!match || Math.abs(now / 1000 - Number(match[1])) > 300)
|
|
66
|
+
return false;
|
|
67
|
+
const expected = (0, crypto_1.createHmac)("sha256", secret)
|
|
68
|
+
.update(match[1] + ".")
|
|
69
|
+
.update(raw)
|
|
70
|
+
.digest();
|
|
71
|
+
return (0, crypto_1.timingSafeEqual)(expected, Buffer.from(match[2], "hex"));
|
|
72
|
+
}
|
|
73
|
+
async function apiRequest(method, path, body, qs, headers) {
|
|
74
|
+
const credentials = await this.getCredentials("socialMintApi");
|
|
75
|
+
return await this.helpers.httpRequestWithAuthentication.call(this, "socialMintApi", {
|
|
76
|
+
method,
|
|
77
|
+
url: String(credentials.baseUrl).replace(/\/$/, "") + "/api/v1" + path,
|
|
78
|
+
body,
|
|
79
|
+
qs,
|
|
80
|
+
headers,
|
|
81
|
+
json: true,
|
|
82
|
+
});
|
|
83
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#8bf0cc"/><path fill="none" stroke="#12392e" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" d="M26 22H16v10h10v10H16m19 0V22l8 12 8-12v20"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><rect width="64" height="64" rx="14" fill="#70e4bb"/><path fill="none" stroke="#12392e" stroke-width="5" stroke-linecap="round" stroke-linejoin="round" d="M26 22H16v10h10v10H16m19 0V22l8 12 8-12v20"/></svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IHookFunctions, INodeType, INodeTypeDescription, IWebhookFunctions, IWebhookResponseData } from "n8n-workflow";
|
|
2
|
+
export declare class SocialMintTrigger implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
webhookMethods: {
|
|
5
|
+
default: {
|
|
6
|
+
checkExists(this: IHookFunctions): Promise<boolean>;
|
|
7
|
+
create(this: IHookFunctions): Promise<boolean>;
|
|
8
|
+
delete(this: IHookFunctions): Promise<boolean>;
|
|
9
|
+
};
|
|
10
|
+
};
|
|
11
|
+
webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.SocialMintTrigger = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
const helpers_1 = require("../SocialMint/helpers");
|
|
6
|
+
class SocialMintTrigger {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.description = {
|
|
9
|
+
displayName: "Postial Trigger",
|
|
10
|
+
name: "socialMintTrigger",
|
|
11
|
+
icon: {
|
|
12
|
+
light: "file:../SocialMint/socialmint.svg",
|
|
13
|
+
dark: "file:../SocialMint/socialmint.dark.svg",
|
|
14
|
+
},
|
|
15
|
+
group: ["trigger"],
|
|
16
|
+
version: 1,
|
|
17
|
+
subtitle: '={{$parameter["events"].join(", ")}}',
|
|
18
|
+
description: "Receive signed Postial publishing and approval events",
|
|
19
|
+
defaults: { name: "Postial Trigger" },
|
|
20
|
+
inputs: [],
|
|
21
|
+
outputs: [n8n_workflow_1.NodeConnectionTypes.Main],
|
|
22
|
+
credentials: [
|
|
23
|
+
{
|
|
24
|
+
name: "socialMintApi",
|
|
25
|
+
required: true,
|
|
26
|
+
displayOptions: { show: { mode: ["automatic"] } },
|
|
27
|
+
},
|
|
28
|
+
],
|
|
29
|
+
webhooks: [
|
|
30
|
+
{
|
|
31
|
+
name: "default",
|
|
32
|
+
httpMethod: "POST",
|
|
33
|
+
responseMode: "onReceived",
|
|
34
|
+
path: "webhook",
|
|
35
|
+
isFullPath: false,
|
|
36
|
+
},
|
|
37
|
+
],
|
|
38
|
+
properties: [
|
|
39
|
+
{
|
|
40
|
+
displayName: "Registration",
|
|
41
|
+
name: "mode",
|
|
42
|
+
type: "options",
|
|
43
|
+
default: "automatic",
|
|
44
|
+
options: [
|
|
45
|
+
{ name: "Automatic", value: "automatic" },
|
|
46
|
+
{ name: "Manual (Paste Secret)", value: "manual" },
|
|
47
|
+
],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
displayName: "Signing Secret",
|
|
51
|
+
name: "secret",
|
|
52
|
+
type: "string",
|
|
53
|
+
typeOptions: { password: true },
|
|
54
|
+
default: "",
|
|
55
|
+
required: true,
|
|
56
|
+
displayOptions: { show: { mode: ["manual"] } },
|
|
57
|
+
description: "Signing secret from the endpoint created in Postial. Register this node’s production webhook URL in Postial.",
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
displayName: "Events",
|
|
61
|
+
name: "events",
|
|
62
|
+
type: "multiOptions",
|
|
63
|
+
default: ["approval.decided"],
|
|
64
|
+
required: true,
|
|
65
|
+
options: [
|
|
66
|
+
{ name: "Approval Decided", value: "approval.decided" },
|
|
67
|
+
{ name: "Post Failed", value: "post.failed" },
|
|
68
|
+
{ name: "Post Needs Review", value: "post.needs_review" },
|
|
69
|
+
{ name: "Post Published", value: "post.published" },
|
|
70
|
+
],
|
|
71
|
+
},
|
|
72
|
+
],
|
|
73
|
+
};
|
|
74
|
+
this.webhookMethods = {
|
|
75
|
+
default: {
|
|
76
|
+
async checkExists() {
|
|
77
|
+
var _a, _b, _c;
|
|
78
|
+
if (this.getNodeParameter("mode") === "manual")
|
|
79
|
+
return true;
|
|
80
|
+
const data = this.getWorkflowStaticData("node");
|
|
81
|
+
const result = await helpers_1.apiRequest.call(this, "GET", "/webhooks");
|
|
82
|
+
const url = this.getNodeWebhookUrl("default");
|
|
83
|
+
const events = this.getNodeParameter("events");
|
|
84
|
+
const config = JSON.stringify([url, events]);
|
|
85
|
+
const endpoints = result.data;
|
|
86
|
+
const matches = endpoints.filter((e) => e.url === url);
|
|
87
|
+
const endpoint = (_a = endpoints.find((e) => e.id === data.endpointId)) !== null && _a !== void 0 ? _a : matches[0];
|
|
88
|
+
if (matches.length > 1)
|
|
89
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Multiple Postial endpoints match this webhook URL; remove duplicate registrations in Postial");
|
|
90
|
+
if (endpoint) {
|
|
91
|
+
const ownsSecret = data.endpointId === endpoint.id && !!data.secret;
|
|
92
|
+
if (ownsSecret &&
|
|
93
|
+
endpoint.active !== false &&
|
|
94
|
+
endpoint.url === url &&
|
|
95
|
+
JSON.stringify([...endpoint.events].sort()) ===
|
|
96
|
+
JSON.stringify([...events].sort())) {
|
|
97
|
+
data.config = config;
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
data.endpointId = endpoint.id;
|
|
101
|
+
if (!ownsSecret)
|
|
102
|
+
delete data.secret;
|
|
103
|
+
try {
|
|
104
|
+
await helpers_1.apiRequest.call(this, "DELETE", "/webhooks/" + encodeURIComponent(endpoint.id));
|
|
105
|
+
}
|
|
106
|
+
catch (error) {
|
|
107
|
+
if (Number((_b = error.statusCode) !== null && _b !== void 0 ? _b : (_c = error.response) === null || _c === void 0 ? void 0 : _c.status) !== 404)
|
|
108
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
delete data.endpointId;
|
|
112
|
+
delete data.secret;
|
|
113
|
+
delete data.config;
|
|
114
|
+
return false;
|
|
115
|
+
},
|
|
116
|
+
async create() {
|
|
117
|
+
if (this.getNodeParameter("mode") === "manual")
|
|
118
|
+
return true;
|
|
119
|
+
const url = this.getNodeWebhookUrl("default"), events = this.getNodeParameter("events");
|
|
120
|
+
const endpoint = await helpers_1.apiRequest.call(this, "POST", "/webhooks", {
|
|
121
|
+
url,
|
|
122
|
+
events,
|
|
123
|
+
});
|
|
124
|
+
if (!endpoint.id || !endpoint.secret)
|
|
125
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), "Postial did not return a webhook ID and signing secret");
|
|
126
|
+
const data = this.getWorkflowStaticData("node");
|
|
127
|
+
data.endpointId = endpoint.id;
|
|
128
|
+
data.secret = endpoint.secret;
|
|
129
|
+
data.config = JSON.stringify([url, events]);
|
|
130
|
+
return true;
|
|
131
|
+
},
|
|
132
|
+
async delete() {
|
|
133
|
+
var _a, _b;
|
|
134
|
+
const data = this.getWorkflowStaticData("node");
|
|
135
|
+
if (data.endpointId) {
|
|
136
|
+
try {
|
|
137
|
+
await helpers_1.apiRequest.call(this, "DELETE", "/webhooks/" + encodeURIComponent(String(data.endpointId)));
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
if (Number((_a = error.statusCode) !== null && _a !== void 0 ? _a : (_b = error.response) === null || _b === void 0 ? void 0 : _b.status) !== 404)
|
|
141
|
+
throw new n8n_workflow_1.NodeApiError(this.getNode(), error);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
delete data.endpointId;
|
|
145
|
+
delete data.secret;
|
|
146
|
+
delete data.config;
|
|
147
|
+
return true;
|
|
148
|
+
},
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
async webhook() {
|
|
153
|
+
var _a, _b;
|
|
154
|
+
const req = this.getRequestObject(), response = this.getResponseObject();
|
|
155
|
+
const raw = req.rawBody;
|
|
156
|
+
const headers = this.getHeaderData();
|
|
157
|
+
const header = (_a = headers["x-postial-signature"]) !== null && _a !== void 0 ? _a : headers["x-socialmint-signature"];
|
|
158
|
+
const secret = this.getNodeParameter("mode") === "manual"
|
|
159
|
+
? this.getNodeParameter("secret")
|
|
160
|
+
: String((_b = this.getWorkflowStaticData("node").secret) !== null && _b !== void 0 ? _b : "");
|
|
161
|
+
if (!Buffer.isBuffer(raw) ||
|
|
162
|
+
typeof header !== "string" ||
|
|
163
|
+
!(0, helpers_1.verifySignature)(raw, header, secret)) {
|
|
164
|
+
response.status(401).send("Invalid signature");
|
|
165
|
+
return { noWebhookResponse: true };
|
|
166
|
+
}
|
|
167
|
+
let body;
|
|
168
|
+
try {
|
|
169
|
+
body = JSON.parse(raw.toString("utf8"));
|
|
170
|
+
}
|
|
171
|
+
catch {
|
|
172
|
+
response.status(400).send("Invalid JSON");
|
|
173
|
+
return { noWebhookResponse: true };
|
|
174
|
+
}
|
|
175
|
+
response.status(200).send("OK");
|
|
176
|
+
if (!body ||
|
|
177
|
+
!this.getNodeParameter("events").includes(body.event))
|
|
178
|
+
return { noWebhookResponse: true };
|
|
179
|
+
return {
|
|
180
|
+
noWebhookResponse: true,
|
|
181
|
+
workflowData: [this.helpers.returnJsonArray(body)],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
exports.SocialMintTrigger = SocialMintTrigger;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
{
|
|
2
|
+
"node": "n8n-nodes-postial.socialMintTrigger",
|
|
3
|
+
"nodeVersion": "1.0",
|
|
4
|
+
"codexVersion": "1.0",
|
|
5
|
+
"categories": [
|
|
6
|
+
"Marketing & Content"
|
|
7
|
+
],
|
|
8
|
+
"resources": {
|
|
9
|
+
"primaryDocumentation": [
|
|
10
|
+
{
|
|
11
|
+
"url": "https://postial.co/docs/api"
|
|
12
|
+
}
|
|
13
|
+
]
|
|
14
|
+
},
|
|
15
|
+
"alias": [
|
|
16
|
+
"Postial",
|
|
17
|
+
"social media",
|
|
18
|
+
"publishing",
|
|
19
|
+
"approval"
|
|
20
|
+
]
|
|
21
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-postial",
|
|
3
|
+
"version": "0.2.0",
|
|
4
|
+
"description": "Postial community nodes for creating and scheduling posts and receiving signed publishing and approval webhooks",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://postial.co",
|
|
7
|
+
"keywords": [
|
|
8
|
+
"n8n-community-node-package"
|
|
9
|
+
],
|
|
10
|
+
"author": {
|
|
11
|
+
"name": "productivity-boost.com Betriebs UG",
|
|
12
|
+
"email": "info@productivity-boost.com"
|
|
13
|
+
},
|
|
14
|
+
"repository": {
|
|
15
|
+
"type": "git",
|
|
16
|
+
"url": "https://github.com/fstandhartinger/n8n-nodes-socialmint.git"
|
|
17
|
+
},
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "n8n-node build && node scripts/copy-codex.mjs",
|
|
20
|
+
"build:watch": "tsc --watch",
|
|
21
|
+
"dev": "n8n-node dev",
|
|
22
|
+
"lint": "n8n-node lint",
|
|
23
|
+
"lint:fix": "n8n-node lint --fix",
|
|
24
|
+
"release": "n8n-node release",
|
|
25
|
+
"prepublishOnly": "npm run build",
|
|
26
|
+
"smoke": "node scripts/smoke.mjs",
|
|
27
|
+
"test": "node --test scripts/unit.mjs"
|
|
28
|
+
},
|
|
29
|
+
"files": [
|
|
30
|
+
"dist"
|
|
31
|
+
],
|
|
32
|
+
"n8n": {
|
|
33
|
+
"n8nNodesApiVersion": 1,
|
|
34
|
+
"strict": true,
|
|
35
|
+
"credentials": [
|
|
36
|
+
"dist/credentials/SocialMintApi.credentials.js"
|
|
37
|
+
],
|
|
38
|
+
"nodes": [
|
|
39
|
+
"dist/nodes/SocialMint/SocialMint.node.js",
|
|
40
|
+
"dist/nodes/SocialMintTrigger/SocialMintTrigger.node.js"
|
|
41
|
+
]
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@n8n/node-cli": "*",
|
|
45
|
+
"eslint": "9.39.4",
|
|
46
|
+
"prettier": "3.8.3",
|
|
47
|
+
"release-it": "20.2.0",
|
|
48
|
+
"typescript": "5.9.3",
|
|
49
|
+
"eslint-plugin-n8n-nodes-base": "^1.16.3"
|
|
50
|
+
},
|
|
51
|
+
"peerDependencies": {
|
|
52
|
+
"n8n-workflow": "*"
|
|
53
|
+
},
|
|
54
|
+
"bugs": {
|
|
55
|
+
"url": "https://github.com/fstandhartinger/n8n-nodes-socialmint/issues"
|
|
56
|
+
},
|
|
57
|
+
"publishConfig": {
|
|
58
|
+
"access": "public"
|
|
59
|
+
}
|
|
60
|
+
}
|