@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.
Files changed (3) hide show
  1. package/README.md +135 -52
  2. package/package.json +15 -19
  3. package/src/server.js +124 -214
package/README.md CHANGED
@@ -1,91 +1,174 @@
1
- # CoCreate-api
1
+ # @cocreate/api
2
2
 
3
- 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. Take it for a spin in our [playground!](https://cocreate.app/docs/api)
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
- ![minified](https://img.badgesize.io/https://cdn.cocreate.app/api/latest/CoCreate-api.min.js?style=flat-square&label=minified&color=orange)
6
- ![gzip](https://img.badgesize.io/https://cdn.cocreate.app/api/latest/CoCreate-api.min.js?compression=gzip&style=flat-square&label=gzip&color=yellow)
7
- ![brotli](https://img.badgesize.io/https://cdn.cocreate.app/api/latest/CoCreate-api.min.js?compression=brotli&style=flat-square&label=brotli)
8
- ![GitHub latest release](https://img.shields.io/github/v/release/CoCreate-app/CoCreate-api?style=flat-square)
9
- ![License](https://img.shields.io/github/license/CoCreate-app/CoCreate-api?style=flat-square)
10
- ![Hiring](https://img.shields.io/static/v1?style=flat-square&label=&message=Hiring&color=blueviolet)
5
+ ---
11
6
 
12
- ![CoCreate-api](https://cdn.cocreate.app/docs/CoCreate-api.gif)
7
+ ## Documentation
13
8
 
14
- ## [Docs & Demo](https://cocreate.app/docs/api)
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
- For a complete guide and working demo refer to the [doumentation](https://cocreate.app/docs/api)
11
+ ## Table of Contents
17
12
 
18
- ## CDN
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
- ```html
21
- <script src="https://cdn.cocreate.app/api/latest/CoCreate-api.min.js"></script>
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
- ```html
25
- <script src="https://cdn.cocreate.app/api/latest/CoCreate-api.min.css"></script>
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
- ## NPM
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
- ```shell
31
- $ npm i @cocreate/api
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
- ## yarn
77
+ ### Direct Programmatic API Execution
78
+
79
+ Execute complex third-party outbound macro operations using unified integration objects.
35
80
 
36
- ```shell
37
- $ yarn install @cocreate/api
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
- # Table of Contents
101
+ ---
41
102
 
42
- - [Table of Contents](#table-of-contents)
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
- <a name="announcements"></a>
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
- # Announcements
112
+ ---
52
113
 
53
- All updates to this library are documented in our [CHANGELOG](https://github.com/CoCreate-app/CoCreate-api/blob/master/CHANGELOG.md) and [releases](https://github.com/CoCreate-app/CoCreate-api/releases). You may also subscribe to email for releases and breaking changes.
114
+ ## Architecture and Payload Specs
54
115
 
55
- <a name="roadmap"></a>
116
+ ### Unified Payload Object Schema
56
117
 
57
- # Roadmap
118
+ The API engine relies on flat, declarative payload objects passed through the execution pipeline.
58
119
 
59
- If you are interested in the future direction of this project, please take a look at our open [issues](https://github.com/CoCreate-app/CoCreate-api/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-api/pulls). We would love to hear your feedback.
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
- <a name="about"></a>
127
+ ### Context Engine Parameter Schema
62
128
 
63
- # About
129
+ The API engine creates an execution context for every request.
64
130
 
65
- CoCreate-api is guided and supported by the CoCreate Developer Experience Team.
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
- Please Email the Developer Experience Team [here](mailto:develop@cocreate.app) in case of any queries.
140
+ ---
68
141
 
69
- CoCreate-api is maintained and funded by CoCreate. The names and logos for CoCreate are trademarks of CoCreate, LLC.
142
+ ## Security Implementations
70
143
 
71
- <a name="contribute"></a>
144
+ > [!WARNING]
145
+ > Parent execution context always overrides incoming payload values to prevent cross-tenant access or privilege escalation.
72
146
 
73
- # How to Contribute
147
+ ### Context Sandboxing
74
148
 
75
- We encourage contribution to our libraries (you might even score some nifty swag), please see our [CONTRIBUTING](https://github.com/CoCreate-app/CoCreate-api/blob/master/CONTRIBUTING.md) guide for details.
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
- We want this library to be community-driven, and CoCreate led. We need your help to realize this goal. To help make sure we are building the right things in the right order, we ask that you create [issues](https://github.com/CoCreate-app/CoCreate-api/issues) and [pull requests](https://github.com/CoCreate-app/CoCreate-api/pulls) or merely upvote or comment on existing issues or pull requests.
151
+ ### Timing-Safe Signature Verification
78
152
 
79
- We appreciate your continued support, thank you!
153
+ Inbound webhooks configured with authentication rules are validated using `crypto.timingSafeEqual`, preventing timing attacks during signature verification.
80
154
 
81
- <a name="license"></a>
155
+ ---
82
156
 
83
- # License
157
+ ## How to Contribute
84
158
 
85
- This software is dual-licensed under the GNU Affero General Public License version 3 (AGPLv3) and a commercial license.
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
- - **Open Source Use**: For open-source projects and non-commercial use, this software is available under the AGPLv3. The AGPLv3 allows you to freely use, modify, and distribute this software, provided that all modifications and derivative works are also licensed under the AGPLv3. For the full license text, see the [LICENSE file](https://github.com/CoCreate-app/CoCreate-api/blob/master/LICENSE).
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
- - **Commercial Use**: For-profit companies and individuals intending to use this software for commercial purposes must obtain a commercial license. The commercial license is available when you sign up for an API key on our [website](https://cocreate.app). This license permits proprietary use and modification of the software without the copyleft requirements of the AGPLv3. It is ideal for integrating this software into proprietary commercial products and applications.
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
- If you have not purchased a commercial license and intend to use this software for commercial purposes, you are required to sign up for an API key on our website.
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.2",
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. ",
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
- "thirdparty-api-intergration",
7
- "thirdparty-api-tool",
8
- "cocreate-api",
9
- "cocreate",
10
- "no-code-framework",
11
- "cocreatejs",
12
- "cocreatejs-component",
13
- "cocreate-framework",
14
- "no-code",
15
- "low-code",
16
- "realtime",
17
- "realtime-framework",
18
- "collaboration",
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, URLSearchParams } from 'node:url';
26
- import Config from '@cocreate/config';
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, // Carry host forward
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, // Lock host context into AST evaluation loop
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 processOperators(apiContext, actions[i]);
193
- apiContext.actions.push(result); // Saved flattened memory profile
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 processOperators(apiContext, actions);
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 processOperators(apiContext, headers);
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 processOperators(apiContext, data[providerName]);
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, // Maintain active host environment
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, // Propagate verified edge 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 processOperators(context, [...auth.parameters]);
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 processOperators(context, actionConfig);
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 processOperators(context, executeOperations[i]);
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 processOperators(context, [executeOperations]);
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
- return crypto.timingSafeEqual(
580
- Buffer.from(expectedSignature),
581
- Buffer.from(signature)
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); // uses host inside data to lookup org
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, // Keep environment host isolation contact
547
+ host: host,
638
548
  $filter: {
639
549
  query: { name: name, organization_id: orgId },
640
550
  limit: 1