@cocreate/api 1.23.2 → 1.23.3
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 +135 -52
- package/package.json +15 -19
- package/src/server.js +124 -214
package/README.md
CHANGED
|
@@ -1,91 +1,174 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @cocreate/api
|
|
2
2
|
|
|
3
|
-
A
|
|
3
|
+
A dynamic, multi-tenant API execution gateway, proxy engine, and webhook orchestration routing layer. It securely manages distributed integration architectures, allowing applications to configure, evaluate, and execute complex third-party API configurations and multi-step macro workflows on the fly. By coupling an Abstract Syntax Tree (AST) evaluation sandbox (`NativeSandbox`) with strict tenant environmental isolation, it transforms declarative configurations into low-latency outbound integrations and secure inbound webhooks without introducing cross-tenant data leaks.
|
|
4
4
|
|
|
5
|
-
|
|
6
|
-

|
|
7
|
-

|
|
8
|
-

|
|
9
|
-

|
|
10
|
-

|
|
5
|
+
---
|
|
11
6
|
|
|
12
|
-
|
|
7
|
+
## Documentation
|
|
13
8
|
|
|
14
|
-
|
|
9
|
+
For complete API references, integration guides, webhook configuration, and usage examples, visit the **[CoCreate API Documentation](https://cocreatejs.com/docs/api)**.
|
|
15
10
|
|
|
16
|
-
|
|
11
|
+
## Table of Contents
|
|
17
12
|
|
|
18
|
-
|
|
13
|
+
- [Documentation](#documentation)
|
|
14
|
+
- [Features](#features)
|
|
15
|
+
- [Dynamic Operations Sandbox](#dynamic-operations-sandbox)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Usage](#usage)
|
|
18
|
+
- [How it Works](#how-it-works)
|
|
19
|
+
- [Architecture and Payload Specs](#architecture-and-payload-specs)
|
|
20
|
+
- [Security Implementations](#security-implementations)
|
|
21
|
+
- [How to Contribute](#how-to-contribute)
|
|
22
|
+
- [License](#license)
|
|
19
23
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
## Features
|
|
27
|
+
|
|
28
|
+
* **Abstract Syntax Tree (AST) Micro-Operators:** Evaluates functional configuration contexts inline, supporting expressive syntax tags (like `$data`, `$event`, `$actions`, and `$request`) along with custom business logic operators.
|
|
29
|
+
* **Tenant Environmental Isolation Firewall:** Guarantees cryptographic data partitioning by intercepting execution matrices and forcefully injecting bound `organization_id` and verified `host` values into core operations.
|
|
30
|
+
* **Real-Time WebSocket Integration Matrix:** Seamlessly hooks into live server networks (`wsManager`), listening for incoming `api` and `endpoint` actions and streaming real-time operational state payloads directly back to the client.
|
|
31
|
+
* **Dual Outbound Execution Vectors:** Wire-speeds native, pre-constructed HTTP payloads instantly, or dynamically builds custom multi-step outbound integrations by resolving runtime metadata against persistence layers.
|
|
32
|
+
* **Cryptographic Signatures & JWT Orchestration:** Features native, low-latency hooks to generate JSON Web Tokens (`$jwt`) using HMAC-SHA256, alongside timing-attack-safe webhook signature verification (`verifySignature`).
|
|
33
|
+
* **Sealed AST Runtime Sandbox:** Features a hardened utility matrix containing sandboxed global objects (`JSON`, `Array`, `Math`) and helper methods (like Base64 encoding/decoding and URL encoders), safely wrapping calculations outside the global scope.
|
|
34
|
+
* **Native Edge Stream Inspection:** Inspects active server response buffers automatically, failing back to a clean 200 OK only if macro pipelines didn't forcefully complete or close the stream.
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Dynamic Operations Sandbox
|
|
39
|
+
|
|
40
|
+
The engine evaluates standard properties and custom functional variables using a predefined sandbox layer. This allows configuration profiles to extract fields safely on the fly.
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
42
|
+
| Core Operator Scope | Extracted Source Value |
|
|
43
|
+
| --- | --- |
|
|
44
|
+
| **`$data`** | Extracts standard key parameters from the active input execution payload (`data`). |
|
|
45
|
+
| **`$event`** | Parses raw incoming JSON parameters transmitted via active webhook streams. |
|
|
46
|
+
| **`$actions`** | Exposes an array of historical execution artifacts from previous macro pipeline links. |
|
|
47
|
+
| **`$request` / `$header`** | Exposes underlying HTTP request properties, methods, and incoming network parameters. |
|
|
48
|
+
| **`$jwt`** | Triggers an active cryptographic signing loop, building a secure JWT token from properties. |
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## Installation
|
|
53
|
+
|
|
54
|
+
```bash
|
|
55
|
+
npm install @cocreate/api
|
|
26
56
|
```
|
|
27
57
|
|
|
28
|
-
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Usage
|
|
61
|
+
|
|
62
|
+
### Module Initialization
|
|
63
|
+
|
|
64
|
+
Bind the API framework directly during the bootstrapping phase of your core `CoCreateServer` application matrix.
|
|
29
65
|
|
|
30
|
-
```
|
|
31
|
-
|
|
66
|
+
```javascript
|
|
67
|
+
import apiGateway from '@cocreate/api';
|
|
68
|
+
import { CoCreateServer } from '@cocreate/server';
|
|
69
|
+
|
|
70
|
+
// Boot the primary application server instance
|
|
71
|
+
const serverContext = await CoCreateServer.start();
|
|
72
|
+
|
|
73
|
+
// Initialize the API engine and register WebSocket listeners
|
|
74
|
+
await apiGateway.init(serverContext);
|
|
32
75
|
```
|
|
33
76
|
|
|
34
|
-
|
|
77
|
+
### Direct Programmatic API Execution
|
|
78
|
+
|
|
79
|
+
Execute complex third-party outbound macro operations using unified integration objects.
|
|
35
80
|
|
|
36
|
-
```
|
|
37
|
-
|
|
81
|
+
```javascript
|
|
82
|
+
import apiGateway from '@cocreate/api';
|
|
83
|
+
|
|
84
|
+
// The method identifies the provider credentials, and the endpoint resolves the data model
|
|
85
|
+
const payload = {
|
|
86
|
+
organization_id: "64b9a32e18f21bc56789abcd",
|
|
87
|
+
method: "stripe",
|
|
88
|
+
endpoint: "POST /v1/customers",
|
|
89
|
+
stripe: {
|
|
90
|
+
email: "$data.userEmail",
|
|
91
|
+
metadata: {
|
|
92
|
+
workspace: "production"
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
userEmail: "developer@cocreate.app"
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
const result = await apiGateway.executeApi(payload);
|
|
38
99
|
```
|
|
39
100
|
|
|
40
|
-
|
|
101
|
+
---
|
|
41
102
|
|
|
42
|
-
|
|
43
|
-
- [Announcements](#announcements)
|
|
44
|
-
- [Roadmap](#roadmap)
|
|
45
|
-
- [How to Contribute](#how-to-contribute)
|
|
46
|
-
- [About](#about)
|
|
47
|
-
- [License](#license)
|
|
103
|
+
## How it Works
|
|
48
104
|
|
|
49
|
-
|
|
105
|
+
1. **Bootstrap & Event Binding:** During `init()`, the module hooks directly into the server's WebSocket framework (`wsManager`), capturing incoming `api` and `endpoint` event signals to route them to the execution handler.
|
|
106
|
+
2. **Context and Firewall Initialization:** For every outbound execution, the engine evaluates the parameters against a parent context firewall. Security profiles match inbound connections against verified socket bounds, locking `organization_id` and `host` configurations.
|
|
107
|
+
3. **Macro Configuration Resolving:** The engine sends queries through the CRUD synchronization layer to match configuration files against stored endpoints or active integration details inside the database collection.
|
|
108
|
+
4. **AST Matrix Processing:** The router recursively scans headers, parameters, and bodies via `processOperators()`. Found strings matching active variables (such as `$` prefixes) are securely compiled in the isolated `NativeSandbox` environment.
|
|
109
|
+
5. **Outbound Network Fetching:** The system builds the request profile, forcefully sets content headers, maps an `AbortController` timeout circuit breaker (15-second cutoff), and executes the request using the runtime's native `fetch` implementation.
|
|
110
|
+
6. **Payload Delivery & Webhook Dispatching:** Webhook targets matching inbound edge rules validate security values, execute configured workflows sequentially, and return structured responses.
|
|
50
111
|
|
|
51
|
-
|
|
112
|
+
---
|
|
52
113
|
|
|
53
|
-
|
|
114
|
+
## Architecture and Payload Specs
|
|
54
115
|
|
|
55
|
-
|
|
116
|
+
### Unified Payload Object Schema
|
|
56
117
|
|
|
57
|
-
|
|
118
|
+
The API engine relies on flat, declarative payload objects passed through the execution pipeline.
|
|
58
119
|
|
|
59
|
-
|
|
120
|
+
| Field Element | Type | Role |
|
|
121
|
+
| --- | --- | --- |
|
|
122
|
+
| `organization_id` | `String` | **Required.** Targets the tenant profile used to resolve integrations and credentials. |
|
|
123
|
+
| `method` | `String` | **Required.** Provider identity (for example `"stripe"`). |
|
|
124
|
+
| `endpoint` | `String` | **Required.** HTTP method and route used to locate the configured endpoint (for example `"POST /v1/customers"`). |
|
|
125
|
+
| `[providerName]` | `Object` | Provider-specific input object (for example `stripe`) that is processed through the AST engine. |
|
|
60
126
|
|
|
61
|
-
|
|
127
|
+
### Context Engine Parameter Schema
|
|
62
128
|
|
|
63
|
-
|
|
129
|
+
The API engine creates an execution context for every request.
|
|
64
130
|
|
|
65
|
-
|
|
131
|
+
| Field Element | Type | Role |
|
|
132
|
+
| --- | --- | --- |
|
|
133
|
+
| `data` | `Object` | Input payload supplied to the execution pipeline. |
|
|
134
|
+
| `event` | `Object` | Parsed webhook request body. |
|
|
135
|
+
| `actions` | `Array` | Results accumulated from previous workflow steps. |
|
|
136
|
+
| `organization_id` | `String` | Tenant identifier. |
|
|
137
|
+
| `host` | `String` | Verified tenant host used for isolation. |
|
|
138
|
+
| `crud` / `wsManager` | `Object` | References to the CRUD layer and WebSocket manager. |
|
|
66
139
|
|
|
67
|
-
|
|
140
|
+
---
|
|
68
141
|
|
|
69
|
-
|
|
142
|
+
## Security Implementations
|
|
70
143
|
|
|
71
|
-
|
|
144
|
+
> [!WARNING]
|
|
145
|
+
> Parent execution context always overrides incoming payload values to prevent cross-tenant access or privilege escalation.
|
|
72
146
|
|
|
73
|
-
|
|
147
|
+
### Context Sandboxing
|
|
74
148
|
|
|
75
|
-
|
|
149
|
+
Internal services such as `crud` and `wsManager` intercept execution arguments and automatically enforce the authenticated tenant context. Even if a malicious configuration attempts to override tenant information, the runtime replaces it with the verified execution context.
|
|
76
150
|
|
|
77
|
-
|
|
151
|
+
### Timing-Safe Signature Verification
|
|
78
152
|
|
|
79
|
-
|
|
153
|
+
Inbound webhooks configured with authentication rules are validated using `crypto.timingSafeEqual`, preventing timing attacks during signature verification.
|
|
80
154
|
|
|
81
|
-
|
|
155
|
+
---
|
|
82
156
|
|
|
83
|
-
|
|
157
|
+
## How to Contribute
|
|
84
158
|
|
|
85
|
-
|
|
159
|
+
We encourage contributions to all CoCreate libraries. Please see our **[CONTRIBUTING.md](https://github.com/CoCreate-app/CoCreate-api/blob/master/CONTRIBUTING.md)** guide for development workflows and contribution guidelines.
|
|
86
160
|
|
|
87
|
-
|
|
161
|
+
If you discover a bug or would like to request a feature, please open an issue on our **[GitHub Issues](https://github.com/CoCreate-app/CoCreate-api/issues)** tracker.
|
|
88
162
|
|
|
89
|
-
|
|
163
|
+
For complete API documentation, integration examples, and webhook guides, visit:
|
|
164
|
+
|
|
165
|
+
https://cocreatejs.com/docs/api
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
## License
|
|
170
|
+
|
|
171
|
+
This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
|
|
90
172
|
|
|
91
|
-
|
|
173
|
+
- **Open Source Use:** For open-source projects and non-commercial use, this software is available under the AGPLv3. For the full license text, see the LICENSE file.
|
|
174
|
+
- **Commercial Use:** Commercial users must obtain a commercial license. Commercial licenses are available when signing up for an API key through the CoCreate platform.
|
package/package.json
CHANGED
|
@@ -1,24 +1,21 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cocreate/api",
|
|
3
|
-
"version": "1.23.
|
|
4
|
-
"description": "A
|
|
3
|
+
"version": "1.23.3",
|
|
4
|
+
"description": "A secure, multi-tenant API execution gateway and proxy layer that dynamically resolves endpoint actions, processes AST micro-operators, handles webhooks, and manages third-party integrations with strict tenant environment isolation.",
|
|
5
5
|
"keywords": [
|
|
6
|
-
"
|
|
7
|
-
"
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
-
"
|
|
12
|
-
"
|
|
13
|
-
"
|
|
14
|
-
"
|
|
15
|
-
"
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
19
|
-
"shared-editing",
|
|
20
|
-
"html5-framework",
|
|
21
|
-
"javascript-framework"
|
|
6
|
+
"api-orchestration",
|
|
7
|
+
"webhook-orchestration",
|
|
8
|
+
"macro-execution",
|
|
9
|
+
"api-gateway",
|
|
10
|
+
"api-proxy",
|
|
11
|
+
"multi-tenant",
|
|
12
|
+
"ast-interpreter",
|
|
13
|
+
"sandbox",
|
|
14
|
+
"jwt-generator",
|
|
15
|
+
"signature-verification",
|
|
16
|
+
"websocket",
|
|
17
|
+
"serverless-runtime",
|
|
18
|
+
"tenant-isolation"
|
|
22
19
|
],
|
|
23
20
|
"publishConfig": {
|
|
24
21
|
"access": "public"
|
|
@@ -79,7 +76,6 @@
|
|
|
79
76
|
"@cocreate/render@1.46.1": true,
|
|
80
77
|
"@cocreate/socket-client@1.40.5": true,
|
|
81
78
|
"@cocreate/utils@1.42.2": true,
|
|
82
|
-
"@cocreate/uuid@1.12.4": true,
|
|
83
79
|
"@cocreate/webpack@1.4.3": true
|
|
84
80
|
}
|
|
85
81
|
}
|
package/src/server.js
CHANGED
|
@@ -22,41 +22,14 @@
|
|
|
22
22
|
// For details, visit <https://cocreate.app/licenses/> or contact us at sales@cocreate.app.
|
|
23
23
|
|
|
24
24
|
import crypto from 'node:crypto';
|
|
25
|
-
import { URL
|
|
26
|
-
import
|
|
27
|
-
import { getValueFromObject, objectToSearchParams } from '@cocreate/utils';
|
|
25
|
+
import { URL } from 'node:url';
|
|
26
|
+
import { getValueFromObject, objectToSearchParams, astAsync } from '@cocreate/utils';
|
|
28
27
|
|
|
29
28
|
// Core module references bound dynamically upon initialization
|
|
30
29
|
let server = null;
|
|
31
30
|
let wsManager = null;
|
|
32
31
|
let crud = null;
|
|
33
32
|
|
|
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
33
|
const API = {
|
|
61
34
|
init,
|
|
62
35
|
executeApi,
|
|
@@ -64,8 +37,6 @@ const API = {
|
|
|
64
37
|
generateJWT,
|
|
65
38
|
verifySignature,
|
|
66
39
|
makeHttpRequest,
|
|
67
|
-
processOperators,
|
|
68
|
-
processOperator,
|
|
69
40
|
getApiConfig,
|
|
70
41
|
normalizeConfig
|
|
71
42
|
};
|
|
@@ -114,10 +85,99 @@ export function normalizeConfig(apiConfig, name) {
|
|
|
114
85
|
return { key, url, headers };
|
|
115
86
|
}
|
|
116
87
|
|
|
88
|
+
/**
|
|
89
|
+
* Builds a secure sandboxed execution context for evaluateAST.
|
|
90
|
+
* Inherits native globals from AST parser automatically, only injecting runtime-specific scopes.
|
|
91
|
+
*/
|
|
92
|
+
function createSandbox(context) {
|
|
93
|
+
// SECURITY FIREWALL: Automatically proxy local system database calls to seal tenant-isolated metadata.
|
|
94
|
+
const secureCrud = context.crud ? new Proxy(context.crud, {
|
|
95
|
+
get(target, prop) {
|
|
96
|
+
const original = target[prop];
|
|
97
|
+
if (typeof original === 'function') {
|
|
98
|
+
return function(...args) {
|
|
99
|
+
if (args.length === 0) {
|
|
100
|
+
args.push({
|
|
101
|
+
organization_id: context.organization_id,
|
|
102
|
+
host: context.host
|
|
103
|
+
});
|
|
104
|
+
} else if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
|
|
105
|
+
args[0].organization_id = context.organization_id;
|
|
106
|
+
args[0].host = context.host;
|
|
107
|
+
}
|
|
108
|
+
return original.apply(target, args);
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
return original;
|
|
112
|
+
}
|
|
113
|
+
}) : undefined;
|
|
114
|
+
|
|
115
|
+
const secureWsManager = context.wsManager ? new Proxy(context.wsManager, {
|
|
116
|
+
get(target, prop) {
|
|
117
|
+
const original = target[prop];
|
|
118
|
+
if (typeof original === 'function') {
|
|
119
|
+
return function(...args) {
|
|
120
|
+
if (args.length === 0) {
|
|
121
|
+
args.push({
|
|
122
|
+
organization_id: context.organization_id,
|
|
123
|
+
host: context.host
|
|
124
|
+
});
|
|
125
|
+
} else if (args.length > 0 && typeof args[0] === 'object' && args[0] !== null) {
|
|
126
|
+
args[0].organization_id = context.organization_id;
|
|
127
|
+
args[0].host = context.host;
|
|
128
|
+
}
|
|
129
|
+
return original.apply(target, args);
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
return original;
|
|
133
|
+
}
|
|
134
|
+
}) : undefined;
|
|
135
|
+
|
|
136
|
+
// Only expose full server instance to platform admin organization
|
|
137
|
+
const isPlatformOrg = server && server.organization_id && (context.organization_id === server.organization_id);
|
|
138
|
+
|
|
139
|
+
const sandbox = {
|
|
140
|
+
// Core Isolated Dynamic AST Scopes
|
|
141
|
+
data: context.data,
|
|
142
|
+
event: context.event,
|
|
143
|
+
actions: context.actions,
|
|
144
|
+
request: context.request,
|
|
145
|
+
rawBody: context.rawBody,
|
|
146
|
+
|
|
147
|
+
// Safe Proxied System Objects
|
|
148
|
+
crud: secureCrud,
|
|
149
|
+
socket: secureWsManager,
|
|
150
|
+
wsManager: secureWsManager,
|
|
151
|
+
|
|
152
|
+
// Host Context Identifiers
|
|
153
|
+
key: context.key,
|
|
154
|
+
config: context.config,
|
|
155
|
+
organization_id: context.organization_id,
|
|
156
|
+
host: context.host,
|
|
157
|
+
|
|
158
|
+
// Custom operators rewritten as sandbox properties
|
|
159
|
+
api: {
|
|
160
|
+
send: async (apiData) => {
|
|
161
|
+
return await executeApi(apiData, context);
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
jwt: (args) => {
|
|
165
|
+
const privateKey = args?.privateKey;
|
|
166
|
+
if (!privateKey) throw new Error("Missing cryptographic signing key inside $jwt context parameters");
|
|
167
|
+
return generateJWT(args, privateKey);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
// Safely inject elevated server core only when authenticated context is root platform
|
|
172
|
+
if (isPlatformOrg) {
|
|
173
|
+
sandbox.server = server;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
return sandbox;
|
|
177
|
+
}
|
|
178
|
+
|
|
117
179
|
export async function executeApi(data, parentContext = null) {
|
|
118
180
|
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
181
|
const orgId = parentContext?.organization_id || data.socket?.organization_id || data.organization_id;
|
|
122
182
|
const host = parentContext?.host || data.socket?.host || data.host;
|
|
123
183
|
|
|
@@ -156,7 +216,7 @@ export async function executeApi(data, parentContext = null) {
|
|
|
156
216
|
method: 'object.read',
|
|
157
217
|
array: 'endpoints',
|
|
158
218
|
organization_id: orgId,
|
|
159
|
-
host: host,
|
|
219
|
+
host: host,
|
|
160
220
|
$filter: {
|
|
161
221
|
query: { endpoint: data.endpoint, organization_id: orgId },
|
|
162
222
|
limit: 1
|
|
@@ -165,20 +225,18 @@ export async function executeApi(data, parentContext = null) {
|
|
|
165
225
|
const endpointResponse = await crud.send(endpointQuery);
|
|
166
226
|
const endpointConfig = endpointResponse?.object?.[0] || {};
|
|
167
227
|
|
|
168
|
-
// Fetch provider credentials with full environmental scope intact
|
|
169
228
|
const rawApiConfig = await getApiConfig({ organization_id: orgId, host }, providerName);
|
|
170
229
|
if (!rawApiConfig) {
|
|
171
230
|
throw new Error(`Connection setting not registered for provider: '${providerName}'.`);
|
|
172
231
|
}
|
|
173
232
|
const apiConfig = normalizeConfig(rawApiConfig, providerName);
|
|
174
233
|
|
|
175
|
-
// Share parent context if executing within a nested macro frame
|
|
176
234
|
const apiContext = parentContext || {
|
|
177
235
|
data: data,
|
|
178
236
|
event: {},
|
|
179
237
|
actions: [],
|
|
180
238
|
organization_id: orgId,
|
|
181
|
-
host: host,
|
|
239
|
+
host: host,
|
|
182
240
|
crud,
|
|
183
241
|
wsManager,
|
|
184
242
|
key: apiConfig.key,
|
|
@@ -189,15 +247,15 @@ export async function executeApi(data, parentContext = null) {
|
|
|
189
247
|
if (actions) {
|
|
190
248
|
if (Array.isArray(actions)) {
|
|
191
249
|
for (let i = 0; i < actions.length; i++) {
|
|
192
|
-
const result = await
|
|
193
|
-
apiContext.actions.push(result);
|
|
250
|
+
const result = await astAsync({ executeAST: actions[i], sandbox: createSandbox(apiContext) });
|
|
251
|
+
apiContext.actions.push(result);
|
|
194
252
|
}
|
|
195
253
|
const finalMacroResult = apiContext.actions[apiContext.actions.length - 1];
|
|
196
254
|
if (finalMacroResult !== undefined) {
|
|
197
255
|
data[providerName] = finalMacroResult;
|
|
198
256
|
}
|
|
199
257
|
} else {
|
|
200
|
-
const result = await
|
|
258
|
+
const result = await astAsync({ executeAST: actions, sandbox: createSandbox(apiContext) });
|
|
201
259
|
apiContext.actions.push(result);
|
|
202
260
|
if (result !== undefined) {
|
|
203
261
|
data[providerName] = result;
|
|
@@ -225,13 +283,13 @@ export async function executeApi(data, parentContext = null) {
|
|
|
225
283
|
}
|
|
226
284
|
|
|
227
285
|
let headers = { ...(apiConfig.headers || {}), ...(endpointConfig.headers || {}) };
|
|
228
|
-
headers = await
|
|
286
|
+
headers = await astAsync({ executeAST: headers, sandbox: createSandbox(apiContext) });
|
|
229
287
|
|
|
230
288
|
const timeout = 15000;
|
|
231
289
|
let options = { method: httpMethod, headers, timeout };
|
|
232
290
|
|
|
233
291
|
if (!["GET", "HEAD"].includes(httpMethod) && data[providerName]) {
|
|
234
|
-
const bodyPayload = await
|
|
292
|
+
const bodyPayload = await astAsync({ executeAST: data[providerName], sandbox: createSandbox(apiContext) });
|
|
235
293
|
|
|
236
294
|
if (typeof bodyPayload === 'object' && bodyPayload !== null) {
|
|
237
295
|
options.body = JSON.stringify(bodyPayload);
|
|
@@ -284,7 +342,7 @@ export async function request(req, res) {
|
|
|
284
342
|
method: 'object.read',
|
|
285
343
|
array: 'webhooks',
|
|
286
344
|
organization_id: organization._id,
|
|
287
|
-
host: hostname,
|
|
345
|
+
host: hostname,
|
|
288
346
|
$filter: {
|
|
289
347
|
query: {
|
|
290
348
|
path: webhookPath,
|
|
@@ -326,7 +384,7 @@ export async function request(req, res) {
|
|
|
326
384
|
request: req,
|
|
327
385
|
response: res,
|
|
328
386
|
rawBody: rawBody,
|
|
329
|
-
host: hostname,
|
|
387
|
+
host: hostname,
|
|
330
388
|
organization: organization,
|
|
331
389
|
organization_id: organization._id,
|
|
332
390
|
crud: crud,
|
|
@@ -338,7 +396,7 @@ export async function request(req, res) {
|
|
|
338
396
|
if (webhookConfig.authenticate) {
|
|
339
397
|
const auth = webhookConfig.authenticate;
|
|
340
398
|
if (auth.method === "webhooks.constructEvent" && Array.isArray(auth.parameters)) {
|
|
341
|
-
const resolvedParams = await
|
|
399
|
+
const resolvedParams = await astAsync({ executeAST: [...auth.parameters], sandbox: createSandbox(context) });
|
|
342
400
|
const payload = resolvedParams[0];
|
|
343
401
|
const signature = resolvedParams[1];
|
|
344
402
|
const secret = resolvedParams[2];
|
|
@@ -355,7 +413,7 @@ export async function request(req, res) {
|
|
|
355
413
|
|
|
356
414
|
if (actionConfig) {
|
|
357
415
|
if (typeof actionConfig === 'string') {
|
|
358
|
-
const actionName = await
|
|
416
|
+
const actionName = await astAsync({ executeAST: actionConfig, sandbox: createSandbox(context) });
|
|
359
417
|
if (actionName && webhookConfig.actions?.[actionName]) {
|
|
360
418
|
executeOperations = webhookConfig.actions[actionName];
|
|
361
419
|
}
|
|
@@ -367,16 +425,15 @@ export async function request(req, res) {
|
|
|
367
425
|
if (executeOperations) {
|
|
368
426
|
if (Array.isArray(executeOperations)) {
|
|
369
427
|
for (let i = 0; i < executeOperations.length; i++) {
|
|
370
|
-
const stepResult = await
|
|
428
|
+
const stepResult = await astAsync({ executeAST: executeOperations[i], sandbox: createSandbox(context) });
|
|
371
429
|
context.actions.push(stepResult);
|
|
372
430
|
}
|
|
373
431
|
} else if (typeof executeOperations === 'object' && executeOperations !== null) {
|
|
374
|
-
const stepResult = await
|
|
432
|
+
const stepResult = await astAsync({ executeAST: [executeOperations], sandbox: createSandbox(context) });
|
|
375
433
|
context.actions.push(stepResult);
|
|
376
434
|
}
|
|
377
435
|
}
|
|
378
436
|
|
|
379
|
-
// Native Stream Inspection: Fall back to default 200 OK only if actions did not write or end the stream
|
|
380
437
|
if (res && !res.writableEnded) {
|
|
381
438
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
382
439
|
res.end(JSON.stringify({
|
|
@@ -392,163 +449,6 @@ export async function request(req, res) {
|
|
|
392
449
|
}
|
|
393
450
|
}
|
|
394
451
|
|
|
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
452
|
export function generateJWT(config, secret) {
|
|
553
453
|
try {
|
|
554
454
|
const header = config.header || { alg: 'HS256', typ: 'JWT' };
|
|
@@ -572,16 +472,26 @@ export function generateJWT(config, secret) {
|
|
|
572
472
|
|
|
573
473
|
export function verifySignature(payload, signature, secret, algorithm = 'sha256') {
|
|
574
474
|
try {
|
|
475
|
+
if (!payload || !signature || !secret) return false;
|
|
476
|
+
|
|
575
477
|
const hmac = crypto.createHmac(algorithm, secret);
|
|
576
478
|
hmac.update(payload);
|
|
577
|
-
const expectedSignature = hmac.digest('hex');
|
|
578
479
|
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
)
|
|
480
|
+
const expectedSignature = hmac.digest();
|
|
481
|
+
let receivedSignature = Buffer.isBuffer(signature) ? signature : Buffer.from(signature, 'hex');
|
|
482
|
+
|
|
483
|
+
if (receivedSignature.length !== expectedSignature.length) {
|
|
484
|
+
receivedSignature = Buffer.from(signature, 'base64');
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
if (receivedSignature.length !== expectedSignature.length) {
|
|
488
|
+
console.warn('[@cocreate/api] Signature verification rejected immediately due to byte-length mismatch.');
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
return crypto.timingSafeEqual(expectedSignature, receivedSignature);
|
|
583
493
|
} catch (err) {
|
|
584
|
-
console.error('[@cocreate/api] Failed signature verification:', err);
|
|
494
|
+
console.error('[@cocreate/api] Failed signature verification:', err.message);
|
|
585
495
|
return false;
|
|
586
496
|
}
|
|
587
497
|
}
|
|
@@ -625,7 +535,7 @@ export async function getApiConfig(data, name) {
|
|
|
625
535
|
let host = data.host;
|
|
626
536
|
|
|
627
537
|
if (!orgId) {
|
|
628
|
-
const organization = await crud.getOrganization(data);
|
|
538
|
+
const organization = await crud.getOrganization(data);
|
|
629
539
|
if (organization.error) throw new Error(organization.error);
|
|
630
540
|
orgId = organization._id;
|
|
631
541
|
}
|
|
@@ -634,7 +544,7 @@ export async function getApiConfig(data, name) {
|
|
|
634
544
|
method: 'object.read',
|
|
635
545
|
array: 'apis',
|
|
636
546
|
organization_id: orgId,
|
|
637
|
-
host: host,
|
|
547
|
+
host: host,
|
|
638
548
|
$filter: {
|
|
639
549
|
query: { name: name, organization_id: orgId },
|
|
640
550
|
limit: 1
|