n8n-nodes-hookpost 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 +71 -0
- package/dist/credentials/HookpostApi.credentials.d.ts +9 -0
- package/dist/credentials/HookpostApi.credentials.js +48 -0
- package/dist/nodes/Hookpost/Hookpost.node.d.ts +10 -0
- package/dist/nodes/Hookpost/Hookpost.node.js +264 -0
- package/dist/nodes/Hookpost/hookpost.svg +3 -0
- package/package.json +45 -0
package/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# n8n-nodes-hookpost
|
|
2
|
+
|
|
3
|
+
An [n8n](https://n8n.io) community node for [Hookpost](https://hookpost.hookstep.in) — schedule and publish social media posts across 30+ networks from your workflows.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
In n8n: **Settings → Community Nodes → Install**, then enter:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
n8n-nodes-hookpost
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
Or install manually into your n8n instance:
|
|
14
|
+
|
|
15
|
+
```bash
|
|
16
|
+
npm install n8n-nodes-hookpost
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
## Credentials
|
|
20
|
+
|
|
21
|
+
Create a **Hookpost API** credential:
|
|
22
|
+
|
|
23
|
+
| Field | Value |
|
|
24
|
+
|---|---|
|
|
25
|
+
| **API Key** | From Hookpost → **Settings → Public API**. An OAuth access token (`pos_…`) also works. |
|
|
26
|
+
| **Base URL** | `https://hookpost.hookstep.in/api` (change only if you self-host) |
|
|
27
|
+
|
|
28
|
+
Press **Test** — it calls `/public/v1/is-connected` to confirm the key before you build anything with it.
|
|
29
|
+
|
|
30
|
+
> Keep the key server-side. It grants full access to your organisation's channels and posts.
|
|
31
|
+
|
|
32
|
+
## Operations
|
|
33
|
+
|
|
34
|
+
| Operation | What it does |
|
|
35
|
+
|---|---|
|
|
36
|
+
| **Create Post** | Schedule, publish now, or save as a draft |
|
|
37
|
+
| **List Posts** | List posts between two dates |
|
|
38
|
+
| **Delete Post** | Delete a post by id |
|
|
39
|
+
| **List Channels** | List your connected channels |
|
|
40
|
+
| **Find Free Slot** | Next free slot in your posting schedule |
|
|
41
|
+
| **Upload From URL** | Add media from a public URL |
|
|
42
|
+
|
|
43
|
+
The channel dropdown loads your real connected channels — no need to look up ids by hand.
|
|
44
|
+
|
|
45
|
+
## Notes
|
|
46
|
+
|
|
47
|
+
**Scheduling.** Leave **Date** empty on a scheduled post and the node asks Hookpost for the next free slot in your posting schedule, matching what the app does when you press Create Post.
|
|
48
|
+
|
|
49
|
+
**Channel settings.** Some channels need extra fields — YouTube wants a title and visibility, Pinterest wants a board. Put them in **Additional Fields → Channel Settings (JSON)**:
|
|
50
|
+
|
|
51
|
+
```json
|
|
52
|
+
{
|
|
53
|
+
"__type": "youtube",
|
|
54
|
+
"title": "My video",
|
|
55
|
+
"type": "private",
|
|
56
|
+
"selfDeclaredMadeForKids": "no"
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Call `/public/v1/integration-settings/:id` to see what a given channel expects.
|
|
61
|
+
|
|
62
|
+
**Errors.** Enable *Continue On Fail* on the node to pass errors downstream as `{ "error": "..." }` instead of stopping the workflow.
|
|
63
|
+
|
|
64
|
+
## Documentation
|
|
65
|
+
|
|
66
|
+
- [Public API reference](https://hookpost.hookstep.in/docs/public-api)
|
|
67
|
+
- [OAuth apps](https://hookpost.hookstep.in/docs/oauth) — for integrations acting on other people's accounts
|
|
68
|
+
|
|
69
|
+
## Licence
|
|
70
|
+
|
|
71
|
+
AGPL-3.0
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { IAuthenticateGeneric, ICredentialTestRequest, ICredentialType, INodeProperties } from 'n8n-workflow';
|
|
2
|
+
export declare class HookpostApi implements ICredentialType {
|
|
3
|
+
name: string;
|
|
4
|
+
displayName: string;
|
|
5
|
+
documentationUrl: string;
|
|
6
|
+
properties: INodeProperties[];
|
|
7
|
+
authenticate: IAuthenticateGeneric;
|
|
8
|
+
test: ICredentialTestRequest;
|
|
9
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.HookpostApi = void 0;
|
|
4
|
+
class HookpostApi {
|
|
5
|
+
constructor() {
|
|
6
|
+
this.name = 'hookpostApi';
|
|
7
|
+
this.displayName = 'Hookpost API';
|
|
8
|
+
this.documentationUrl = 'https://hookpost.hookstep.in/docs/public-api';
|
|
9
|
+
this.properties = [
|
|
10
|
+
{
|
|
11
|
+
displayName: 'API Key',
|
|
12
|
+
name: 'apiKey',
|
|
13
|
+
type: 'string',
|
|
14
|
+
typeOptions: { password: true },
|
|
15
|
+
default: '',
|
|
16
|
+
required: true,
|
|
17
|
+
description: 'Found in Hookpost under Settings → Public API. An OAuth access token (pos_…) works here too.',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
displayName: 'Base URL',
|
|
21
|
+
name: 'baseUrl',
|
|
22
|
+
type: 'string',
|
|
23
|
+
default: 'https://hookpost.hookstep.in/api',
|
|
24
|
+
required: true,
|
|
25
|
+
description: 'Change this only if you self-host Hookpost. Include /api, and no trailing slash.',
|
|
26
|
+
},
|
|
27
|
+
];
|
|
28
|
+
// Hookpost expects the key as a bare Authorization header - no "Bearer"
|
|
29
|
+
// prefix. Sending one is rejected with 401.
|
|
30
|
+
this.authenticate = {
|
|
31
|
+
type: 'generic',
|
|
32
|
+
properties: {
|
|
33
|
+
headers: {
|
|
34
|
+
Authorization: '={{$credentials.apiKey}}',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
};
|
|
38
|
+
// Lets n8n's "Test" button verify the key without creating anything.
|
|
39
|
+
this.test = {
|
|
40
|
+
request: {
|
|
41
|
+
baseURL: '={{$credentials.baseUrl}}',
|
|
42
|
+
url: '/public/v1/is-connected',
|
|
43
|
+
method: 'GET',
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
exports.HookpostApi = HookpostApi;
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { IExecuteFunctions, ILoadOptionsFunctions, INodeExecutionData, INodePropertyOptions, INodeType, INodeTypeDescription } from 'n8n-workflow';
|
|
2
|
+
export declare class Hookpost implements INodeType {
|
|
3
|
+
description: INodeTypeDescription;
|
|
4
|
+
methods: {
|
|
5
|
+
loadOptions: {
|
|
6
|
+
getChannels(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]>;
|
|
7
|
+
};
|
|
8
|
+
};
|
|
9
|
+
execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]>;
|
|
10
|
+
}
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Hookpost = void 0;
|
|
4
|
+
const n8n_workflow_1 = require("n8n-workflow");
|
|
5
|
+
async function hookpostRequest(method, path, body, qs) {
|
|
6
|
+
const credentials = await this.getCredentials('hookpostApi');
|
|
7
|
+
const baseUrl = String(credentials.baseUrl || '').replace(/\/+$/, '');
|
|
8
|
+
return this.helpers.httpRequestWithAuthentication.call(this, 'hookpostApi', {
|
|
9
|
+
method,
|
|
10
|
+
url: `${baseUrl}/public/v1${path}`,
|
|
11
|
+
body,
|
|
12
|
+
qs,
|
|
13
|
+
json: true,
|
|
14
|
+
});
|
|
15
|
+
}
|
|
16
|
+
class Hookpost {
|
|
17
|
+
constructor() {
|
|
18
|
+
this.description = {
|
|
19
|
+
displayName: 'Hookpost',
|
|
20
|
+
name: 'hookpost',
|
|
21
|
+
icon: 'file:hookpost.svg',
|
|
22
|
+
group: ['output'],
|
|
23
|
+
version: 1,
|
|
24
|
+
subtitle: '={{$parameter["operation"]}}',
|
|
25
|
+
description: 'Schedule and publish social media posts with Hookpost',
|
|
26
|
+
defaults: { name: 'Hookpost' },
|
|
27
|
+
inputs: ['main'],
|
|
28
|
+
outputs: ['main'],
|
|
29
|
+
credentials: [{ name: 'hookpostApi', required: true }],
|
|
30
|
+
properties: [
|
|
31
|
+
{
|
|
32
|
+
displayName: 'Operation',
|
|
33
|
+
name: 'operation',
|
|
34
|
+
type: 'options',
|
|
35
|
+
noDataExpression: true,
|
|
36
|
+
options: [
|
|
37
|
+
{ name: 'Create Post', value: 'createPost', description: 'Schedule, publish now, or save a draft', action: 'Create a post' },
|
|
38
|
+
{ name: 'List Posts', value: 'listPosts', description: 'List posts in a date range', action: 'List posts' },
|
|
39
|
+
{ name: 'Delete Post', value: 'deletePost', description: 'Delete a post by id', action: 'Delete a post' },
|
|
40
|
+
{ name: 'List Channels', value: 'listChannels', description: 'List connected channels', action: 'List channels' },
|
|
41
|
+
{ name: 'Find Free Slot', value: 'findSlot', description: 'Next free slot in your posting schedule', action: 'Find a free slot' },
|
|
42
|
+
{ name: 'Upload From URL', value: 'uploadFromUrl', description: 'Add media from a remote URL', action: 'Upload media from a URL' },
|
|
43
|
+
],
|
|
44
|
+
default: 'createPost',
|
|
45
|
+
},
|
|
46
|
+
// ---- Create Post ----
|
|
47
|
+
{
|
|
48
|
+
displayName: 'Channel Name or ID',
|
|
49
|
+
name: 'integrationId',
|
|
50
|
+
type: 'options',
|
|
51
|
+
typeOptions: { loadOptionsMethod: 'getChannels' },
|
|
52
|
+
default: '',
|
|
53
|
+
required: true,
|
|
54
|
+
displayOptions: { show: { operation: ['createPost'] } },
|
|
55
|
+
description: 'The connected channel to post to. Choose from the list, or specify an ID using an expression.',
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
displayName: 'Content',
|
|
59
|
+
name: 'content',
|
|
60
|
+
type: 'string',
|
|
61
|
+
typeOptions: { rows: 4 },
|
|
62
|
+
default: '',
|
|
63
|
+
required: true,
|
|
64
|
+
displayOptions: { show: { operation: ['createPost'] } },
|
|
65
|
+
description: 'The post text',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
displayName: 'When',
|
|
69
|
+
name: 'type',
|
|
70
|
+
type: 'options',
|
|
71
|
+
options: [
|
|
72
|
+
{ name: 'Schedule', value: 'schedule', description: 'Queue it for the date below' },
|
|
73
|
+
{ name: 'Publish Now', value: 'now', description: 'Publish immediately' },
|
|
74
|
+
{ name: 'Save as Draft', value: 'draft', description: 'Save without publishing' },
|
|
75
|
+
],
|
|
76
|
+
default: 'schedule',
|
|
77
|
+
displayOptions: { show: { operation: ['createPost'] } },
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
displayName: 'Date',
|
|
81
|
+
name: 'date',
|
|
82
|
+
type: 'dateTime',
|
|
83
|
+
default: '',
|
|
84
|
+
displayOptions: { show: { operation: ['createPost'], type: ['schedule'] } },
|
|
85
|
+
description: 'When to publish. Leave empty to use the next free slot in your schedule.',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
displayName: 'Additional Fields',
|
|
89
|
+
name: 'additionalFields',
|
|
90
|
+
type: 'collection',
|
|
91
|
+
placeholder: 'Add Field',
|
|
92
|
+
default: {},
|
|
93
|
+
displayOptions: { show: { operation: ['createPost'] } },
|
|
94
|
+
options: [
|
|
95
|
+
{
|
|
96
|
+
displayName: 'Image URLs',
|
|
97
|
+
name: 'imageUrls',
|
|
98
|
+
type: 'string',
|
|
99
|
+
default: '',
|
|
100
|
+
description: 'Comma-separated media URLs already uploaded to Hookpost',
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
displayName: 'Channel Settings (JSON)',
|
|
104
|
+
name: 'settings',
|
|
105
|
+
type: 'json',
|
|
106
|
+
default: '',
|
|
107
|
+
description: 'Per-channel settings. Some channels require these - YouTube needs a title and visibility, Pinterest needs a board. Call integration-settings for the channel to see its schema.',
|
|
108
|
+
},
|
|
109
|
+
],
|
|
110
|
+
},
|
|
111
|
+
// ---- List Posts ----
|
|
112
|
+
{
|
|
113
|
+
displayName: 'Start Date',
|
|
114
|
+
name: 'startDate',
|
|
115
|
+
type: 'dateTime',
|
|
116
|
+
default: '',
|
|
117
|
+
required: true,
|
|
118
|
+
displayOptions: { show: { operation: ['listPosts'] } },
|
|
119
|
+
},
|
|
120
|
+
{
|
|
121
|
+
displayName: 'End Date',
|
|
122
|
+
name: 'endDate',
|
|
123
|
+
type: 'dateTime',
|
|
124
|
+
default: '',
|
|
125
|
+
required: true,
|
|
126
|
+
displayOptions: { show: { operation: ['listPosts'] } },
|
|
127
|
+
},
|
|
128
|
+
// ---- Delete Post ----
|
|
129
|
+
{
|
|
130
|
+
displayName: 'Post ID',
|
|
131
|
+
name: 'postId',
|
|
132
|
+
type: 'string',
|
|
133
|
+
default: '',
|
|
134
|
+
required: true,
|
|
135
|
+
displayOptions: { show: { operation: ['deletePost'] } },
|
|
136
|
+
},
|
|
137
|
+
// ---- Find Free Slot ----
|
|
138
|
+
{
|
|
139
|
+
displayName: 'Channel Name or ID',
|
|
140
|
+
name: 'slotIntegrationId',
|
|
141
|
+
type: 'options',
|
|
142
|
+
typeOptions: { loadOptionsMethod: 'getChannels' },
|
|
143
|
+
default: '',
|
|
144
|
+
required: true,
|
|
145
|
+
displayOptions: { show: { operation: ['findSlot'] } },
|
|
146
|
+
description: 'Choose from the list, or specify an ID using an expression',
|
|
147
|
+
},
|
|
148
|
+
// ---- Upload From URL ----
|
|
149
|
+
{
|
|
150
|
+
displayName: 'File URL',
|
|
151
|
+
name: 'fileUrl',
|
|
152
|
+
type: 'string',
|
|
153
|
+
default: '',
|
|
154
|
+
required: true,
|
|
155
|
+
displayOptions: { show: { operation: ['uploadFromUrl'] } },
|
|
156
|
+
description: 'Publicly reachable URL of the image or video to add',
|
|
157
|
+
},
|
|
158
|
+
],
|
|
159
|
+
};
|
|
160
|
+
this.methods = {
|
|
161
|
+
loadOptions: {
|
|
162
|
+
async getChannels() {
|
|
163
|
+
const channels = await hookpostRequest.call(this, 'GET', '/integrations');
|
|
164
|
+
const list = Array.isArray(channels) ? channels : [];
|
|
165
|
+
return list.map((c) => ({
|
|
166
|
+
name: `${c.name} (${c.identifier || c.providerIdentifier})`,
|
|
167
|
+
value: c.id,
|
|
168
|
+
}));
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
async execute() {
|
|
174
|
+
const items = this.getInputData();
|
|
175
|
+
const out = [];
|
|
176
|
+
for (let i = 0; i < items.length; i++) {
|
|
177
|
+
const operation = this.getNodeParameter('operation', i);
|
|
178
|
+
try {
|
|
179
|
+
let result;
|
|
180
|
+
if (operation === 'createPost') {
|
|
181
|
+
const integrationId = this.getNodeParameter('integrationId', i);
|
|
182
|
+
const content = this.getNodeParameter('content', i);
|
|
183
|
+
const type = this.getNodeParameter('type', i);
|
|
184
|
+
const extra = this.getNodeParameter('additionalFields', i, {});
|
|
185
|
+
// A schedule with no date would otherwise be rejected, so fall back to
|
|
186
|
+
// the next free slot - the same default the app itself uses.
|
|
187
|
+
let date = this.getNodeParameter('date', i, '');
|
|
188
|
+
if (type === 'schedule' && !date) {
|
|
189
|
+
const slot = await hookpostRequest.call(this, 'GET', `/find-slot/${integrationId}`);
|
|
190
|
+
date = slot?.date;
|
|
191
|
+
}
|
|
192
|
+
const image = (extra.imageUrls || '')
|
|
193
|
+
.split(',')
|
|
194
|
+
.map((u) => u.trim())
|
|
195
|
+
.filter(Boolean)
|
|
196
|
+
.map((path) => ({ id: path, path }));
|
|
197
|
+
let settings = undefined;
|
|
198
|
+
if (extra.settings) {
|
|
199
|
+
settings =
|
|
200
|
+
typeof extra.settings === 'string'
|
|
201
|
+
? JSON.parse(extra.settings)
|
|
202
|
+
: extra.settings;
|
|
203
|
+
}
|
|
204
|
+
result = await hookpostRequest.call(this, 'POST', '/posts', {
|
|
205
|
+
type,
|
|
206
|
+
date: date || new Date().toISOString(),
|
|
207
|
+
shortLink: false,
|
|
208
|
+
tags: [],
|
|
209
|
+
posts: [
|
|
210
|
+
{
|
|
211
|
+
integration: { id: integrationId },
|
|
212
|
+
value: [{ content, image }],
|
|
213
|
+
...(settings ? { settings } : {}),
|
|
214
|
+
},
|
|
215
|
+
],
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
else if (operation === 'listPosts') {
|
|
219
|
+
result = await hookpostRequest.call(this, 'GET', '/posts', undefined, {
|
|
220
|
+
startDate: this.getNodeParameter('startDate', i),
|
|
221
|
+
endDate: this.getNodeParameter('endDate', i),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
else if (operation === 'deletePost') {
|
|
225
|
+
const postId = this.getNodeParameter('postId', i);
|
|
226
|
+
result = await hookpostRequest.call(this, 'DELETE', `/posts/${postId}`);
|
|
227
|
+
}
|
|
228
|
+
else if (operation === 'listChannels') {
|
|
229
|
+
result = await hookpostRequest.call(this, 'GET', '/integrations');
|
|
230
|
+
}
|
|
231
|
+
else if (operation === 'findSlot') {
|
|
232
|
+
const id = this.getNodeParameter('slotIntegrationId', i);
|
|
233
|
+
result = await hookpostRequest.call(this, 'GET', `/find-slot/${id}`);
|
|
234
|
+
}
|
|
235
|
+
else if (operation === 'uploadFromUrl') {
|
|
236
|
+
result = await hookpostRequest.call(this, 'POST', '/upload-from-url', {
|
|
237
|
+
url: this.getNodeParameter('fileUrl', i),
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
else {
|
|
241
|
+
throw new n8n_workflow_1.NodeOperationError(this.getNode(), `Unknown operation: ${operation}`, { itemIndex: i });
|
|
242
|
+
}
|
|
243
|
+
if (Array.isArray(result)) {
|
|
244
|
+
out.push(...result.map((r) => ({ json: r, pairedItem: { item: i } })));
|
|
245
|
+
}
|
|
246
|
+
else {
|
|
247
|
+
out.push({ json: result ?? {}, pairedItem: { item: i } });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
catch (error) {
|
|
251
|
+
if (this.continueOnFail()) {
|
|
252
|
+
out.push({
|
|
253
|
+
json: { error: error.message },
|
|
254
|
+
pairedItem: { item: i },
|
|
255
|
+
});
|
|
256
|
+
continue;
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return [out];
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
exports.Hookpost = Hookpost;
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "n8n-nodes-hookpost",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "n8n community node for Hookpost - schedule and publish social media posts across 30+ networks",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"n8n-community-node-package",
|
|
7
|
+
"n8n",
|
|
8
|
+
"hookpost",
|
|
9
|
+
"social media",
|
|
10
|
+
"scheduling"
|
|
11
|
+
],
|
|
12
|
+
"license": "AGPL-3.0",
|
|
13
|
+
"author": "JR Consulting Co.",
|
|
14
|
+
"homepage": "https://hookpost.hookstep.in/docs/public-api",
|
|
15
|
+
"repository": {
|
|
16
|
+
"type": "git",
|
|
17
|
+
"url": "git+https://github.com/jatinder14/hookpost.git",
|
|
18
|
+
"directory": "apps/n8n-node"
|
|
19
|
+
},
|
|
20
|
+
"main": "index.js",
|
|
21
|
+
"scripts": {
|
|
22
|
+
"build": "tsc && node copy-assets.js",
|
|
23
|
+
"prepublishOnly": "npm run build"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"dist"
|
|
27
|
+
],
|
|
28
|
+
"n8n": {
|
|
29
|
+
"n8nNodesApiVersion": 1,
|
|
30
|
+
"credentials": [
|
|
31
|
+
"dist/credentials/HookpostApi.credentials.js"
|
|
32
|
+
],
|
|
33
|
+
"nodes": [
|
|
34
|
+
"dist/nodes/Hookpost/Hookpost.node.js"
|
|
35
|
+
]
|
|
36
|
+
},
|
|
37
|
+
"peerDependencies": {
|
|
38
|
+
"n8n-workflow": "*"
|
|
39
|
+
},
|
|
40
|
+
"devDependencies": {
|
|
41
|
+
"n8n-workflow": "^1.82.0",
|
|
42
|
+
"typescript": "^5.6.0",
|
|
43
|
+
"@types/node": "^22.0.0"
|
|
44
|
+
}
|
|
45
|
+
}
|