n8n-nodes-takeoff-pro 0.1.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 +55 -0
- package/dist/credentials/ExayardApi.credentials.d.ts +14 -0
- package/dist/credentials/ExayardApi.credentials.js +34 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +13 -0
- package/dist/nodes/Exayard/Exayard.node.d.ts +12 -0
- package/dist/nodes/Exayard/Exayard.node.js +321 -0
- package/dist/nodes/Exayard/Exayard.svg +4 -0
- package/dist/nodes/ExayardTrigger/ExayardTrigger.node.d.ts +23 -0
- package/dist/nodes/ExayardTrigger/ExayardTrigger.node.js +148 -0
- package/dist/nodes/ExayardTrigger/ExayardTrigger.svg +5 -0
- package/dist/src/vendor/client.d.ts +101 -0
- package/dist/src/vendor/client.js +130 -0
- package/dist/src/vendor/error.d.ts +42 -0
- package/dist/src/vendor/error.js +68 -0
- package/dist/src/vendor/index.d.ts +21 -0
- package/dist/src/vendor/index.js +25 -0
- package/dist/src/vendor/webhooks.d.ts +37 -0
- package/dist/src/vendor/webhooks.js +90 -0
- package/package.json +55 -0
package/README.md
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# n8n-nodes-exayard
|
|
2
|
+
|
|
3
|
+
n8n community node for [Exayard](https://exayard.com) — AI-powered construction takeoffs, estimates, and bids.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
In your n8n instance:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
npm install n8n-nodes-exayard
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Restart n8n. The package ships two nodes (`Exayard` and `Exayard Trigger`) and one credential type (`Exayard API`).
|
|
14
|
+
|
|
15
|
+
## Authentication
|
|
16
|
+
|
|
17
|
+
1. Sign in to Exayard at https://exayard.com.
|
|
18
|
+
2. Open **Settings → Developer** and create an API key. Scope it to read or write per resource — least-privilege keys keep workflows safe.
|
|
19
|
+
3. In n8n, **Credentials → New → Exayard API** and paste the key. The default base URL (`https://api.exayard.com/v1`) is correct for production.
|
|
20
|
+
|
|
21
|
+
## Nodes
|
|
22
|
+
|
|
23
|
+
### Exayard (action)
|
|
24
|
+
|
|
25
|
+
| Resource | Operations |
|
|
26
|
+
|---|---|
|
|
27
|
+
| Project | List, Get, Create, Archive, Export |
|
|
28
|
+
| Webhook | List Endpoints, Create Endpoint, Delete Endpoint, List Deliveries |
|
|
29
|
+
| Help | Search |
|
|
30
|
+
| Me | Get |
|
|
31
|
+
|
|
32
|
+
### Exayard Trigger
|
|
33
|
+
|
|
34
|
+
Subscribes to Exayard lifecycle events and emits a workflow run per delivery. Supported events:
|
|
35
|
+
|
|
36
|
+
- `project.{created,updated,archived}`
|
|
37
|
+
- `assessment.{started,completed,approved,cancelled}`
|
|
38
|
+
- `estimate.generated`
|
|
39
|
+
- `bid.generated`
|
|
40
|
+
- `file.processed`
|
|
41
|
+
- `*` (all events)
|
|
42
|
+
|
|
43
|
+
Activating the trigger registers a webhook endpoint at Exayard pointing at the n8n production URL. Deactivating it deletes the endpoint so you do not leak orphan subscriptions. Signatures are verified per-delivery (HMAC-SHA256, 5-minute timestamp window).
|
|
44
|
+
|
|
45
|
+
## Links
|
|
46
|
+
|
|
47
|
+
- Marketing landing page: https://exayard.com/integrations/n8n
|
|
48
|
+
- API docs: https://developers.exayard.com
|
|
49
|
+
- OpenAPI: https://api.exayard.com/v1/openapi.json
|
|
50
|
+
- Status: https://status.exayard.com
|
|
51
|
+
- Source: https://github.com/exayard/exayard-mcp-examples
|
|
52
|
+
|
|
53
|
+
## License
|
|
54
|
+
|
|
55
|
+
MIT.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
/**
|
|
3
|
+
* Credential type for the Exayard n8n nodes.
|
|
4
|
+
*
|
|
5
|
+
* Stores an Exayard API key generated at
|
|
6
|
+
* https://exayard.com/settings/developer. The key is sent on every
|
|
7
|
+
* request as `Authorization: Bearer <key>`.
|
|
8
|
+
*/
|
|
9
|
+
export declare class ExayardApi implements ICredentialType {
|
|
10
|
+
name: string;
|
|
11
|
+
displayName: string;
|
|
12
|
+
documentationUrl: string;
|
|
13
|
+
properties: INodeProperties[];
|
|
14
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExayardApi = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Credential type for the Exayard n8n nodes.
|
|
6
|
+
*
|
|
7
|
+
* Stores an Exayard API key generated at
|
|
8
|
+
* https://exayard.com/settings/developer. The key is sent on every
|
|
9
|
+
* request as `Authorization: Bearer <key>`.
|
|
10
|
+
*/
|
|
11
|
+
class ExayardApi {
|
|
12
|
+
name = 'exayardApi';
|
|
13
|
+
displayName = 'Exayard API';
|
|
14
|
+
documentationUrl = 'https://developers.exayard.com/authentication';
|
|
15
|
+
properties = [
|
|
16
|
+
{
|
|
17
|
+
displayName: 'API Key',
|
|
18
|
+
name: 'apiKey',
|
|
19
|
+
type: 'string',
|
|
20
|
+
typeOptions: { password: true },
|
|
21
|
+
default: '',
|
|
22
|
+
required: true,
|
|
23
|
+
description: 'Generate one at Exayard → Settings → Developer. Scope to read:* or read:*/write:* depending on what this workflow needs.'
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
displayName: 'Base URL',
|
|
27
|
+
name: 'baseUrl',
|
|
28
|
+
type: 'string',
|
|
29
|
+
default: 'https://api.exayard.com/v1',
|
|
30
|
+
description: 'Override only if you are pointing at a non-production deployment.'
|
|
31
|
+
}
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
exports.ExayardApi = ExayardApi;
|
package/dist/index.d.ts
ADDED
package/dist/index.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// Re-exports so consumers (and the n8n loader) can import the node and
|
|
3
|
+
// credential classes without reaching into the dist tree directly. The
|
|
4
|
+
// actual `n8n` package.json field still points at compiled dist/*.js so
|
|
5
|
+
// runtime discovery does not depend on this file.
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.ExayardTrigger = exports.Exayard = exports.ExayardApi = void 0;
|
|
8
|
+
var ExayardApi_credentials_1 = require("./credentials/ExayardApi.credentials");
|
|
9
|
+
Object.defineProperty(exports, "ExayardApi", { enumerable: true, get: function () { return ExayardApi_credentials_1.ExayardApi; } });
|
|
10
|
+
var Exayard_node_1 = require("./nodes/Exayard/Exayard.node");
|
|
11
|
+
Object.defineProperty(exports, "Exayard", { enumerable: true, get: function () { return Exayard_node_1.Exayard; } });
|
|
12
|
+
var ExayardTrigger_node_1 = require("./nodes/ExayardTrigger/ExayardTrigger.node");
|
|
13
|
+
Object.defineProperty(exports, "ExayardTrigger", { enumerable: true, get: function () { return ExayardTrigger_node_1.ExayardTrigger; } });
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { IExecuteFunctions, INodeExecutionData, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
/**
|
|
3
|
+
* Action node for the Exayard API.
|
|
4
|
+
*
|
|
5
|
+
* Mirrors the @exayard/sdk surface: Projects, Webhooks, Help, Me.
|
|
6
|
+
* Each operation maps directly to a method on the SDK client so the
|
|
7
|
+
* shape stays in sync when new endpoints land.
|
|
8
|
+
*/
|
|
9
|
+
export declare class Exayard implements INodeType {
|
|
10
|
+
description: INodeTypeDescription;
|
|
11
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
12
|
+
}
|
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Exayard = void 0;
|
|
4
|
+
const vendor_1 = require("../../src/vendor");
|
|
5
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
6
|
+
/**
|
|
7
|
+
* Action node for the Exayard API.
|
|
8
|
+
*
|
|
9
|
+
* Mirrors the @exayard/sdk surface: Projects, Webhooks, Help, Me.
|
|
10
|
+
* Each operation maps directly to a method on the SDK client so the
|
|
11
|
+
* shape stays in sync when new endpoints land.
|
|
12
|
+
*/
|
|
13
|
+
class Exayard {
|
|
14
|
+
description = {
|
|
15
|
+
displayName: 'Exayard',
|
|
16
|
+
name: 'exayard',
|
|
17
|
+
icon: 'file:Exayard.svg',
|
|
18
|
+
group: ['transform'],
|
|
19
|
+
version: 1,
|
|
20
|
+
subtitle: '={{$parameter["resource"] + ": " + $parameter["operation"]}}',
|
|
21
|
+
description: 'AI-powered construction takeoffs, estimates, and bids',
|
|
22
|
+
defaults: { name: 'Exayard' },
|
|
23
|
+
inputs: ['main'],
|
|
24
|
+
outputs: ['main'],
|
|
25
|
+
credentials: [{ name: 'exayardApi', required: true }],
|
|
26
|
+
properties: [
|
|
27
|
+
{
|
|
28
|
+
displayName: 'Resource',
|
|
29
|
+
name: 'resource',
|
|
30
|
+
type: 'options',
|
|
31
|
+
noDataExpression: true,
|
|
32
|
+
default: 'project',
|
|
33
|
+
options: [
|
|
34
|
+
{ name: 'Project', value: 'project' },
|
|
35
|
+
{ name: 'Webhook', value: 'webhook' },
|
|
36
|
+
{ name: 'Help', value: 'help' },
|
|
37
|
+
{ name: 'Me', value: 'me' }
|
|
38
|
+
]
|
|
39
|
+
},
|
|
40
|
+
// Project operations
|
|
41
|
+
{
|
|
42
|
+
displayName: 'Operation',
|
|
43
|
+
name: 'operation',
|
|
44
|
+
type: 'options',
|
|
45
|
+
noDataExpression: true,
|
|
46
|
+
default: 'list',
|
|
47
|
+
displayOptions: { show: { resource: ['project'] } },
|
|
48
|
+
options: [
|
|
49
|
+
{ name: 'List', value: 'list', action: 'List projects', description: 'List projects in an organization' },
|
|
50
|
+
{ name: 'Get', value: 'get', action: 'Get a project', description: 'Get a single project by ID' },
|
|
51
|
+
{ name: 'Create', value: 'create', action: 'Create a project', description: 'Create a new project' },
|
|
52
|
+
{ name: 'Archive', value: 'archive', action: 'Archive a project', description: 'Archive an existing project' },
|
|
53
|
+
{ name: 'Export', value: 'export', action: 'Export a project', description: 'Export project data' }
|
|
54
|
+
]
|
|
55
|
+
},
|
|
56
|
+
// Webhook operations
|
|
57
|
+
{
|
|
58
|
+
displayName: 'Operation',
|
|
59
|
+
name: 'operation',
|
|
60
|
+
type: 'options',
|
|
61
|
+
noDataExpression: true,
|
|
62
|
+
default: 'list',
|
|
63
|
+
displayOptions: { show: { resource: ['webhook'] } },
|
|
64
|
+
options: [
|
|
65
|
+
{ name: 'List Endpoints', value: 'list', action: 'List webhook endpoints' },
|
|
66
|
+
{ name: 'Create Endpoint', value: 'create', action: 'Create a webhook endpoint' },
|
|
67
|
+
{ name: 'Delete Endpoint', value: 'delete', action: 'Delete a webhook endpoint' },
|
|
68
|
+
{ name: 'List Deliveries', value: 'deliveries', action: 'List deliveries for an endpoint' }
|
|
69
|
+
]
|
|
70
|
+
},
|
|
71
|
+
// Help operations
|
|
72
|
+
{
|
|
73
|
+
displayName: 'Operation',
|
|
74
|
+
name: 'operation',
|
|
75
|
+
type: 'options',
|
|
76
|
+
noDataExpression: true,
|
|
77
|
+
default: 'search',
|
|
78
|
+
displayOptions: { show: { resource: ['help'] } },
|
|
79
|
+
options: [{ name: 'Search', value: 'search', action: 'Search help articles' }]
|
|
80
|
+
},
|
|
81
|
+
// Me operations
|
|
82
|
+
{
|
|
83
|
+
displayName: 'Operation',
|
|
84
|
+
name: 'operation',
|
|
85
|
+
type: 'options',
|
|
86
|
+
noDataExpression: true,
|
|
87
|
+
default: 'get',
|
|
88
|
+
displayOptions: { show: { resource: ['me'] } },
|
|
89
|
+
options: [{ name: 'Get', value: 'get', action: 'Get the authenticated identity' }]
|
|
90
|
+
},
|
|
91
|
+
// Shared org ID
|
|
92
|
+
{
|
|
93
|
+
displayName: 'Organization ID',
|
|
94
|
+
name: 'organizationId',
|
|
95
|
+
type: 'string',
|
|
96
|
+
default: '',
|
|
97
|
+
required: true,
|
|
98
|
+
description: 'Exayard organization ID (org_...)',
|
|
99
|
+
displayOptions: { show: { resource: ['project', 'webhook'] } }
|
|
100
|
+
},
|
|
101
|
+
// Project: Get / Archive / Export — ID
|
|
102
|
+
{
|
|
103
|
+
displayName: 'Project ID',
|
|
104
|
+
name: 'projectId',
|
|
105
|
+
type: 'string',
|
|
106
|
+
default: '',
|
|
107
|
+
required: true,
|
|
108
|
+
displayOptions: { show: { resource: ['project'], operation: ['get', 'archive', 'export'] } }
|
|
109
|
+
},
|
|
110
|
+
// Project: List — optional filters
|
|
111
|
+
{
|
|
112
|
+
displayName: 'Status',
|
|
113
|
+
name: 'status',
|
|
114
|
+
type: 'string',
|
|
115
|
+
default: '',
|
|
116
|
+
description: 'Optional status filter (active, draft, archived, etc.)',
|
|
117
|
+
displayOptions: { show: { resource: ['project'], operation: ['list'] } }
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
displayName: 'Search',
|
|
121
|
+
name: 'search',
|
|
122
|
+
type: 'string',
|
|
123
|
+
default: '',
|
|
124
|
+
description: 'Free-text search across project names',
|
|
125
|
+
displayOptions: { show: { resource: ['project'], operation: ['list'] } }
|
|
126
|
+
},
|
|
127
|
+
{
|
|
128
|
+
displayName: 'Limit',
|
|
129
|
+
name: 'limit',
|
|
130
|
+
type: 'number',
|
|
131
|
+
default: 50,
|
|
132
|
+
typeOptions: { minValue: 1, maxValue: 200 },
|
|
133
|
+
description: 'Max number of projects per page',
|
|
134
|
+
displayOptions: { show: { resource: ['project'], operation: ['list'] } }
|
|
135
|
+
},
|
|
136
|
+
// Project: Create — name
|
|
137
|
+
{
|
|
138
|
+
displayName: 'Name',
|
|
139
|
+
name: 'name',
|
|
140
|
+
type: 'string',
|
|
141
|
+
default: '',
|
|
142
|
+
required: true,
|
|
143
|
+
displayOptions: { show: { resource: ['project'], operation: ['create'] } }
|
|
144
|
+
},
|
|
145
|
+
// Webhook: Create — url + events
|
|
146
|
+
{
|
|
147
|
+
displayName: 'Endpoint URL',
|
|
148
|
+
name: 'url',
|
|
149
|
+
type: 'string',
|
|
150
|
+
default: '',
|
|
151
|
+
required: true,
|
|
152
|
+
placeholder: 'https://example.com/webhooks/exayard',
|
|
153
|
+
displayOptions: { show: { resource: ['webhook'], operation: ['create'] } }
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
displayName: 'Events',
|
|
157
|
+
name: 'events',
|
|
158
|
+
type: 'multiOptions',
|
|
159
|
+
default: ['project.created', 'assessment.completed', 'estimate.generated', 'bid.generated'],
|
|
160
|
+
options: [
|
|
161
|
+
{ name: 'project.created', value: 'project.created' },
|
|
162
|
+
{ name: 'project.updated', value: 'project.updated' },
|
|
163
|
+
{ name: 'project.archived', value: 'project.archived' },
|
|
164
|
+
{ name: 'assessment.started', value: 'assessment.started' },
|
|
165
|
+
{ name: 'assessment.completed', value: 'assessment.completed' },
|
|
166
|
+
{ name: 'assessment.approved', value: 'assessment.approved' },
|
|
167
|
+
{ name: 'assessment.cancelled', value: 'assessment.cancelled' },
|
|
168
|
+
{ name: 'estimate.generated', value: 'estimate.generated' },
|
|
169
|
+
{ name: 'bid.generated', value: 'bid.generated' },
|
|
170
|
+
{ name: 'file.processed', value: 'file.processed' },
|
|
171
|
+
{ name: '* (all events)', value: '*' }
|
|
172
|
+
],
|
|
173
|
+
required: true,
|
|
174
|
+
displayOptions: { show: { resource: ['webhook'], operation: ['create'] } }
|
|
175
|
+
},
|
|
176
|
+
{
|
|
177
|
+
displayName: 'Description',
|
|
178
|
+
name: 'description',
|
|
179
|
+
type: 'string',
|
|
180
|
+
default: '',
|
|
181
|
+
displayOptions: { show: { resource: ['webhook'], operation: ['create'] } }
|
|
182
|
+
},
|
|
183
|
+
// Webhook: Delete / Deliveries — endpoint ID
|
|
184
|
+
{
|
|
185
|
+
displayName: 'Endpoint ID',
|
|
186
|
+
name: 'endpointId',
|
|
187
|
+
type: 'string',
|
|
188
|
+
default: '',
|
|
189
|
+
required: true,
|
|
190
|
+
displayOptions: { show: { resource: ['webhook'], operation: ['delete', 'deliveries'] } }
|
|
191
|
+
},
|
|
192
|
+
// Help: Search — query
|
|
193
|
+
{
|
|
194
|
+
displayName: 'Query',
|
|
195
|
+
name: 'query',
|
|
196
|
+
type: 'string',
|
|
197
|
+
default: '',
|
|
198
|
+
required: true,
|
|
199
|
+
displayOptions: { show: { resource: ['help'], operation: ['search'] } }
|
|
200
|
+
}
|
|
201
|
+
]
|
|
202
|
+
};
|
|
203
|
+
async execute() {
|
|
204
|
+
const credentials = await this.getCredentials('exayardApi');
|
|
205
|
+
const client = new vendor_1.Exayard({
|
|
206
|
+
apiKey: credentials.apiKey,
|
|
207
|
+
baseUrl: credentials.baseUrl || undefined
|
|
208
|
+
});
|
|
209
|
+
const items = this.getInputData();
|
|
210
|
+
const out = [];
|
|
211
|
+
for (let i = 0; i < items.length; i++) {
|
|
212
|
+
const resource = this.getNodeParameter('resource', i);
|
|
213
|
+
const operation = this.getNodeParameter('operation', i);
|
|
214
|
+
try {
|
|
215
|
+
let result;
|
|
216
|
+
if (resource === 'me') {
|
|
217
|
+
result = await client.me.get();
|
|
218
|
+
}
|
|
219
|
+
else if (resource === 'project') {
|
|
220
|
+
const organizationId = this.getNodeParameter('organizationId', i);
|
|
221
|
+
if (operation === 'list') {
|
|
222
|
+
const status = this.getNodeParameter('status', i, '');
|
|
223
|
+
const search = this.getNodeParameter('search', i, '');
|
|
224
|
+
const limit = this.getNodeParameter('limit', i, 50);
|
|
225
|
+
result = await client.projects.list({
|
|
226
|
+
organizationId,
|
|
227
|
+
status: status || undefined,
|
|
228
|
+
search: search || undefined,
|
|
229
|
+
limit
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
else if (operation === 'get') {
|
|
233
|
+
const projectId = this.getNodeParameter('projectId', i);
|
|
234
|
+
result = await client.projects.get(projectId, { organizationId });
|
|
235
|
+
}
|
|
236
|
+
else if (operation === 'create') {
|
|
237
|
+
const name = this.getNodeParameter('name', i);
|
|
238
|
+
result = await client.projects.create({ organizationId, name });
|
|
239
|
+
}
|
|
240
|
+
else if (operation === 'archive') {
|
|
241
|
+
const projectId = this.getNodeParameter('projectId', i);
|
|
242
|
+
await client.projects.archive(projectId, { organizationId });
|
|
243
|
+
result = { ok: true, archived: projectId };
|
|
244
|
+
}
|
|
245
|
+
else if (operation === 'export') {
|
|
246
|
+
const projectId = this.getNodeParameter('projectId', i);
|
|
247
|
+
result = await client.projects.export(projectId, { organizationId });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
else if (resource === 'webhook') {
|
|
251
|
+
const organizationId = this.getNodeParameter('organizationId', i);
|
|
252
|
+
if (operation === 'list') {
|
|
253
|
+
result = await client.webhooks.listEndpoints({ organizationId });
|
|
254
|
+
}
|
|
255
|
+
else if (operation === 'create') {
|
|
256
|
+
const url = this.getNodeParameter('url', i);
|
|
257
|
+
const events = this.getNodeParameter('events', i);
|
|
258
|
+
const description = this.getNodeParameter('description', i, '');
|
|
259
|
+
result = await client.webhooks.createEndpoint({
|
|
260
|
+
organizationId,
|
|
261
|
+
url,
|
|
262
|
+
events,
|
|
263
|
+
description: description || undefined
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
else if (operation === 'delete') {
|
|
267
|
+
const endpointId = this.getNodeParameter('endpointId', i);
|
|
268
|
+
await client.webhooks.deleteEndpoint(endpointId, { organizationId });
|
|
269
|
+
result = { ok: true, deleted: endpointId };
|
|
270
|
+
}
|
|
271
|
+
else if (operation === 'deliveries') {
|
|
272
|
+
const endpointId = this.getNodeParameter('endpointId', i);
|
|
273
|
+
result = await client.webhooks.listDeliveries(endpointId, { organizationId });
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
else if (resource === 'help') {
|
|
277
|
+
const query = this.getNodeParameter('query', i);
|
|
278
|
+
result = await client.help.search({ query });
|
|
279
|
+
}
|
|
280
|
+
if (result === undefined) {
|
|
281
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unsupported resource/operation combination: ${resource}.${operation}`, { itemIndex: i });
|
|
282
|
+
}
|
|
283
|
+
// n8n expects each output item to be wrapped in { json: ... }.
|
|
284
|
+
// Array-returning operations are split into one item per row so
|
|
285
|
+
// downstream nodes can iterate naturally. Cast through unknown
|
|
286
|
+
// because n8n's IDataObject is stricter than Record<string,
|
|
287
|
+
// unknown> — values must be IDataObject | GenericValue, but
|
|
288
|
+
// upstream SDK return types are intentionally loose.
|
|
289
|
+
const push = (row) => {
|
|
290
|
+
out.push({ json: row, pairedItem: { item: i } });
|
|
291
|
+
};
|
|
292
|
+
if (Array.isArray(result)) {
|
|
293
|
+
for (const row of result)
|
|
294
|
+
push(row);
|
|
295
|
+
}
|
|
296
|
+
else if (result && typeof result === 'object' && 'items' in result && Array.isArray(result.items)) {
|
|
297
|
+
for (const row of result.items)
|
|
298
|
+
push(row);
|
|
299
|
+
}
|
|
300
|
+
else {
|
|
301
|
+
push(result);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch (err) {
|
|
305
|
+
if (err instanceof vendor_1.ExayardError) {
|
|
306
|
+
// Surface RFC 9457 problem+json fields verbatim so workflow
|
|
307
|
+
// branches can read code / detail / doc_url. Re-throw as a
|
|
308
|
+
// NodeOperationError so n8n's standard error handling kicks in
|
|
309
|
+
// (Continue On Fail, etc.).
|
|
310
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `${err.title} (${err.code}): ${err.detail}`, {
|
|
311
|
+
itemIndex: i,
|
|
312
|
+
description: err.docUrl ? `See ${err.docUrl}` : undefined
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
throw err;
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
return [out];
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
exports.Exayard = Exayard;
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
|
|
2
|
+
<rect width="60" height="60" rx="12" fill="#0F1115"/>
|
|
3
|
+
<text x="30" y="38" font-family="-apple-system, system-ui, sans-serif" font-weight="700" font-size="24" text-anchor="middle" fill="#F5F2EA">E</text>
|
|
4
|
+
</svg>
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { IHookFunctions, INodeType, INodeTypeDescription, IWebhookFunctions, IWebhookResponseData } from 'n8n-workflow';
|
|
2
|
+
/**
|
|
3
|
+
* Webhook trigger node for Exayard lifecycle events.
|
|
4
|
+
*
|
|
5
|
+
* On activate, registers a webhook endpoint at api.exayard.com pointing
|
|
6
|
+
* at the n8n production webhook URL. The endpoint secret returned by
|
|
7
|
+
* Exayard is stored in node static data so signature verification works
|
|
8
|
+
* across restarts.
|
|
9
|
+
*
|
|
10
|
+
* On deactivate, deletes the registered endpoint so we don't leak
|
|
11
|
+
* orphan webhook subscriptions on the Exayard side.
|
|
12
|
+
*/
|
|
13
|
+
export declare class ExayardTrigger implements INodeType {
|
|
14
|
+
description: INodeTypeDescription;
|
|
15
|
+
webhookMethods: {
|
|
16
|
+
default: {
|
|
17
|
+
checkExists(this: IHookFunctions): Promise<boolean>;
|
|
18
|
+
create(this: IHookFunctions): Promise<boolean>;
|
|
19
|
+
delete(this: IHookFunctions): Promise<boolean>;
|
|
20
|
+
};
|
|
21
|
+
};
|
|
22
|
+
webhook(this: IWebhookFunctions): Promise<IWebhookResponseData>;
|
|
23
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ExayardTrigger = void 0;
|
|
4
|
+
const vendor_1 = require("../../src/vendor");
|
|
5
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
6
|
+
/**
|
|
7
|
+
* Webhook trigger node for Exayard lifecycle events.
|
|
8
|
+
*
|
|
9
|
+
* On activate, registers a webhook endpoint at api.exayard.com pointing
|
|
10
|
+
* at the n8n production webhook URL. The endpoint secret returned by
|
|
11
|
+
* Exayard is stored in node static data so signature verification works
|
|
12
|
+
* across restarts.
|
|
13
|
+
*
|
|
14
|
+
* On deactivate, deletes the registered endpoint so we don't leak
|
|
15
|
+
* orphan webhook subscriptions on the Exayard side.
|
|
16
|
+
*/
|
|
17
|
+
class ExayardTrigger {
|
|
18
|
+
description = {
|
|
19
|
+
displayName: 'Exayard Trigger',
|
|
20
|
+
name: 'exayardTrigger',
|
|
21
|
+
icon: 'file:ExayardTrigger.svg',
|
|
22
|
+
group: ['trigger'],
|
|
23
|
+
version: 1,
|
|
24
|
+
description: 'Triggers when an Exayard lifecycle event fires',
|
|
25
|
+
defaults: { name: 'Exayard Trigger' },
|
|
26
|
+
inputs: [],
|
|
27
|
+
outputs: ['main'],
|
|
28
|
+
credentials: [{ name: 'exayardApi', required: true }],
|
|
29
|
+
webhooks: [
|
|
30
|
+
{
|
|
31
|
+
name: 'default',
|
|
32
|
+
httpMethod: 'POST',
|
|
33
|
+
responseMode: 'onReceived',
|
|
34
|
+
path: 'webhook'
|
|
35
|
+
}
|
|
36
|
+
],
|
|
37
|
+
properties: [
|
|
38
|
+
{
|
|
39
|
+
displayName: 'Organization ID',
|
|
40
|
+
name: 'organizationId',
|
|
41
|
+
type: 'string',
|
|
42
|
+
default: '',
|
|
43
|
+
required: true,
|
|
44
|
+
description: 'Exayard organization ID (org_...)'
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
displayName: 'Events',
|
|
48
|
+
name: 'events',
|
|
49
|
+
type: 'multiOptions',
|
|
50
|
+
default: ['project.created', 'assessment.completed', 'estimate.generated', 'bid.generated'],
|
|
51
|
+
options: [
|
|
52
|
+
{ name: 'project.created', value: 'project.created' },
|
|
53
|
+
{ name: 'project.updated', value: 'project.updated' },
|
|
54
|
+
{ name: 'project.archived', value: 'project.archived' },
|
|
55
|
+
{ name: 'assessment.started', value: 'assessment.started' },
|
|
56
|
+
{ name: 'assessment.completed', value: 'assessment.completed' },
|
|
57
|
+
{ name: 'assessment.approved', value: 'assessment.approved' },
|
|
58
|
+
{ name: 'assessment.cancelled', value: 'assessment.cancelled' },
|
|
59
|
+
{ name: 'estimate.generated', value: 'estimate.generated' },
|
|
60
|
+
{ name: 'bid.generated', value: 'bid.generated' },
|
|
61
|
+
{ name: 'file.processed', value: 'file.processed' },
|
|
62
|
+
{ name: '* (all events)', value: '*' }
|
|
63
|
+
],
|
|
64
|
+
required: true
|
|
65
|
+
}
|
|
66
|
+
]
|
|
67
|
+
};
|
|
68
|
+
webhookMethods = {
|
|
69
|
+
default: {
|
|
70
|
+
async checkExists() {
|
|
71
|
+
const data = this.getWorkflowStaticData('node');
|
|
72
|
+
return typeof data.endpointId === 'string';
|
|
73
|
+
},
|
|
74
|
+
async create() {
|
|
75
|
+
const credentials = await this.getCredentials('exayardApi');
|
|
76
|
+
const organizationId = this.getNodeParameter('organizationId');
|
|
77
|
+
const events = this.getNodeParameter('events');
|
|
78
|
+
const url = this.getNodeWebhookUrl('default');
|
|
79
|
+
if (!url) {
|
|
80
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), 'n8n did not provide a webhook URL.');
|
|
81
|
+
}
|
|
82
|
+
const client = new vendor_1.Exayard({
|
|
83
|
+
apiKey: credentials.apiKey,
|
|
84
|
+
baseUrl: credentials.baseUrl || undefined
|
|
85
|
+
});
|
|
86
|
+
const res = await client.webhooks.createEndpoint({
|
|
87
|
+
organizationId,
|
|
88
|
+
url,
|
|
89
|
+
events,
|
|
90
|
+
description: 'n8n trigger'
|
|
91
|
+
});
|
|
92
|
+
const data = this.getWorkflowStaticData('node');
|
|
93
|
+
data.endpointId = res.id;
|
|
94
|
+
data.endpointSecret = res.secret;
|
|
95
|
+
data.organizationId = organizationId;
|
|
96
|
+
return true;
|
|
97
|
+
},
|
|
98
|
+
async delete() {
|
|
99
|
+
const data = this.getWorkflowStaticData('node');
|
|
100
|
+
if (typeof data.endpointId !== 'string' || typeof data.organizationId !== 'string') {
|
|
101
|
+
return true;
|
|
102
|
+
}
|
|
103
|
+
const credentials = await this.getCredentials('exayardApi');
|
|
104
|
+
const client = new vendor_1.Exayard({
|
|
105
|
+
apiKey: credentials.apiKey,
|
|
106
|
+
baseUrl: credentials.baseUrl || undefined
|
|
107
|
+
});
|
|
108
|
+
try {
|
|
109
|
+
await client.webhooks.deleteEndpoint(data.endpointId, { organizationId: data.organizationId });
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// Best-effort cleanup; Exayard might have already removed the endpoint.
|
|
113
|
+
}
|
|
114
|
+
delete data.endpointId;
|
|
115
|
+
delete data.endpointSecret;
|
|
116
|
+
delete data.organizationId;
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
async webhook() {
|
|
122
|
+
const req = this.getRequestObject();
|
|
123
|
+
const signature = (req.headers['exayard-signature'] || req.headers['Exayard-Signature']);
|
|
124
|
+
const data = this.getWorkflowStaticData('node');
|
|
125
|
+
const secret = data.endpointSecret;
|
|
126
|
+
if (!signature || !secret) {
|
|
127
|
+
// Missing secret usually means the workflow was activated without
|
|
128
|
+
// the create() hook completing — return 400 so Exayard retries
|
|
129
|
+
// until the secret is in place.
|
|
130
|
+
return { webhookResponse: { status: 400, body: { error: 'missing_signature_or_secret' } } };
|
|
131
|
+
}
|
|
132
|
+
const rawBody = JSON.stringify(this.getBodyData());
|
|
133
|
+
try {
|
|
134
|
+
const event = await (0, vendor_1.constructWebhookEvent)(rawBody, signature, secret);
|
|
135
|
+
return { workflowData: [[{ json: event }]] };
|
|
136
|
+
}
|
|
137
|
+
catch (err) {
|
|
138
|
+
if (err instanceof vendor_1.WebhookSignatureError) {
|
|
139
|
+
// 401 tells Exayard the signature failed; it will retry with
|
|
140
|
+
// exponential backoff up to 3 days, but in practice signature
|
|
141
|
+
// failures mean a config issue and should be loud.
|
|
142
|
+
return { webhookResponse: { status: 401, body: { error: err.code, detail: err.message } } };
|
|
143
|
+
}
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
exports.ExayardTrigger = ExayardTrigger;
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 60 60" width="60" height="60">
|
|
2
|
+
<rect width="60" height="60" rx="12" fill="#0F1115"/>
|
|
3
|
+
<text x="30" y="38" font-family="-apple-system, system-ui, sans-serif" font-weight="700" font-size="24" text-anchor="middle" fill="#F5F2EA">E</text>
|
|
4
|
+
<circle cx="48" cy="12" r="6" fill="#FBBF24"/>
|
|
5
|
+
</svg>
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Configuration for the Exayard SDK client.
|
|
3
|
+
*/
|
|
4
|
+
export interface ExayardOptions {
|
|
5
|
+
/** API key from /settings/api-keys. Either this or bearerToken must be set. */
|
|
6
|
+
apiKey?: string;
|
|
7
|
+
/** OAuth bearer token (for connected-app flows). */
|
|
8
|
+
bearerToken?: string;
|
|
9
|
+
/** Override the base URL. Defaults to https://api.exayard.com/v1. */
|
|
10
|
+
baseUrl?: string;
|
|
11
|
+
/** Per-request timeout in ms. Defaults to 60000. */
|
|
12
|
+
timeoutMs?: number;
|
|
13
|
+
/** Optional fetch override — for tests or custom runtimes. */
|
|
14
|
+
fetch?: typeof fetch;
|
|
15
|
+
}
|
|
16
|
+
export declare class Exayard {
|
|
17
|
+
private readonly apiKey?;
|
|
18
|
+
private readonly bearerToken?;
|
|
19
|
+
private readonly baseUrl;
|
|
20
|
+
private readonly timeoutMs;
|
|
21
|
+
private readonly fetchImpl;
|
|
22
|
+
constructor(opts?: ExayardOptions);
|
|
23
|
+
private request;
|
|
24
|
+
readonly me: {
|
|
25
|
+
get: () => Promise<unknown>;
|
|
26
|
+
};
|
|
27
|
+
readonly projects: {
|
|
28
|
+
list: (query: {
|
|
29
|
+
organizationId: string;
|
|
30
|
+
status?: string;
|
|
31
|
+
search?: string;
|
|
32
|
+
limit?: number;
|
|
33
|
+
cursor?: string;
|
|
34
|
+
}) => Promise<{
|
|
35
|
+
items: unknown[];
|
|
36
|
+
next_cursor?: string;
|
|
37
|
+
}>;
|
|
38
|
+
get: (id: string, query: {
|
|
39
|
+
organizationId: string;
|
|
40
|
+
}) => Promise<unknown>;
|
|
41
|
+
create: (body: {
|
|
42
|
+
organizationId: string;
|
|
43
|
+
name: string;
|
|
44
|
+
}, opts?: {
|
|
45
|
+
idempotencyKey?: string;
|
|
46
|
+
}) => Promise<{
|
|
47
|
+
id: string;
|
|
48
|
+
_id: string;
|
|
49
|
+
}>;
|
|
50
|
+
export: (id: string, query: {
|
|
51
|
+
organizationId: string;
|
|
52
|
+
}) => Promise<unknown>;
|
|
53
|
+
archive: (id: string, query: {
|
|
54
|
+
organizationId: string;
|
|
55
|
+
}) => Promise<void>;
|
|
56
|
+
};
|
|
57
|
+
readonly help: {
|
|
58
|
+
search: (body: {
|
|
59
|
+
query: string;
|
|
60
|
+
limit?: number;
|
|
61
|
+
section?: string;
|
|
62
|
+
}) => Promise<{
|
|
63
|
+
results: Array<{
|
|
64
|
+
id: string;
|
|
65
|
+
title: string;
|
|
66
|
+
description: string;
|
|
67
|
+
url: string;
|
|
68
|
+
score: number;
|
|
69
|
+
}>;
|
|
70
|
+
total: number;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
readonly webhooks: {
|
|
74
|
+
listEndpoints: (query: {
|
|
75
|
+
organizationId: string;
|
|
76
|
+
}) => Promise<unknown[]>;
|
|
77
|
+
createEndpoint: (body: {
|
|
78
|
+
organizationId: string;
|
|
79
|
+
url: string;
|
|
80
|
+
events: string[];
|
|
81
|
+
description?: string;
|
|
82
|
+
}) => Promise<{
|
|
83
|
+
id: string;
|
|
84
|
+
secret: string;
|
|
85
|
+
}>;
|
|
86
|
+
deleteEndpoint: (id: string, query: {
|
|
87
|
+
organizationId: string;
|
|
88
|
+
}) => Promise<void>;
|
|
89
|
+
listDeliveries: (id: string, query: {
|
|
90
|
+
organizationId: string;
|
|
91
|
+
limit?: number;
|
|
92
|
+
}) => Promise<unknown[]>;
|
|
93
|
+
/**
|
|
94
|
+
* Parse + verify an inbound webhook delivery. Throws WebhookSignatureError
|
|
95
|
+
* on any signature failure — always catch and return 400 to let us retry.
|
|
96
|
+
*/
|
|
97
|
+
constructEvent: <T = unknown>(rawBody: string, signatureHeader: string, secret: string, options?: {
|
|
98
|
+
now?: number;
|
|
99
|
+
}) => Promise<import("./webhooks").WebhookEvent<T>>;
|
|
100
|
+
};
|
|
101
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Exayard = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Vendored from @exayard/sdk (packages/sdk/src/client.ts).
|
|
6
|
+
*
|
|
7
|
+
* Copied in so this n8n community node has ZERO runtime dependencies, which is
|
|
8
|
+
* required for n8n verified-community-node eligibility. Keep in sync with the
|
|
9
|
+
* upstream SDK if the client surface changes.
|
|
10
|
+
*/
|
|
11
|
+
const error_1 = require("./error");
|
|
12
|
+
const webhooks_1 = require("./webhooks");
|
|
13
|
+
const UNSAFE_METHODS = new Set(['POST', 'PATCH', 'PUT', 'DELETE']);
|
|
14
|
+
const randomIdempotencyKey = () => {
|
|
15
|
+
// Good enough for SDK auto-retries; callers needing deterministic keys
|
|
16
|
+
// pass their own. Uses crypto.randomUUID in runtimes that have it and
|
|
17
|
+
// falls back to timestamp+random.
|
|
18
|
+
if (typeof crypto !== 'undefined' && 'randomUUID' in crypto) {
|
|
19
|
+
return `idem_${crypto.randomUUID().replace(/-/g, '')}`;
|
|
20
|
+
}
|
|
21
|
+
return `idem_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 10)}`;
|
|
22
|
+
};
|
|
23
|
+
class Exayard {
|
|
24
|
+
apiKey;
|
|
25
|
+
bearerToken;
|
|
26
|
+
baseUrl;
|
|
27
|
+
timeoutMs;
|
|
28
|
+
fetchImpl;
|
|
29
|
+
constructor(opts = {}) {
|
|
30
|
+
if (!opts.apiKey && !opts.bearerToken) {
|
|
31
|
+
throw new Error('Exayard: pass `apiKey` or `bearerToken` — see https://developers.exayard.com/api-reference.');
|
|
32
|
+
}
|
|
33
|
+
this.apiKey = opts.apiKey;
|
|
34
|
+
this.bearerToken = opts.bearerToken;
|
|
35
|
+
this.baseUrl = (opts.baseUrl ?? 'https://api.exayard.com/v1').replace(/\/$/, '');
|
|
36
|
+
this.timeoutMs = opts.timeoutMs ?? 60_000;
|
|
37
|
+
this.fetchImpl = opts.fetch ?? fetch;
|
|
38
|
+
}
|
|
39
|
+
// Internal request primitive. Public resource methods compose this.
|
|
40
|
+
async request(opts) {
|
|
41
|
+
const url = new URL(`${this.baseUrl}${opts.path}`);
|
|
42
|
+
if (opts.query) {
|
|
43
|
+
for (const [k, v] of Object.entries(opts.query)) {
|
|
44
|
+
if (v !== undefined)
|
|
45
|
+
url.searchParams.set(k, String(v));
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const headers = {
|
|
49
|
+
Accept: 'application/json',
|
|
50
|
+
Authorization: `Bearer ${this.apiKey ?? this.bearerToken}`
|
|
51
|
+
};
|
|
52
|
+
if (opts.body !== undefined)
|
|
53
|
+
headers['Content-Type'] = 'application/json';
|
|
54
|
+
// Idempotency-Key for unsafe methods — auto-generated unless caller
|
|
55
|
+
// pins one for replay-safe retries from their own retry loop.
|
|
56
|
+
const autoIdem = opts.autoIdempotencyKey ?? UNSAFE_METHODS.has(opts.method);
|
|
57
|
+
if (opts.idempotencyKey) {
|
|
58
|
+
headers['Idempotency-Key'] = opts.idempotencyKey;
|
|
59
|
+
}
|
|
60
|
+
else if (autoIdem) {
|
|
61
|
+
headers['Idempotency-Key'] = randomIdempotencyKey();
|
|
62
|
+
}
|
|
63
|
+
const ac = new AbortController();
|
|
64
|
+
const timeout = setTimeout(() => ac.abort(), this.timeoutMs);
|
|
65
|
+
let res;
|
|
66
|
+
try {
|
|
67
|
+
res = await this.fetchImpl(url.toString(), {
|
|
68
|
+
method: opts.method,
|
|
69
|
+
headers,
|
|
70
|
+
body: opts.body !== undefined ? JSON.stringify(opts.body) : undefined,
|
|
71
|
+
signal: ac.signal
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
finally {
|
|
75
|
+
clearTimeout(timeout);
|
|
76
|
+
}
|
|
77
|
+
if (!res.ok) {
|
|
78
|
+
const ct = res.headers.get('Content-Type') ?? '';
|
|
79
|
+
if (ct.includes('problem+json') || ct.includes('application/json')) {
|
|
80
|
+
const body = (await res.json());
|
|
81
|
+
throw new error_1.ExayardError(body);
|
|
82
|
+
}
|
|
83
|
+
// Fallback for non-JSON error pages (shouldn't happen against /v1 but
|
|
84
|
+
// could hit a CDN error page if the service is down).
|
|
85
|
+
const text = await res.text().catch(() => '');
|
|
86
|
+
throw new error_1.ExayardError({
|
|
87
|
+
type: 'https://developers.exayard.com/concepts/errors#transport_error',
|
|
88
|
+
title: 'Transport Error',
|
|
89
|
+
status: res.status,
|
|
90
|
+
detail: text.slice(0, 500) || `HTTP ${res.status}`,
|
|
91
|
+
code: 'transport_error',
|
|
92
|
+
doc_url: 'https://developers.exayard.com/concepts/errors#transport_error',
|
|
93
|
+
request_id: res.headers.get('X-Request-Id') ?? undefined
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
if (res.status === 204)
|
|
97
|
+
return undefined;
|
|
98
|
+
return (await res.json());
|
|
99
|
+
}
|
|
100
|
+
// ---------------------------------------------------------------------------
|
|
101
|
+
// Resource surface — a curated subset covering the flows agents and integrations
|
|
102
|
+
// most often hit. Full OpenAPI coverage can be layered on via generated code
|
|
103
|
+
// without changing the public shape of this client.
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
me = {
|
|
106
|
+
get: () => this.request({ method: 'GET', path: '/me' })
|
|
107
|
+
};
|
|
108
|
+
projects = {
|
|
109
|
+
list: (query) => this.request({ method: 'GET', path: '/projects', query }),
|
|
110
|
+
get: (id, query) => this.request({ method: 'GET', path: `/projects/${id}`, query }),
|
|
111
|
+
create: (body, opts = {}) => this.request({ method: 'POST', path: '/projects', body, idempotencyKey: opts.idempotencyKey }),
|
|
112
|
+
export: (id, query) => this.request({ method: 'GET', path: `/projects/${id}/export`, query }),
|
|
113
|
+
archive: (id, query) => this.request({ method: 'POST', path: `/projects/${id}/archive`, query })
|
|
114
|
+
};
|
|
115
|
+
help = {
|
|
116
|
+
search: (body) => this.request({ method: 'POST', path: '/help/search', body, autoIdempotencyKey: false })
|
|
117
|
+
};
|
|
118
|
+
webhooks = {
|
|
119
|
+
listEndpoints: (query) => this.request({ method: 'GET', path: '/webhook_endpoints', query }),
|
|
120
|
+
createEndpoint: (body) => this.request({ method: 'POST', path: '/webhook_endpoints', body }),
|
|
121
|
+
deleteEndpoint: (id, query) => this.request({ method: 'DELETE', path: `/webhook_endpoints/${id}`, query }),
|
|
122
|
+
listDeliveries: (id, query) => this.request({ method: 'GET', path: `/webhook_endpoints/${id}/deliveries`, query }),
|
|
123
|
+
/**
|
|
124
|
+
* Parse + verify an inbound webhook delivery. Throws WebhookSignatureError
|
|
125
|
+
* on any signature failure — always catch and return 400 to let us retry.
|
|
126
|
+
*/
|
|
127
|
+
constructEvent: webhooks_1.constructWebhookEvent
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
exports.Exayard = Exayard;
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored from @exayard/sdk (packages/sdk/src/error.ts).
|
|
3
|
+
*
|
|
4
|
+
* Copied in so this n8n community node has ZERO runtime dependencies, which is
|
|
5
|
+
* required for n8n verified-community-node eligibility. Keep in sync with the
|
|
6
|
+
* upstream SDK if the error envelope changes.
|
|
7
|
+
*
|
|
8
|
+
* Typed error surface for SDK consumers.
|
|
9
|
+
*
|
|
10
|
+
* Every non-2xx response from /v1 is parsed into an ExayardError so callers
|
|
11
|
+
* can branch on `err.code` (stable) rather than `err.message` (human). Shape
|
|
12
|
+
* matches the RFC 9457 Problem Details envelope — title/detail/instance are
|
|
13
|
+
* present, extensions (code/param/doc_url/request_id) come through intact.
|
|
14
|
+
*/
|
|
15
|
+
export interface ExayardErrorBody {
|
|
16
|
+
type: string;
|
|
17
|
+
title: string;
|
|
18
|
+
status: number;
|
|
19
|
+
detail: string;
|
|
20
|
+
instance?: string;
|
|
21
|
+
code: string;
|
|
22
|
+
param?: string;
|
|
23
|
+
doc_url?: string;
|
|
24
|
+
request_id?: string;
|
|
25
|
+
}
|
|
26
|
+
export declare class ExayardError extends Error {
|
|
27
|
+
readonly type: string;
|
|
28
|
+
readonly title: string;
|
|
29
|
+
readonly status: number;
|
|
30
|
+
readonly detail: string;
|
|
31
|
+
readonly instance?: string;
|
|
32
|
+
readonly code: string;
|
|
33
|
+
readonly param?: string;
|
|
34
|
+
readonly docUrl?: string;
|
|
35
|
+
readonly requestId?: string;
|
|
36
|
+
constructor(body: ExayardErrorBody);
|
|
37
|
+
isRateLimited(): boolean;
|
|
38
|
+
isUnauthenticated(): boolean;
|
|
39
|
+
isNotFound(): boolean;
|
|
40
|
+
isInsufficientScope(): boolean;
|
|
41
|
+
isIdempotencyConflict(): boolean;
|
|
42
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vendored from @exayard/sdk (packages/sdk/src/error.ts).
|
|
4
|
+
*
|
|
5
|
+
* Copied in so this n8n community node has ZERO runtime dependencies, which is
|
|
6
|
+
* required for n8n verified-community-node eligibility. Keep in sync with the
|
|
7
|
+
* upstream SDK if the error envelope changes.
|
|
8
|
+
*
|
|
9
|
+
* Typed error surface for SDK consumers.
|
|
10
|
+
*
|
|
11
|
+
* Every non-2xx response from /v1 is parsed into an ExayardError so callers
|
|
12
|
+
* can branch on `err.code` (stable) rather than `err.message` (human). Shape
|
|
13
|
+
* matches the RFC 9457 Problem Details envelope — title/detail/instance are
|
|
14
|
+
* present, extensions (code/param/doc_url/request_id) come through intact.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.ExayardError = void 0;
|
|
18
|
+
class ExayardError extends Error {
|
|
19
|
+
type;
|
|
20
|
+
title;
|
|
21
|
+
status;
|
|
22
|
+
detail;
|
|
23
|
+
instance;
|
|
24
|
+
code;
|
|
25
|
+
param;
|
|
26
|
+
docUrl;
|
|
27
|
+
requestId;
|
|
28
|
+
constructor(body) {
|
|
29
|
+
// Guard against upstream error pages that don't conform to Problem Details —
|
|
30
|
+
// a missing code/title/detail should still produce a useful message rather
|
|
31
|
+
// than the string "undefined (undefined): undefined".
|
|
32
|
+
const title = body.title ?? 'error';
|
|
33
|
+
const status = typeof body.status === 'number' ? body.status : 0;
|
|
34
|
+
const code = body.code ?? `http_${status || 'unknown'}`;
|
|
35
|
+
const detail = body.detail ?? '';
|
|
36
|
+
const message = detail
|
|
37
|
+
? `${title} (${code}): ${detail}`
|
|
38
|
+
: `HTTP ${status || 'unknown'}: ${title}`;
|
|
39
|
+
super(message);
|
|
40
|
+
this.name = 'ExayardError';
|
|
41
|
+
this.type = body.type ?? 'about:blank';
|
|
42
|
+
this.title = title;
|
|
43
|
+
this.status = status;
|
|
44
|
+
this.detail = detail;
|
|
45
|
+
this.instance = body.instance;
|
|
46
|
+
this.code = code;
|
|
47
|
+
this.param = body.param;
|
|
48
|
+
this.docUrl = body.doc_url;
|
|
49
|
+
this.requestId = body.request_id;
|
|
50
|
+
}
|
|
51
|
+
// Convenience guards for the most common branching points.
|
|
52
|
+
isRateLimited() {
|
|
53
|
+
return this.code === 'rate_limited' || this.status === 429;
|
|
54
|
+
}
|
|
55
|
+
isUnauthenticated() {
|
|
56
|
+
return this.code === 'unauthenticated' || this.status === 401;
|
|
57
|
+
}
|
|
58
|
+
isNotFound() {
|
|
59
|
+
return this.code === 'not_found' || this.status === 404;
|
|
60
|
+
}
|
|
61
|
+
isInsufficientScope() {
|
|
62
|
+
return this.code === 'insufficient_scope' || this.status === 403;
|
|
63
|
+
}
|
|
64
|
+
isIdempotencyConflict() {
|
|
65
|
+
return this.code === 'idempotency_key_reused';
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
exports.ExayardError = ExayardError;
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored copy of @exayard/sdk.
|
|
3
|
+
*
|
|
4
|
+
* The Exayard SDK is internal/unpublished, so its source is vendored here to
|
|
5
|
+
* keep this n8n community node free of runtime dependencies (a requirement for
|
|
6
|
+
* n8n verified-community-node eligibility). The four files under this directory
|
|
7
|
+
* mirror packages/sdk/src/{client,error,webhooks,index}.ts and have ZERO npm
|
|
8
|
+
* dependencies — they rely only on built-ins (fetch, URL, crypto, TextEncoder,
|
|
9
|
+
* btoa). Keep in sync with the upstream SDK.
|
|
10
|
+
*
|
|
11
|
+
* Usage:
|
|
12
|
+
*
|
|
13
|
+
* import { Exayard } from '../../vendor'
|
|
14
|
+
* const exa = new Exayard({ apiKey: '...' })
|
|
15
|
+
*/
|
|
16
|
+
export { Exayard } from './client';
|
|
17
|
+
export type { ExayardOptions } from './client';
|
|
18
|
+
export { ExayardError } from './error';
|
|
19
|
+
export type { ExayardErrorBody } from './error';
|
|
20
|
+
export { constructWebhookEvent, WebhookSignatureError } from './webhooks';
|
|
21
|
+
export type { WebhookEvent, WebhookEventType } from './webhooks';
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vendored copy of @exayard/sdk.
|
|
4
|
+
*
|
|
5
|
+
* The Exayard SDK is internal/unpublished, so its source is vendored here to
|
|
6
|
+
* keep this n8n community node free of runtime dependencies (a requirement for
|
|
7
|
+
* n8n verified-community-node eligibility). The four files under this directory
|
|
8
|
+
* mirror packages/sdk/src/{client,error,webhooks,index}.ts and have ZERO npm
|
|
9
|
+
* dependencies — they rely only on built-ins (fetch, URL, crypto, TextEncoder,
|
|
10
|
+
* btoa). Keep in sync with the upstream SDK.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
*
|
|
14
|
+
* import { Exayard } from '../../vendor'
|
|
15
|
+
* const exa = new Exayard({ apiKey: '...' })
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.WebhookSignatureError = exports.constructWebhookEvent = exports.ExayardError = exports.Exayard = void 0;
|
|
19
|
+
var client_1 = require("./client");
|
|
20
|
+
Object.defineProperty(exports, "Exayard", { enumerable: true, get: function () { return client_1.Exayard; } });
|
|
21
|
+
var error_1 = require("./error");
|
|
22
|
+
Object.defineProperty(exports, "ExayardError", { enumerable: true, get: function () { return error_1.ExayardError; } });
|
|
23
|
+
var webhooks_1 = require("./webhooks");
|
|
24
|
+
Object.defineProperty(exports, "constructWebhookEvent", { enumerable: true, get: function () { return webhooks_1.constructWebhookEvent; } });
|
|
25
|
+
Object.defineProperty(exports, "WebhookSignatureError", { enumerable: true, get: function () { return webhooks_1.WebhookSignatureError; } });
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Vendored from @exayard/sdk (packages/sdk/src/webhooks.ts).
|
|
3
|
+
*
|
|
4
|
+
* Copied in so this n8n community node has ZERO runtime dependencies, which is
|
|
5
|
+
* required for n8n verified-community-node eligibility. Keep in sync with the
|
|
6
|
+
* upstream SDK if the signing scheme changes.
|
|
7
|
+
*
|
|
8
|
+
* Webhook signature verification — Stripe-compatible pattern.
|
|
9
|
+
*
|
|
10
|
+
* Exayard signs every delivery with:
|
|
11
|
+
* Exayard-Signature: t=<unix>,v1=<base64-hmac-sha256>
|
|
12
|
+
*
|
|
13
|
+
* where the signed payload is `${t}.${rawBody}` and the HMAC secret is the
|
|
14
|
+
* endpoint's whsec_... value returned once at creation time.
|
|
15
|
+
*/
|
|
16
|
+
export type WebhookEventType = 'project.created' | 'project.updated' | 'project.archived' | 'assessment.started' | 'assessment.completed' | 'assessment.approved' | 'assessment.cancelled' | 'estimate.generated' | 'bid.generated' | 'file.processed';
|
|
17
|
+
export interface WebhookEvent<T = unknown> {
|
|
18
|
+
id: string;
|
|
19
|
+
type: WebhookEventType;
|
|
20
|
+
created: number;
|
|
21
|
+
data: T;
|
|
22
|
+
}
|
|
23
|
+
export declare class WebhookSignatureError extends Error {
|
|
24
|
+
readonly code: 'invalid_signature' | 'replay_window_exceeded' | 'malformed_header';
|
|
25
|
+
constructor(code: WebhookSignatureError['code'], message: string);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Verify a webhook delivery and parse its payload as a typed WebhookEvent.
|
|
29
|
+
*
|
|
30
|
+
* Throws WebhookSignatureError with a stable `code` on failure:
|
|
31
|
+
* - malformed_header: Exayard-Signature isn't in t=…,v1=… form
|
|
32
|
+
* - replay_window_exceeded: timestamp is >5 minutes from now
|
|
33
|
+
* - invalid_signature: digest doesn't match
|
|
34
|
+
*/
|
|
35
|
+
export declare const constructWebhookEvent: <T = unknown>(rawBody: string, signatureHeader: string, secret: string, options?: {
|
|
36
|
+
now?: number;
|
|
37
|
+
}) => Promise<WebhookEvent<T>>;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Vendored from @exayard/sdk (packages/sdk/src/webhooks.ts).
|
|
4
|
+
*
|
|
5
|
+
* Copied in so this n8n community node has ZERO runtime dependencies, which is
|
|
6
|
+
* required for n8n verified-community-node eligibility. Keep in sync with the
|
|
7
|
+
* upstream SDK if the signing scheme changes.
|
|
8
|
+
*
|
|
9
|
+
* Webhook signature verification — Stripe-compatible pattern.
|
|
10
|
+
*
|
|
11
|
+
* Exayard signs every delivery with:
|
|
12
|
+
* Exayard-Signature: t=<unix>,v1=<base64-hmac-sha256>
|
|
13
|
+
*
|
|
14
|
+
* where the signed payload is `${t}.${rawBody}` and the HMAC secret is the
|
|
15
|
+
* endpoint's whsec_... value returned once at creation time.
|
|
16
|
+
*/
|
|
17
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
18
|
+
exports.constructWebhookEvent = exports.WebhookSignatureError = void 0;
|
|
19
|
+
class WebhookSignatureError extends Error {
|
|
20
|
+
code;
|
|
21
|
+
constructor(code, message) {
|
|
22
|
+
super(message);
|
|
23
|
+
this.name = 'WebhookSignatureError';
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
exports.WebhookSignatureError = WebhookSignatureError;
|
|
28
|
+
// 5-minute replay window matches the server-side signing convention in
|
|
29
|
+
// packages/backend/convex/webhooks.ts.
|
|
30
|
+
const REPLAY_WINDOW_SECONDS = 5 * 60;
|
|
31
|
+
const parseSignatureHeader = (header) => {
|
|
32
|
+
const parts = header.split(',').map(s => s.trim());
|
|
33
|
+
let t;
|
|
34
|
+
let v1;
|
|
35
|
+
for (const part of parts) {
|
|
36
|
+
const eq = part.indexOf('=');
|
|
37
|
+
if (eq === -1)
|
|
38
|
+
continue;
|
|
39
|
+
const k = part.slice(0, eq);
|
|
40
|
+
const v = part.slice(eq + 1);
|
|
41
|
+
if (k === 't')
|
|
42
|
+
t = parseInt(v, 10);
|
|
43
|
+
if (k === 'v1')
|
|
44
|
+
v1 = v;
|
|
45
|
+
}
|
|
46
|
+
if (t === undefined || Number.isNaN(t) || !v1) {
|
|
47
|
+
throw new WebhookSignatureError('malformed_header', `Exayard-Signature must be "t=<unix>,v1=<digest>" — got "${header}".`);
|
|
48
|
+
}
|
|
49
|
+
return { t, v1 };
|
|
50
|
+
};
|
|
51
|
+
const hmacSha256Base64 = async (secret, payload) => {
|
|
52
|
+
const key = await crypto.subtle.importKey('raw', new TextEncoder().encode(secret), { name: 'HMAC', hash: 'SHA-256' }, false, ['sign']);
|
|
53
|
+
const digest = await crypto.subtle.sign('HMAC', key, new TextEncoder().encode(payload));
|
|
54
|
+
const bytes = new Uint8Array(digest);
|
|
55
|
+
let str = '';
|
|
56
|
+
for (let i = 0; i < bytes.length; i++)
|
|
57
|
+
str += String.fromCharCode(bytes[i]);
|
|
58
|
+
return btoa(str);
|
|
59
|
+
};
|
|
60
|
+
// Constant-time base64 compare. Short-circuit on length mismatch is fine —
|
|
61
|
+
// attacker already knows the digest length (SHA-256 = 44 base64 chars).
|
|
62
|
+
const safeCompare = (a, b) => {
|
|
63
|
+
if (a.length !== b.length)
|
|
64
|
+
return false;
|
|
65
|
+
let result = 0;
|
|
66
|
+
for (let i = 0; i < a.length; i++)
|
|
67
|
+
result |= a.charCodeAt(i) ^ b.charCodeAt(i);
|
|
68
|
+
return result === 0;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* Verify a webhook delivery and parse its payload as a typed WebhookEvent.
|
|
72
|
+
*
|
|
73
|
+
* Throws WebhookSignatureError with a stable `code` on failure:
|
|
74
|
+
* - malformed_header: Exayard-Signature isn't in t=…,v1=… form
|
|
75
|
+
* - replay_window_exceeded: timestamp is >5 minutes from now
|
|
76
|
+
* - invalid_signature: digest doesn't match
|
|
77
|
+
*/
|
|
78
|
+
const constructWebhookEvent = async (rawBody, signatureHeader, secret, options = {}) => {
|
|
79
|
+
const { t, v1 } = parseSignatureHeader(signatureHeader);
|
|
80
|
+
const nowSec = Math.floor((options.now ?? Date.now()) / 1000);
|
|
81
|
+
if (Math.abs(nowSec - t) > REPLAY_WINDOW_SECONDS) {
|
|
82
|
+
throw new WebhookSignatureError('replay_window_exceeded', `Signature timestamp ${t} is outside the ${REPLAY_WINDOW_SECONDS}-second window (now=${nowSec}).`);
|
|
83
|
+
}
|
|
84
|
+
const expected = await hmacSha256Base64(secret, `${t}.${rawBody}`);
|
|
85
|
+
if (!safeCompare(expected, v1)) {
|
|
86
|
+
throw new WebhookSignatureError('invalid_signature', 'HMAC digest mismatch. Check the endpoint secret.');
|
|
87
|
+
}
|
|
88
|
+
return JSON.parse(rawBody);
|
|
89
|
+
};
|
|
90
|
+
exports.constructWebhookEvent = constructWebhookEvent;
|
package/package.json
ADDED
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-takeoff-pro",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "n8n community node for Exayard — AI-powered construction takeoffs, estimates, and bids.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"exayard",
|
|
8
|
+
"construction",
|
|
9
|
+
"estimating",
|
|
10
|
+
"takeoff"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"homepage": "https://exayard.com/integrations/n8n",
|
|
14
|
+
"author": {
|
|
15
|
+
"name": "Exayard",
|
|
16
|
+
"email": "support@exayard.com",
|
|
17
|
+
"url": "https://exayard.com"
|
|
18
|
+
},
|
|
19
|
+
"repository": {
|
|
20
|
+
"type": "git",
|
|
21
|
+
"url": "https://github.com/ShortGenius/exayard.git",
|
|
22
|
+
"directory": "packages/n8n-node"
|
|
23
|
+
},
|
|
24
|
+
"engines": {
|
|
25
|
+
"node": ">=20"
|
|
26
|
+
},
|
|
27
|
+
"main": "index.js",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"build": "tsc -p tsconfig.json && yarn copy-icons",
|
|
30
|
+
"copy-icons": "node -e \"const fs=require('fs');const path=require('path');for (const dir of ['nodes/Exayard','nodes/ExayardTrigger']) { fs.mkdirSync(path.join('dist',dir),{recursive:true}); for (const f of fs.readdirSync(dir).filter(n => n.endsWith('.svg'))) { fs.copyFileSync(path.join(dir,f), path.join('dist',dir,f)); } }\"",
|
|
31
|
+
"typecheck": "tsc --noEmit"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist"
|
|
35
|
+
],
|
|
36
|
+
"n8n": {
|
|
37
|
+
"n8nNodesApiVersion": 1,
|
|
38
|
+
"credentials": [
|
|
39
|
+
"dist/credentials/ExayardApi.credentials.js"
|
|
40
|
+
],
|
|
41
|
+
"nodes": [
|
|
42
|
+
"dist/nodes/Exayard/Exayard.node.js",
|
|
43
|
+
"dist/nodes/ExayardTrigger/ExayardTrigger.node.js"
|
|
44
|
+
]
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"n8n-workflow": "*"
|
|
48
|
+
},
|
|
49
|
+
"devDependencies": {
|
|
50
|
+
"@exayard/config-typescript": "*",
|
|
51
|
+
"@types/node": "22.19.0",
|
|
52
|
+
"n8n-workflow": "1.91.0",
|
|
53
|
+
"typescript": "5.9.3"
|
|
54
|
+
}
|
|
55
|
+
}
|