@cocreate/api 1.22.4 → 1.23.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/.github/workflows/automated.yml +25 -13
- package/.github/workflows/manual.yml +1 -2
- package/CHANGELOG.md +25 -0
- package/package.json +39 -19
- package/release.config.js +12 -4
- package/src/server.js +651 -0
- /package/src/{index.js → client.js} +0 -0
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
name: Automated Workflow
|
|
2
|
+
|
|
2
3
|
on:
|
|
3
4
|
push:
|
|
4
5
|
branches:
|
|
5
6
|
- master
|
|
7
|
+
|
|
6
8
|
jobs:
|
|
7
9
|
about:
|
|
8
10
|
runs-on: ubuntu-latest
|
|
@@ -18,27 +20,37 @@ jobs:
|
|
|
18
20
|
with:
|
|
19
21
|
direction: overwrite-github
|
|
20
22
|
githubToken: "${{ secrets.GITHUB }}"
|
|
23
|
+
|
|
21
24
|
release:
|
|
22
25
|
runs-on: ubuntu-latest
|
|
23
26
|
steps:
|
|
24
27
|
- name: Checkout
|
|
25
|
-
uses: actions/checkout@
|
|
26
|
-
- name: Setup Node.js
|
|
27
|
-
uses: actions/setup-node@v3
|
|
28
|
+
uses: actions/checkout@v4
|
|
28
29
|
with:
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
30
|
+
fetch-depth: 0 # Required so semantic-release can trace git tags/history
|
|
31
|
+
|
|
32
|
+
- name: Setup Node.js
|
|
33
|
+
uses: actions/setup-node@v4
|
|
33
34
|
with:
|
|
34
|
-
|
|
35
|
-
|
|
35
|
+
node-version: 22
|
|
36
|
+
|
|
37
|
+
- name: Install Semantic Release & Plugins
|
|
38
|
+
# Installs semantic-release and its plugins on the runner
|
|
39
|
+
run: |
|
|
40
|
+
npm install -g semantic-release \
|
|
41
|
+
@semantic-release/changelog \
|
|
42
|
+
@semantic-release/npm \
|
|
43
|
+
@semantic-release/github \
|
|
36
44
|
@semantic-release/git
|
|
37
|
-
|
|
45
|
+
|
|
46
|
+
- name: Run Semantic Release (Native)
|
|
47
|
+
id: semantic
|
|
48
|
+
# This will automatically pick up your export default config file in the repository root
|
|
49
|
+
run: npx semantic-release
|
|
38
50
|
env:
|
|
39
|
-
GITHUB_TOKEN: "${{ secrets.
|
|
51
|
+
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
|
40
52
|
NPM_TOKEN: "${{ secrets.NPM_TOKEN }}"
|
|
53
|
+
|
|
41
54
|
outputs:
|
|
42
55
|
new_release_published: "${{ steps.semantic.outputs.new_release_published }}"
|
|
43
|
-
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
|
44
|
-
|
|
56
|
+
new_release_version: "${{ steps.semantic.outputs.new_release_version }}"
|
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,28 @@
|
|
|
1
|
+
# [1.23.0](https://github.com/CoCreate-app/CoCreate-api/compare/v1.22.5...v1.23.0) (2026-07-17)
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
### Bug Fixes
|
|
5
|
+
|
|
6
|
+
* bump package versions ([c00d388](https://github.com/CoCreate-app/CoCreate-api/commit/c00d388116a8580d7fba19d32e4f963c9399961a))
|
|
7
|
+
* removed post install and add add packages to apprved list ([ae349df](https://github.com/CoCreate-app/CoCreate-api/commit/ae349df51e715e4de1ec26df0a3d0f0cf4e947bb))
|
|
8
|
+
* semantic version handling ([09356af](https://github.com/CoCreate-app/CoCreate-api/commit/09356af9dc8096ba59687cc8edd415da960ef176))
|
|
9
|
+
* update module export syntax in release configuration ([0f7d0c1](https://github.com/CoCreate-app/CoCreate-api/commit/0f7d0c180a143ecb2d035febc8cfdb07f27a46f6))
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
### Features
|
|
13
|
+
|
|
14
|
+
* add CoCreate API client and server modules ([9eb47fd](https://github.com/CoCreate-app/CoCreate-api/commit/9eb47fd5506b69678a52afed78c3c7f700947a55))
|
|
15
|
+
* add export mappings for server and client modules in package.json ([146c96b](https://github.com/CoCreate-app/CoCreate-api/commit/146c96b07b51efcceb87ba11a8ca77465caf32f6))
|
|
16
|
+
* enhance API execution flow and improve configuration handling ([f81a7ac](https://github.com/CoCreate-app/CoCreate-api/commit/f81a7acb459d7f7a24ce4de9b9613016af297bdd))
|
|
17
|
+
* update automated workflow and release configuration for improved semantic release handling ([d7caf5d](https://github.com/CoCreate-app/CoCreate-api/commit/d7caf5db97dcc53ffecee43beb5842dcb903466b))
|
|
18
|
+
|
|
19
|
+
## [1.22.5](https://github.com/CoCreate-app/CoCreate-api/compare/v1.22.4...v1.22.5) (2025-12-26)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
### Bug Fixes
|
|
23
|
+
|
|
24
|
+
* update worklow ([45cb3bc](https://github.com/CoCreate-app/CoCreate-api/commit/45cb3bc8b3aa8328a7dbce98a2b908eb2ba14a2e))
|
|
25
|
+
|
|
1
26
|
## [1.22.4](https://github.com/CoCreate-app/CoCreate-api/compare/v1.22.3...v1.22.4) (2025-05-01)
|
|
2
27
|
|
|
3
28
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/api",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.23.0",
|
|
4
4
|
"description": "A simple api helper component in vanilla javascript used by JavaScript developers to create thirdparty api intergrations. CoCreate-api includes the client component and server side for api processing. Thirdparty apis can be accessible using HTML5 attributes and/or JavaScript API. ",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"thirdparty-api-intergration",
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
"cocreate-framework",
|
|
14
14
|
"no-code",
|
|
15
15
|
"low-code",
|
|
16
|
-
"collaborative-framework",
|
|
17
16
|
"realtime",
|
|
18
17
|
"realtime-framework",
|
|
19
18
|
"collaboration",
|
|
@@ -27,8 +26,7 @@
|
|
|
27
26
|
"scripts": {
|
|
28
27
|
"start": "npx webpack --config webpack.config.js",
|
|
29
28
|
"build": "npx webpack --mode=production --config webpack.config.js",
|
|
30
|
-
"dev": "npx webpack --config webpack.config.js --watch"
|
|
31
|
-
"postinstall": "node -e \"const { execSync } = require('child_process'); try { execSync('coc --version', { stdio: 'ignore' }); } catch (error) { try { execSync('npm install -g @cocreate/cli', { stdio: 'inherit' }); console.log('Installed \"@cocreate/cli\" globally.'); } catch (error) { console.error('Failed to install \"@cocreate/cli\" globally:', error); } }\""
|
|
29
|
+
"dev": "npx webpack --config webpack.config.js --watch"
|
|
32
30
|
},
|
|
33
31
|
"repository": {
|
|
34
32
|
"type": "git",
|
|
@@ -44,22 +42,44 @@
|
|
|
44
42
|
"type": "GitHub Sponsors ❤",
|
|
45
43
|
"url": "https://github.com/sponsors/CoCreate-app"
|
|
46
44
|
},
|
|
47
|
-
"
|
|
48
|
-
"
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
"
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
45
|
+
"type": "module",
|
|
46
|
+
"main": "./src/server.js",
|
|
47
|
+
"browser": "./src/client.js",
|
|
48
|
+
"exports": {
|
|
49
|
+
".": {
|
|
50
|
+
"node": "./src/server.js",
|
|
51
|
+
"deno": "./src/server.js",
|
|
52
|
+
"browser": "./src/client.js",
|
|
53
|
+
"default": "./src/server.js"
|
|
54
|
+
}
|
|
56
55
|
},
|
|
57
56
|
"dependencies": {
|
|
58
|
-
"@cocreate/actions": "^1.21.
|
|
59
|
-
"@cocreate/crud-client": "^1.34.
|
|
60
|
-
"@cocreate/element-prototype": "^1.
|
|
61
|
-
"@cocreate/render": "^1.
|
|
62
|
-
"@cocreate/socket-client": "^1.40.
|
|
63
|
-
"@cocreate/utils": "^1.
|
|
57
|
+
"@cocreate/actions": "^1.21.4",
|
|
58
|
+
"@cocreate/crud-client": "^1.34.6",
|
|
59
|
+
"@cocreate/element-prototype": "^1.31.4",
|
|
60
|
+
"@cocreate/render": "^1.46.1",
|
|
61
|
+
"@cocreate/socket-client": "^1.40.5",
|
|
62
|
+
"@cocreate/utils": "^1.42.2"
|
|
63
|
+
},
|
|
64
|
+
"devDependencies": {
|
|
65
|
+
"@cocreate/webpack": "^1.4.3"
|
|
66
|
+
},
|
|
67
|
+
"allowScripts": {
|
|
68
|
+
"@cocreate/actions@1.21.4": true,
|
|
69
|
+
"@cocreate/api@1.22.5": true,
|
|
70
|
+
"@cocreate/config@1.13.4": true,
|
|
71
|
+
"@cocreate/crud-client@1.34.6": true,
|
|
72
|
+
"@cocreate/element-prototype@1.31.4": true,
|
|
73
|
+
"@cocreate/elements@1.42.9": true,
|
|
74
|
+
"@cocreate/filter@1.33.6": true,
|
|
75
|
+
"@cocreate/indexeddb@1.23.4": true,
|
|
76
|
+
"@cocreate/local-storage@1.16.5": true,
|
|
77
|
+
"@cocreate/observer@1.19.0": true,
|
|
78
|
+
"@cocreate/organizations@1.30.1": true,
|
|
79
|
+
"@cocreate/render@1.46.1": true,
|
|
80
|
+
"@cocreate/socket-client@1.40.5": true,
|
|
81
|
+
"@cocreate/utils@1.42.2": true,
|
|
82
|
+
"@cocreate/uuid@1.12.4": true,
|
|
83
|
+
"@cocreate/webpack@1.4.3": true
|
|
64
84
|
}
|
|
65
85
|
}
|
package/release.config.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
|
|
1
|
+
export default {
|
|
2
2
|
dryRun: false,
|
|
3
3
|
branches: ["master"],
|
|
4
4
|
plugins: [
|
|
@@ -10,12 +10,20 @@ module.exports = {
|
|
|
10
10
|
changelogFile: "CHANGELOG.md",
|
|
11
11
|
},
|
|
12
12
|
],
|
|
13
|
-
"@semantic-release/npm",
|
|
14
|
-
"@semantic-release/github",
|
|
13
|
+
"@semantic-release/npm",
|
|
15
14
|
[
|
|
16
|
-
"@semantic-release/
|
|
15
|
+
"@semantic-release/github",
|
|
17
16
|
{
|
|
17
|
+
successComment: false,
|
|
18
|
+
failTitle: false,
|
|
19
|
+
},
|
|
20
|
+
],
|
|
21
|
+
[
|
|
22
|
+
"@semantic-release/git",
|
|
23
|
+
{
|
|
24
|
+
// Only stage and commit the changelog and package.json
|
|
18
25
|
assets: ["CHANGELOG.md", "package.json"],
|
|
26
|
+
message: "chore(release): ${nextRelease.version} [skip ci]",
|
|
19
27
|
},
|
|
20
28
|
],
|
|
21
29
|
],
|
package/src/server.js
ADDED
|
@@ -0,0 +1,651 @@
|
|
|
1
|
+
/********************************************************************************
|
|
2
|
+
* Copyright (C) 2023 CoCreate and Contributors.
|
|
3
|
+
*
|
|
4
|
+
* This program is free software: you can redistribute it and/or modify
|
|
5
|
+
* it under the terms of the GNU Affero General Public License as published
|
|
6
|
+
* by the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
* (at your option) any later version.
|
|
8
|
+
*
|
|
9
|
+
* This program is distributed in the hope that it will be useful,
|
|
10
|
+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
* GNU Affero General Public License for more details.
|
|
13
|
+
*
|
|
14
|
+
* You should have received a copy of the GNU Affero General Public License
|
|
15
|
+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
*
|
|
17
|
+
********************************************************************************/
|
|
18
|
+
|
|
19
|
+
// Commercial Licensing Information:
|
|
20
|
+
// For commercial use of this software without the copyleft provisions of the AGPLv3,
|
|
21
|
+
// you must obtain a commercial license from CoCreate LLC.
|
|
22
|
+
// For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
|
|
23
|
+
|
|
24
|
+
import crypto from 'node:crypto';
|
|
25
|
+
import { URL, URLSearchParams } from 'node:url';
|
|
26
|
+
import Config from '@cocreate/config';
|
|
27
|
+
import { getValueFromObject, objectToSearchParams } from '@cocreate/utils';
|
|
28
|
+
|
|
29
|
+
// Core module references bound dynamically upon initialization
|
|
30
|
+
let server = null;
|
|
31
|
+
let wsManager = null;
|
|
32
|
+
let crud = null;
|
|
33
|
+
|
|
34
|
+
const NativeSandbox = {
|
|
35
|
+
Math,
|
|
36
|
+
Number,
|
|
37
|
+
String,
|
|
38
|
+
Boolean,
|
|
39
|
+
Date,
|
|
40
|
+
JSON,
|
|
41
|
+
Array,
|
|
42
|
+
Object,
|
|
43
|
+
parseInt,
|
|
44
|
+
parseFloat,
|
|
45
|
+
encodeURIComponent,
|
|
46
|
+
decodeURIComponent,
|
|
47
|
+
btoa: (str) => Buffer.from(String(str)).toString("base64"),
|
|
48
|
+
atob: (b64) => Buffer.from(String(b64), "base64").toString("utf-8"),
|
|
49
|
+
concat: (...args) => args.flat().map(arg => typeof arg === 'object' ? JSON.stringify(arg) : arg).join(''),
|
|
50
|
+
formUrlencode: (obj) => {
|
|
51
|
+
if (!obj || typeof obj !== 'object') return String(obj || '');
|
|
52
|
+
const params = new URLSearchParams();
|
|
53
|
+
for (const [key, val] of Object.entries(obj)) {
|
|
54
|
+
params.append(key, typeof val === 'object' ? JSON.stringify(val) : String(val));
|
|
55
|
+
}
|
|
56
|
+
return params.toString();
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
const API = {
|
|
61
|
+
init,
|
|
62
|
+
executeApi,
|
|
63
|
+
request,
|
|
64
|
+
generateJWT,
|
|
65
|
+
verifySignature,
|
|
66
|
+
makeHttpRequest,
|
|
67
|
+
processOperators,
|
|
68
|
+
processOperator,
|
|
69
|
+
getApiConfig,
|
|
70
|
+
normalizeConfig
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
export async function init(Server) {
|
|
74
|
+
server = Server;
|
|
75
|
+
wsManager = server.wsManager;
|
|
76
|
+
crud = server.crud;
|
|
77
|
+
|
|
78
|
+
wsManager.on("api", (data) => {
|
|
79
|
+
executeApi(data);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
wsManager.on("endpoint", (data) => {
|
|
83
|
+
executeApi(data);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
return API;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export function normalizeConfig(apiConfig, name) {
|
|
90
|
+
let key = null;
|
|
91
|
+
let url = null;
|
|
92
|
+
let headers = {};
|
|
93
|
+
|
|
94
|
+
if (typeof apiConfig === 'string') {
|
|
95
|
+
key = apiConfig;
|
|
96
|
+
} else if (apiConfig && typeof apiConfig === 'object') {
|
|
97
|
+
url = apiConfig.url;
|
|
98
|
+
headers = apiConfig.headers || {};
|
|
99
|
+
|
|
100
|
+
if (apiConfig.key) {
|
|
101
|
+
if (typeof apiConfig.key === 'string') {
|
|
102
|
+
key = apiConfig.key;
|
|
103
|
+
} else if (typeof apiConfig.key === 'object') {
|
|
104
|
+
key = apiConfig.key.apiKey || apiConfig.key.key || JSON.stringify(apiConfig.key);
|
|
105
|
+
}
|
|
106
|
+
} else if (apiConfig.apiKey) {
|
|
107
|
+
key = apiConfig.apiKey;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (apiConfig.url) url = apiConfig.url;
|
|
111
|
+
if (apiConfig.headers) headers = { ...headers, ...apiConfig.headers };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return { key, url, headers };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function executeApi(data, parentContext = null) {
|
|
118
|
+
try {
|
|
119
|
+
// SECURITY FIREWALL: Parent contexts and authenticated socket boundaries take absolute priority
|
|
120
|
+
// over raw payload elements to prevent config-driven cross-tenant data leaks or privilege escalations.
|
|
121
|
+
const orgId = parentContext?.organization_id || data.socket?.organization_id || data.organization_id;
|
|
122
|
+
const host = parentContext?.host || data.socket?.host || data.host;
|
|
123
|
+
|
|
124
|
+
// PATH A: Native Execution of a Pre-prepared HTTP Request Object
|
|
125
|
+
if (data.url && data.method) {
|
|
126
|
+
const timeout = data.timeout || 15000;
|
|
127
|
+
const options = {
|
|
128
|
+
method: data.method.toUpperCase(),
|
|
129
|
+
headers: data.headers || {},
|
|
130
|
+
timeout
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
if (data.body) {
|
|
134
|
+
options.body = typeof data.body === 'object' ? JSON.stringify(data.body) : data.body;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const response = await makeHttpRequest(data.url, options);
|
|
138
|
+
const responsePayload = await response.json();
|
|
139
|
+
|
|
140
|
+
if (data.socket && wsManager) {
|
|
141
|
+
data.response = responsePayload;
|
|
142
|
+
wsManager.send(data);
|
|
143
|
+
}
|
|
144
|
+
return responsePayload;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// PATH B: Standard Dynamic Configuration Resolve & Execute
|
|
148
|
+
if (!data.method || !data.endpoint) {
|
|
149
|
+
throw new Error("API execution request is missing 'method' or 'endpoint' directives.");
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
const providerName = data.method.split(/[./]/)[0];
|
|
153
|
+
const httpMethod = data.endpoint.split(" ")[0].toUpperCase();
|
|
154
|
+
|
|
155
|
+
const endpointQuery = {
|
|
156
|
+
method: 'object.read',
|
|
157
|
+
array: 'endpoints',
|
|
158
|
+
organization_id: orgId,
|
|
159
|
+
host: host, // Carry host forward
|
|
160
|
+
$filter: {
|
|
161
|
+
query: { endpoint: data.endpoint, organization_id: orgId },
|
|
162
|
+
limit: 1
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
const endpointResponse = await crud.send(endpointQuery);
|
|
166
|
+
const endpointConfig = endpointResponse?.object?.[0] || {};
|
|
167
|
+
|
|
168
|
+
// Fetch provider credentials with full environmental scope intact
|
|
169
|
+
const rawApiConfig = await getApiConfig({ organization_id: orgId, host }, providerName);
|
|
170
|
+
if (!rawApiConfig) {
|
|
171
|
+
throw new Error(`Connection setting not registered for provider: '${providerName}'.`);
|
|
172
|
+
}
|
|
173
|
+
const apiConfig = normalizeConfig(rawApiConfig, providerName);
|
|
174
|
+
|
|
175
|
+
// Share parent context if executing within a nested macro frame
|
|
176
|
+
const apiContext = parentContext || {
|
|
177
|
+
data: data,
|
|
178
|
+
event: {},
|
|
179
|
+
actions: [],
|
|
180
|
+
organization_id: orgId,
|
|
181
|
+
host: host, // Lock host context into AST evaluation loop
|
|
182
|
+
crud,
|
|
183
|
+
wsManager,
|
|
184
|
+
key: apiConfig.key,
|
|
185
|
+
config: apiConfig
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
const actions = endpointConfig.actions || endpointConfig.action;
|
|
189
|
+
if (actions) {
|
|
190
|
+
if (Array.isArray(actions)) {
|
|
191
|
+
for (let i = 0; i < actions.length; i++) {
|
|
192
|
+
const result = await processOperators(apiContext, actions[i]);
|
|
193
|
+
apiContext.actions.push(result); // Saved flattened memory profile
|
|
194
|
+
}
|
|
195
|
+
const finalMacroResult = apiContext.actions[apiContext.actions.length - 1];
|
|
196
|
+
if (finalMacroResult !== undefined) {
|
|
197
|
+
data[providerName] = finalMacroResult;
|
|
198
|
+
}
|
|
199
|
+
} else {
|
|
200
|
+
const result = await processOperators(apiContext, actions);
|
|
201
|
+
apiContext.actions.push(result);
|
|
202
|
+
if (result !== undefined) {
|
|
203
|
+
data[providerName] = result;
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
if (data.socket && wsManager) {
|
|
208
|
+
wsManager.send(data);
|
|
209
|
+
}
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// --- Standard Outbound Construction Path ---
|
|
214
|
+
if (!apiConfig.url) {
|
|
215
|
+
throw new Error(`Configuration missing 'url' base for provider '${providerName}'.`);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
let url = apiConfig.url.endsWith("/") ? apiConfig.url.slice(0, -1) : apiConfig.url;
|
|
219
|
+
let endpointPath = endpointConfig.path || data.endpoint.split(" ")[1];
|
|
220
|
+
url += endpointPath.startsWith("/") ? endpointPath : `/${endpointPath}`;
|
|
221
|
+
|
|
222
|
+
if (data[providerName] && data[providerName].$searchParams) {
|
|
223
|
+
url += objectToSearchParams(data[providerName].$searchParams);
|
|
224
|
+
delete data[providerName].$searchParams;
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
let headers = { ...(apiConfig.headers || {}), ...(endpointConfig.headers || {}) };
|
|
228
|
+
headers = await processOperators(apiContext, headers);
|
|
229
|
+
|
|
230
|
+
const timeout = 15000;
|
|
231
|
+
let options = { method: httpMethod, headers, timeout };
|
|
232
|
+
|
|
233
|
+
if (!["GET", "HEAD"].includes(httpMethod) && data[providerName]) {
|
|
234
|
+
const bodyPayload = await processOperators(apiContext, data[providerName]);
|
|
235
|
+
|
|
236
|
+
if (typeof bodyPayload === 'object' && bodyPayload !== null) {
|
|
237
|
+
options.body = JSON.stringify(bodyPayload);
|
|
238
|
+
if (!headers["Content-Type"]) {
|
|
239
|
+
options.headers["Content-Type"] = "application/json; charset=utf-8";
|
|
240
|
+
}
|
|
241
|
+
} else {
|
|
242
|
+
options.body = String(bodyPayload);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const response = await makeHttpRequest(url, options);
|
|
247
|
+
const responsePayload = await response.json();
|
|
248
|
+
data[providerName] = responsePayload;
|
|
249
|
+
|
|
250
|
+
if (data.socket && wsManager) {
|
|
251
|
+
wsManager.send(data);
|
|
252
|
+
}
|
|
253
|
+
return responsePayload;
|
|
254
|
+
} catch (error) {
|
|
255
|
+
data.error = error.message;
|
|
256
|
+
if (error.response) {
|
|
257
|
+
data.status = error.response.status;
|
|
258
|
+
data.statusText = error.response.statusText;
|
|
259
|
+
data.responseData = error.response.data;
|
|
260
|
+
}
|
|
261
|
+
if (data.socket && wsManager) {
|
|
262
|
+
wsManager.send(data);
|
|
263
|
+
}
|
|
264
|
+
throw error;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
export async function request(req, res) {
|
|
269
|
+
try {
|
|
270
|
+
const urlObject = new URL(`http://${req.headers.host}${req.url}`);
|
|
271
|
+
const hostname = urlObject.hostname;
|
|
272
|
+
|
|
273
|
+
let organization;
|
|
274
|
+
try {
|
|
275
|
+
organization = await crud.getOrganization({ host: hostname });
|
|
276
|
+
} catch (error) {
|
|
277
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
278
|
+
return res.end(JSON.stringify({ error: "Organization mapping not found" }));
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const webhookPath = urlObject.pathname.replace(/^\/webhooks\//, "");
|
|
282
|
+
|
|
283
|
+
const webhookQuery = {
|
|
284
|
+
method: 'object.read',
|
|
285
|
+
array: 'webhooks',
|
|
286
|
+
organization_id: organization._id,
|
|
287
|
+
host: hostname, // Maintain active host environment
|
|
288
|
+
$filter: {
|
|
289
|
+
query: {
|
|
290
|
+
path: webhookPath,
|
|
291
|
+
organization_id: organization._id
|
|
292
|
+
},
|
|
293
|
+
limit: 1
|
|
294
|
+
}
|
|
295
|
+
};
|
|
296
|
+
const webhookResponse = await crud.send(webhookQuery);
|
|
297
|
+
const webhookConfig = webhookResponse?.object?.[0];
|
|
298
|
+
|
|
299
|
+
if (!webhookConfig) {
|
|
300
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
301
|
+
return res.end(JSON.stringify({ error: `Webhook configuration for path '${webhookPath}' not found.` }));
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
let rawBody = "";
|
|
305
|
+
await new Promise((resolve, reject) => {
|
|
306
|
+
req.on("data", (chunk) => { rawBody += chunk.toString(); });
|
|
307
|
+
req.on("end", resolve);
|
|
308
|
+
req.on("error", reject);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
const queryParams = Object.fromEntries(urlObject.searchParams.entries());
|
|
312
|
+
|
|
313
|
+
let parsedEvent = {};
|
|
314
|
+
if (rawBody) {
|
|
315
|
+
try {
|
|
316
|
+
parsedEvent = JSON.parse(rawBody);
|
|
317
|
+
} catch {
|
|
318
|
+
parsedEvent = { rawBody };
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
const context = {
|
|
323
|
+
data: {},
|
|
324
|
+
event: parsedEvent,
|
|
325
|
+
actions: [],
|
|
326
|
+
request: req,
|
|
327
|
+
response: res,
|
|
328
|
+
rawBody: rawBody,
|
|
329
|
+
host: hostname, // Propagate verified edge hostname
|
|
330
|
+
organization: organization,
|
|
331
|
+
organization_id: organization._id,
|
|
332
|
+
crud: crud,
|
|
333
|
+
wsManager: wsManager
|
|
334
|
+
};
|
|
335
|
+
|
|
336
|
+
context.request.query = queryParams;
|
|
337
|
+
|
|
338
|
+
if (webhookConfig.authenticate) {
|
|
339
|
+
const auth = webhookConfig.authenticate;
|
|
340
|
+
if (auth.method === "webhooks.constructEvent" && Array.isArray(auth.parameters)) {
|
|
341
|
+
const resolvedParams = await processOperators(context, [...auth.parameters]);
|
|
342
|
+
const payload = resolvedParams[0];
|
|
343
|
+
const signature = resolvedParams[1];
|
|
344
|
+
const secret = resolvedParams[2];
|
|
345
|
+
|
|
346
|
+
if (!verifySignature(payload, signature, secret, "sha256")) {
|
|
347
|
+
res.writeHead(401, { "Content-Type": "application/json" });
|
|
348
|
+
return res.end(JSON.stringify({ error: "Unauthorized access: signature verification failed." }));
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
let executeOperations = null;
|
|
354
|
+
const actionConfig = webhookConfig.action;
|
|
355
|
+
|
|
356
|
+
if (actionConfig) {
|
|
357
|
+
if (typeof actionConfig === 'string') {
|
|
358
|
+
const actionName = await processOperators(context, actionConfig);
|
|
359
|
+
if (actionName && webhookConfig.actions?.[actionName]) {
|
|
360
|
+
executeOperations = webhookConfig.actions[actionName];
|
|
361
|
+
}
|
|
362
|
+
} else {
|
|
363
|
+
executeOperations = actionConfig;
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
if (executeOperations) {
|
|
368
|
+
if (Array.isArray(executeOperations)) {
|
|
369
|
+
for (let i = 0; i < executeOperations.length; i++) {
|
|
370
|
+
const stepResult = await processOperators(context, executeOperations[i]);
|
|
371
|
+
context.actions.push(stepResult);
|
|
372
|
+
}
|
|
373
|
+
} else if (typeof executeOperations === 'object' && executeOperations !== null) {
|
|
374
|
+
const stepResult = await processOperators(context, [executeOperations]);
|
|
375
|
+
context.actions.push(stepResult);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Native Stream Inspection: Fall back to default 200 OK only if actions did not write or end the stream
|
|
380
|
+
if (res && !res.writableEnded) {
|
|
381
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
382
|
+
res.end(JSON.stringify({
|
|
383
|
+
message: "Webhook processed successfully",
|
|
384
|
+
payload: context.actions[context.actions.length - 1] || {}
|
|
385
|
+
}));
|
|
386
|
+
}
|
|
387
|
+
} catch (error) {
|
|
388
|
+
if (res && !res.writableEnded) {
|
|
389
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
390
|
+
res.end(JSON.stringify({ error: "Invalid webhook request format", message: error.message }));
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export async function processOperators(context, execute) {
|
|
396
|
+
if (Array.isArray(execute)) {
|
|
397
|
+
let evaluatedArray = [];
|
|
398
|
+
for (let index = 0; index < execute.length; index++) {
|
|
399
|
+
evaluatedArray[index] = await processOperators(context, execute[index]);
|
|
400
|
+
}
|
|
401
|
+
return evaluatedArray;
|
|
402
|
+
} else if (typeof execute === "object" && execute !== null) {
|
|
403
|
+
const keys = Object.keys(execute);
|
|
404
|
+
const operatorKey = keys.find(k => k.startsWith("$") && !["$storage", "$database", "$array", "$filter"].includes(k));
|
|
405
|
+
|
|
406
|
+
if (operatorKey && keys.length === 1) {
|
|
407
|
+
return await processOperator(context, operatorKey, execute[operatorKey]);
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
let evaluatedObject = {};
|
|
411
|
+
for (let key of keys) {
|
|
412
|
+
let finalKey = key;
|
|
413
|
+
|
|
414
|
+
if (key.startsWith("$") && !["$storage", "$database", "$array", "$filter"].includes(key)) {
|
|
415
|
+
let resolvedKey = await processOperator(context, key);
|
|
416
|
+
if (typeof resolvedKey === "string" || typeof resolvedKey === "number") {
|
|
417
|
+
finalKey = String(resolvedKey);
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
let finalValue = execute[key];
|
|
422
|
+
if (typeof finalValue === "string" && finalValue.startsWith("$") && !["$storage", "$database", "$array", "$filter"].includes(finalValue)) {
|
|
423
|
+
finalValue = await processOperator(context, finalValue);
|
|
424
|
+
} else if (Array.isArray(finalValue) || (typeof finalValue === "object" && finalValue !== null)) {
|
|
425
|
+
finalValue = await processOperators(context, finalValue);
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
evaluatedObject[finalKey] = finalValue;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
return evaluatedObject;
|
|
432
|
+
} else if (typeof execute === "string" && execute.startsWith("$") && !["$storage", "$database", "$array", "$filter"].includes(execute)) {
|
|
433
|
+
return await processOperator(context, execute);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
return execute;
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
export async function processOperator(context, operator, args) {
|
|
440
|
+
if (args === undefined) {
|
|
441
|
+
if (operator.startsWith("$data.")) {
|
|
442
|
+
return getValueFromObject(context.data, operator.substring(6));
|
|
443
|
+
} else if (operator === "$data") {
|
|
444
|
+
return context.data;
|
|
445
|
+
} else if (operator.startsWith("$event.")) {
|
|
446
|
+
const result = getValueFromObject(context.event, operator.substring(7));
|
|
447
|
+
return await processOperators(context, result);
|
|
448
|
+
} else if (operator === "$event") {
|
|
449
|
+
return await processOperators(context, context.event);
|
|
450
|
+
} else if (operator.startsWith("$actions.")) {
|
|
451
|
+
return getValueFromObject(context.actions, operator.substring(9));
|
|
452
|
+
} else if (operator === "$actions") {
|
|
453
|
+
return context.actions;
|
|
454
|
+
} else if (operator.startsWith("$request.")) {
|
|
455
|
+
return getValueFromObject(context.request, operator.substring(9));
|
|
456
|
+
} else if (operator === "$request") {
|
|
457
|
+
return context.request;
|
|
458
|
+
} else if (operator.startsWith("$header.")) {
|
|
459
|
+
const headerKey = operator.substring(8).toLowerCase();
|
|
460
|
+
return context.request?.headers?.[headerKey];
|
|
461
|
+
} else if (operator === "$rawBody") {
|
|
462
|
+
return context.rawBody;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
if (operator === "$jwt") {
|
|
467
|
+
const evaluatedContext = args !== undefined ? await processOperators(context, args) : {};
|
|
468
|
+
const privateKey = evaluatedContext.privateKey;
|
|
469
|
+
if (!privateKey) throw new Error("Missing cryptographic signing key inside $jwt context parameters");
|
|
470
|
+
return generateJWT(evaluatedContext, privateKey);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
const cleanPath = operator.startsWith("$") ? operator.substring(1) : operator;
|
|
474
|
+
const parts = cleanPath.split('.');
|
|
475
|
+
|
|
476
|
+
const Sandbox = {
|
|
477
|
+
...NativeSandbox,
|
|
478
|
+
crud: context.crud,
|
|
479
|
+
socket: context.wsManager,
|
|
480
|
+
wsManager: context.wsManager,
|
|
481
|
+
request: context.request,
|
|
482
|
+
response: context.response,
|
|
483
|
+
key: context.key,
|
|
484
|
+
config: context.config,
|
|
485
|
+
organization_id: context.organization_id,
|
|
486
|
+
host: context.host, // Seal active host into AST sandbox environment
|
|
487
|
+
api: {
|
|
488
|
+
send: async (apiData) => {
|
|
489
|
+
return await executeApi(apiData, context);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
};
|
|
493
|
+
|
|
494
|
+
let target = Sandbox[parts[0]];
|
|
495
|
+
let parentContext = Sandbox;
|
|
496
|
+
|
|
497
|
+
if (target !== undefined) {
|
|
498
|
+
for (let i = 1; i < parts.length; i++) {
|
|
499
|
+
parentContext = target;
|
|
500
|
+
target = target[parts[i]];
|
|
501
|
+
if (target === undefined) break;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
if (typeof target === 'function') {
|
|
505
|
+
let argsArray = [];
|
|
506
|
+
if (args !== undefined) {
|
|
507
|
+
// Check if the argument is explicitly formatted using our human-friendly parameter operators
|
|
508
|
+
if (typeof args === 'object' && args !== null && (args.$args !== undefined || args.$arguments !== undefined)) {
|
|
509
|
+
const explicitArgs = args.$args !== undefined ? args.$args : args.$arguments;
|
|
510
|
+
|
|
511
|
+
if (Array.isArray(explicitArgs)) {
|
|
512
|
+
// Multi-parameter routing: Evaluate each index independently
|
|
513
|
+
argsArray = [];
|
|
514
|
+
for (let i = 0; i < explicitArgs.length; i++) {
|
|
515
|
+
argsArray.push(await processOperators(context, explicitArgs[i]));
|
|
516
|
+
}
|
|
517
|
+
} else {
|
|
518
|
+
// Single-parameter fallback: Spelled out explicitly but contains a single expression
|
|
519
|
+
const evaluatedSingle = await processOperators(context, explicitArgs);
|
|
520
|
+
argsArray = [evaluatedSingle];
|
|
521
|
+
}
|
|
522
|
+
} else {
|
|
523
|
+
// Standard / Implicit routing: Treat the entire value as a single, isolated argument (arrays behave like arrays!)
|
|
524
|
+
const evaluatedSingle = await processOperators(context, args);
|
|
525
|
+
argsArray = [evaluatedSingle];
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// SECURITY FIREWALL: Forcefully overwrite parameters for local system calls
|
|
530
|
+
// to align with the sealed context of the authenticated user.
|
|
531
|
+
if (parentContext === context.crud || parentContext === context.wsManager) {
|
|
532
|
+
if (argsArray.length === 0) {
|
|
533
|
+
argsArray.push({
|
|
534
|
+
organization_id: context.organization_id,
|
|
535
|
+
host: context.host
|
|
536
|
+
});
|
|
537
|
+
} else if (argsArray.length > 0 && typeof argsArray[0] === 'object' && argsArray[0] !== null) {
|
|
538
|
+
argsArray[0].organization_id = context.organization_id;
|
|
539
|
+
argsArray[0].host = context.host; // Lock environment isolation parameters down
|
|
540
|
+
}
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
return await target.apply(parentContext, argsArray);
|
|
544
|
+
} else if (target !== undefined) {
|
|
545
|
+
return target;
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
return operator;
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function generateJWT(config, secret) {
|
|
553
|
+
try {
|
|
554
|
+
const header = config.header || { alg: 'HS256', typ: 'JWT' };
|
|
555
|
+
const payload = config.payload || {};
|
|
556
|
+
|
|
557
|
+
const encodedHeader = Buffer.from(JSON.stringify(header)).toString('base64url');
|
|
558
|
+
const encodedPayload = Buffer.from(JSON.stringify(payload)).toString('base64url');
|
|
559
|
+
|
|
560
|
+
const signatureInput = `${encodedHeader}.${encodedPayload}`;
|
|
561
|
+
|
|
562
|
+
const hmac = crypto.createHmac('sha256', secret);
|
|
563
|
+
hmac.update(signatureInput);
|
|
564
|
+
const signature = hmac.digest('base64url');
|
|
565
|
+
|
|
566
|
+
return `${signatureInput}.${signature}`;
|
|
567
|
+
} catch (err) {
|
|
568
|
+
console.error('[@cocreate/api] Failed to generate JWT:', err);
|
|
569
|
+
throw err;
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
export function verifySignature(payload, signature, secret, algorithm = 'sha256') {
|
|
574
|
+
try {
|
|
575
|
+
const hmac = crypto.createHmac(algorithm, secret);
|
|
576
|
+
hmac.update(payload);
|
|
577
|
+
const expectedSignature = hmac.digest('hex');
|
|
578
|
+
|
|
579
|
+
return crypto.timingSafeEqual(
|
|
580
|
+
Buffer.from(expectedSignature),
|
|
581
|
+
Buffer.from(signature)
|
|
582
|
+
);
|
|
583
|
+
} catch (err) {
|
|
584
|
+
console.error('[@cocreate/api] Failed signature verification:', err);
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
export async function makeHttpRequest(url, options) {
|
|
590
|
+
let controller, timeoutId;
|
|
591
|
+
if (global.AbortController) {
|
|
592
|
+
controller = new global.AbortController();
|
|
593
|
+
timeoutId = setTimeout(() => controller.abort(), options.timeout || 15000);
|
|
594
|
+
options.signal = controller.signal;
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
const fetchFn = global.fetch;
|
|
598
|
+
if (typeof fetchFn !== "function") {
|
|
599
|
+
throw new Error("System runtime environment is missing a valid HTTP fetch implementation.");
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
try {
|
|
603
|
+
const response = await fetchFn(url, options);
|
|
604
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
605
|
+
|
|
606
|
+
if (!response.ok) {
|
|
607
|
+
const text = await response.text();
|
|
608
|
+
const error = new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
|
|
609
|
+
error.response = {
|
|
610
|
+
status: response.status,
|
|
611
|
+
statusText: response.statusText,
|
|
612
|
+
data: text
|
|
613
|
+
};
|
|
614
|
+
throw error;
|
|
615
|
+
}
|
|
616
|
+
return response;
|
|
617
|
+
} catch (error) {
|
|
618
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
619
|
+
throw error;
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
|
|
623
|
+
export async function getApiConfig(data, name) {
|
|
624
|
+
let orgId = data.organization_id;
|
|
625
|
+
let host = data.host;
|
|
626
|
+
|
|
627
|
+
if (!orgId) {
|
|
628
|
+
const organization = await crud.getOrganization(data); // uses host inside data to lookup org
|
|
629
|
+
if (organization.error) throw new Error(organization.error);
|
|
630
|
+
orgId = organization._id;
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
const query = {
|
|
634
|
+
method: 'object.read',
|
|
635
|
+
array: 'apis',
|
|
636
|
+
organization_id: orgId,
|
|
637
|
+
host: host, // Keep environment host isolation contact
|
|
638
|
+
$filter: {
|
|
639
|
+
query: { name: name, organization_id: orgId },
|
|
640
|
+
limit: 1
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
|
|
644
|
+
const response = await crud.send(query);
|
|
645
|
+
const apiConfig = response?.object?.[0];
|
|
646
|
+
|
|
647
|
+
if (!apiConfig) throw new Error(`Integration settings are missing for provider: '${name}'`);
|
|
648
|
+
return apiConfig;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
export default API;
|
|
File without changes
|