Skip to content

Commit 05fb81e

Browse files
authored
feat: HTTP external functions SDK support (#33)
* feat: add registerHttpFunction API for HTTP external functions - Node: registerHttpFunction(id, config), HttpInvocationConfig types, integration test - Rust: register_http_function, HttpInvocationConfig, HttpFunctionRef, unit tests - Python: register_http_function, HttpInvocationConfig, unit + integration tests - Add HttpFunctionsModule to config-test.yaml for integration tests * feat(tests): enhance HTTP external functions tests for registration, event delivery, and unregistration - Added tests for registering and unregistering HTTP functions, ensuring proper function lifecycle management. - Implemented tests for delivering events with custom headers and to multiple external functions across different topics. - Included a test to verify that events stop being delivered after unregistration. - Enhanced the WebhookProbe class to capture headers from incoming requests for more comprehensive testing. - Updated integration tests to cover all configuration options for HTTP functions, including authentication and custom headers. * feat: add validation for duplicate function IDs in registration - Implemented checks in Node, Python, and Rust SDKs to prevent registration of functions with duplicate IDs, enhancing error handling and ensuring unique function identifiers. - Updated tests to verify the new validation logic for HTTP external functions and standard functions, ensuring robustness in function registration processes. * fix: improve enum variant handling and clean up serialization code - Added a Clippy lint allowance for large enum variants in the `Outbound` enum to enhance code quality. - Simplified the serialization call in the tests by removing unnecessary method chaining, improving readability and maintainability. * feat: update test configuration for HTTP functions and API module - Added a new port configuration for the HTTP functions module. - Updated the port for the Rest API module to 3199. - Introduced security settings for the HTTP functions module, including URL allowlist and HTTPS requirements.
1 parent 22c6405 commit 05fb81e

File tree

14 files changed

+1368
-7
lines changed

14 files changed

+1368
-7
lines changed

.github/engine-config/test-config.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
port: 49134
12
modules:
23
- class: modules::stream::StreamModule
34
config:
@@ -19,7 +20,7 @@ modules:
1920

2021
- class: modules::api::RestApiModule
2122
config:
22-
port: 3111
23+
port: 3199
2324
host: 127.0.0.1
2425
default_timeout: 30000
2526
concurrency_request_limit: 1024
@@ -59,6 +60,15 @@ modules:
5960
adapter:
6061
class: modules::queue::BuiltinQueueAdapter
6162

63+
- class: modules::http_functions::HttpFunctionsModule
64+
config:
65+
security:
66+
url_allowlist:
67+
- localhost
68+
- 127.0.0.1
69+
block_private_ips: false
70+
require_https: false
71+
6272
- class: modules::pubsub::PubSubModule
6373
config:
6474
adapter:

packages/node/iii/src/iii-types.ts

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,19 @@ export type RegisterServiceMessage = {
5353
parent_service_id?: string
5454
}
5555

56+
export type HttpAuthConfig =
57+
| { type: 'hmac'; secret_key: string }
58+
| { type: 'bearer'; token_key: string }
59+
| { type: 'api_key'; header: string; value_key: string }
60+
61+
export type HttpInvocationConfig = {
62+
url: string
63+
method?: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'
64+
timeout_ms?: number
65+
headers?: Record<string, string>
66+
auth?: HttpAuthConfig
67+
}
68+
5669
export type RegisterFunctionFormat = {
5770
name: string
5871
/**
@@ -80,7 +93,7 @@ export type RegisterFunctionFormat = {
8093
export type RegisterFunctionMessage = {
8194
message_type: MessageType.RegisterFunction
8295
/**
83-
* The path of the function
96+
* The path of the function (use :: for namespacing, e.g. external::my_lambda)
8497
*/
8598
id: string
8699
/**
@@ -96,6 +109,10 @@ export type RegisterFunctionMessage = {
96109
*/
97110
response_format?: RegisterFunctionFormat
98111
metadata?: Record<string, unknown>
112+
/**
113+
* HTTP invocation config for external HTTP functions (Lambda, Cloudflare Workers, etc.)
114+
*/
115+
invocation?: HttpInvocationConfig
99116
}
100117

101118
export type InvokeFunctionMessage = {

packages/node/iii/src/iii.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import {
1414
import {
1515
type IIIMessage,
1616
type FunctionInfo,
17+
type HttpInvocationConfig,
1718
type InvocationResultMessage,
1819
type InvokeFunctionMessage,
1920
MessageType,
@@ -101,6 +102,7 @@ export type InitOptions = {
101102
class Sdk implements ISdk {
102103
private ws?: WebSocket
103104
private functions = new Map<string, RemoteFunctionData>()
105+
private httpFunctions = new Map<string, RegisterFunctionMessage>()
104106
private services = new Map<string, Omit<RegisterServiceMessage, 'functions'>>()
105107
private invocations = new Map<string, Invocation & { timeout?: NodeJS.Timeout }>()
106108
private triggers = new Map<string, RegisterTriggerMessage>()
@@ -190,6 +192,9 @@ class Sdk implements ISdk {
190192
if (!message.id || message.id.trim() === '') {
191193
throw new Error('id is required')
192194
}
195+
if (this.httpFunctions.has(message.id)) {
196+
throw new Error(`function id already registered: ${message.id}`)
197+
}
193198

194199
this.sendMessage(MessageType.RegisterFunction, message, true)
195200
this.functions.set(message.id, {
@@ -230,6 +235,38 @@ class Sdk implements ISdk {
230235
}
231236
}
232237

238+
registerHttpFunction = (id: string, config: HttpInvocationConfig): FunctionRef => {
239+
if (!id || id.trim() === '') {
240+
throw new Error('id is required')
241+
}
242+
if (this.functions.has(id) || this.httpFunctions.has(id)) {
243+
throw new Error(`function id already registered: ${id}`)
244+
}
245+
246+
const message: RegisterFunctionMessage = {
247+
message_type: MessageType.RegisterFunction,
248+
id,
249+
invocation: {
250+
url: config.url,
251+
method: config.method ?? 'POST',
252+
timeout_ms: config.timeout_ms,
253+
headers: config.headers,
254+
auth: config.auth,
255+
},
256+
}
257+
258+
this.sendMessage(MessageType.RegisterFunction, message, true)
259+
this.httpFunctions.set(id, message)
260+
261+
return {
262+
id,
263+
unregister: () => {
264+
this.sendMessage(MessageType.UnregisterFunction, { id }, true)
265+
this.httpFunctions.delete(id)
266+
},
267+
}
268+
}
269+
233270
registerService = (message: Omit<RegisterServiceMessage, 'message_type'>): void => {
234271
this.sendMessage(MessageType.RegisterService, message, true)
235272
this.services.set(message.id, { ...message, message_type: MessageType.RegisterService })
@@ -606,6 +643,9 @@ class Sdk implements ISdk {
606643
this.functions.forEach(({ message }) => {
607644
this.sendMessage(MessageType.RegisterFunction, message, true)
608645
})
646+
this.httpFunctions.forEach(message => {
647+
this.sendMessage(MessageType.RegisterFunction, message, true)
648+
})
609649
this.triggers.forEach(trigger => {
610650
this.sendMessage(MessageType.RegisterTrigger, trigger, true)
611651
})

packages/node/iii/src/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ export { Logger } from './logger'
55
export type { ISdk } from './types'
66

77
export type { ApiRequest, HttpRequest, HttpResponse, ApiResponse, Channel } from './types'
8+
export type { HttpInvocationConfig, HttpAuthConfig } from './iii-types'
89
export type { StreamChannelRef } from './iii-types'
910
export { ChannelWriter, ChannelReader } from './channels'
1011

packages/node/iii/src/types.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import type {
22
FunctionInfo,
3+
HttpInvocationConfig,
34
RegisterFunctionMessage,
45
RegisterServiceMessage,
56
RegisterTriggerMessage,
@@ -105,6 +106,14 @@ export interface ISdk {
105106
*/
106107
registerFunction(func: RegisterFunctionInput, handler: RemoteFunctionHandler): FunctionRef
107108

109+
/**
110+
* Registers an HTTP external function (Lambda, Cloudflare Workers, etc.). The engine invokes the URL when triggered.
111+
* @param id - Function path (use :: for namespacing, e.g. external::my_lambda)
112+
* @param config - HTTP endpoint config (url, method, timeout_ms, headers, auth)
113+
* @returns A function ref for unregistering
114+
*/
115+
registerHttpFunction(id: string, config: HttpInvocationConfig): FunctionRef
116+
108117
/**
109118
* Invokes a function.
110119
* @param function_id - The path to the function

packages/node/iii/tests/fixtures/config-test.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,15 @@ modules:
6060
adapter:
6161
class: modules::queue::BuiltinQueueAdapter
6262

63+
- class: modules::http_functions::HttpFunctionsModule
64+
config:
65+
security:
66+
url_allowlist:
67+
- localhost
68+
- 127.0.0.1
69+
block_private_ips: false
70+
require_https: false
71+
6372
- class: modules::cron::CronModule
6473
config:
6574
adapter:

0 commit comments

Comments
 (0)