426e9eeabd
Voice Workbench / headless workbench (mocked backends) (push) Has been cancelled
Voice Workbench / real acoustic lane (nightly, provisioned only) (push) Has been cancelled
ci / test (push) Has been cancelled
ci / lint-and-format (push) Has been cancelled
ci / build (push) Has been cancelled
ci / dev-startup (push) Has been cancelled
gitleaks / gitleaks (push) Has been cancelled
Markdown Links / Relative Markdown Links (push) Has been cancelled
Quality (Extended) / Homepage Build (PR smoke) (push) Has been cancelled
Quality (Extended) / Comment-only diff guard (push) Has been cancelled
Quality (Extended) / Format + Type Safety Ratchet (push) Has been cancelled
Quality (Extended) / Develop Gate (secret scan + UI determinism) (push) Has been cancelled
Quality (Extended) / Develop Gate (lint) (push) Has been cancelled
Chat shell gestures / Chat shell gesture + parity e2e (push) Has been cancelled
Cloud Gateway Discord / Test (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx @biomejs/biome check packages/lifeops-bench/src, benchmark-lint) (push) Has been cancelled
Benchmark Bridge Tests / benchmark (bunx vitest run --config packages/lifeops-bench/vitest.config.ts --root packages/lifeops-bench --passWithNoTests, benchmark-tests) (push) Has been cancelled
Build Agent Image / build-and-push (push) Has been cancelled
Dev Smoke / bun run dev onboarding chat (push) Has been cancelled
Dev Smoke / Vite HMR dependency-level smoke (push) Has been cancelled
Electrobun Submodule Guard / electrobun gitlink is fetchable (push) Has been cancelled
Publish @elizaos/example-code / check_npm (push) Has been cancelled
Publish @elizaos/example-code / publish_npm (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / verify_version (push) Has been cancelled
Publish @elizaos/plugin-elizacloud / publish_npm (push) Has been cancelled
Sandbox Live Smoke / Sandbox live smoke (push) Has been cancelled
Snap Build & Test / Build Snap (amd64) (push) Has been cancelled
Snap Build & Test / Build Snap (arm64) (push) Has been cancelled
Test Packaging / elizaos CLI global-install smoke (node + bun) (push) Has been cancelled
Cloud Gateway Webhook / Test (push) Has been cancelled
Cloud Tests / lint-and-types (push) Has been cancelled
Cloud Tests / unit-tests (push) Has been cancelled
Cloud Tests / integration-tests (push) Has been cancelled
Cloud Tests / e2e-tests (push) Has been cancelled
CodeQL Advanced / Analyze (javascript-typescript) (push) Has been cancelled
Deploy Apps Worker (Product 2) / Determine environment (push) Has been cancelled
Deploy Apps Worker (Product 2) / Deploy apps worker to apps-control host (${{ needs.determine-env.outputs.environment }}) (push) Has been cancelled
Deploy Eliza Provisioning Worker / Determine environment (push) Has been cancelled
Deploy Eliza Provisioning Worker / Deploy worker to Hetzner host (${{ needs.determine-env.outputs.environment }} @ ${{ needs.determine-env.outputs.deployment_sha }}) (push) Has been cancelled
Dev Smoke / Classify changed paths (push) Has been cancelled
supply-chain / sbom (push) Has been cancelled
supply-chain / vulnerability-scan (push) Has been cancelled
Build, Push & Deploy to Phala Cloud / build-and-push (push) Has been cancelled
Test Packaging / Validate Packaging Configs (push) Has been cancelled
Test Packaging / Build & Test PyPI Package (push) Has been cancelled
Test Packaging / PyPI on Python ${{ matrix.python }} (push) Has been cancelled
Test Packaging / Pack & Test JS Tarballs (push) Has been cancelled
UI Fixture E2E / ui-fixture-e2e (push) Has been cancelled
UI Fixture E2E / fixture-e2e (push) Has been cancelled
UI Story Gate / story-gate (push) Has been cancelled
vault-ci / test (macos-latest) (push) Has been cancelled
vault-ci / test (ubuntu-latest) (push) Has been cancelled
vault-ci / test (windows-latest) (push) Has been cancelled
vault-ci / app-core wiring tests (push) Has been cancelled
verify-patches / verify patches/CHECKSUMS.sha256 (push) Has been cancelled
Voice Benchmark Smoke / voice-emotion fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voiceagentbench fixture smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench-quality unit smoke (push) Has been cancelled
Voice Benchmark Smoke / voicebench TypeScript unit (no audio) (push) Has been cancelled
Voice Benchmark Smoke / voice bench smoke summary (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/app-core test bun run --cwd packages/elizaos test bun run --cwd packages/cloud/shared test], app-and-cli) (push) Has been cancelled
Windows CI / windows ([bun run --cwd packages/scenario-runner test bun run --cwd packages/vault test bun run --cwd packages/security test bun run --cwd plugins/plugin-coding-tools test], framework-packages) (push) Has been cancelled
Windows CI / windows ([bun run --cwd plugins/plugin-elizacloud test bun run --cwd plugins/plugin-discord test bun run --cwd plugins/plugin-anthropic test bun run --cwd plugins/plugin-openai test bun run --cwd plugins/plugin-app-control test bun run --cwd plugins/pl… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run build --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/agent --concurrency=4 node packages/scripts/run-bash-linux-only.mjs scripts/verify-riscv64-buildpaths.sh node packages/scripts/run… (push) Has been cancelled
Windows CI / windows ([node packages/scripts/run-turbo.mjs run typecheck --filter=@elizaos/core --filter=@elizaos/shared --filter=@elizaos/cloud-shared --concurrency=4 bun run --cwd packages/core test bun run --cwd packages/shared test], core-runtime, 75) (push) Has been cancelled
154 lines
6.1 KiB
Swift
154 lines
6.1 KiB
Swift
import Foundation
|
|
import JavaScriptCore
|
|
|
|
/// Implements `http_fetch` from `BRIDGE_CONTRACT.md`.
|
|
///
|
|
/// Returns a JS Promise via the constructor pattern: the bridge captures the
|
|
/// resolve callback as a `ManagedCallback`, runs the URLSession request off
|
|
/// the JSContext queue, then re-enters the JSContext queue to fulfill the
|
|
/// promise.
|
|
public final class HTTPBridge {
|
|
private weak var context: JSContext?
|
|
private let urlSession: URLSession
|
|
|
|
public init(session: URLSession = .shared) {
|
|
self.urlSession = session
|
|
}
|
|
|
|
public func install(into ctx: JSContext) {
|
|
self.context = ctx
|
|
|
|
ctx.installBridgeFunction(name: "http_fetch") { args in
|
|
guard let ctx = self.context else { return NSNull() }
|
|
guard let opts = args.first, opts.isObject else {
|
|
return Self.rejectedPromise(in: ctx, error: "http_fetch: missing options")
|
|
}
|
|
|
|
let url = opts.objectForKeyedSubscript("url")?.toString() ?? ""
|
|
let method = (opts.objectForKeyedSubscript("method")?.toString() ?? "GET").uppercased()
|
|
let headers = opts.objectForKeyedSubscript("headers")?.toStringMap() ?? [:]
|
|
let bodyValue = opts.objectForKeyedSubscript("body")
|
|
let body: Data? = (bodyValue?.isNullish == false) ? bodyValue?.toData() : nil
|
|
let timeoutMs = opts.objectForKeyedSubscript("timeout_ms")?.toNumber()?.doubleValue
|
|
|
|
guard let target = URL(string: url) else {
|
|
return Self.rejectedPromise(in: ctx, error: "http_fetch: invalid url")
|
|
}
|
|
guard target.scheme == "http" || target.scheme == "https" else {
|
|
return Self.rejectedPromise(in: ctx, error: "http_fetch: unsupported URL scheme")
|
|
}
|
|
if Self.isLocalOrPrivateNetwork(target) {
|
|
return Self.rejectedPromise(in: ctx, error: "http_fetch: loopback/private URLs must use path-only http_request IPC")
|
|
}
|
|
|
|
var request = URLRequest(url: target)
|
|
request.httpMethod = method
|
|
for (k, v) in headers {
|
|
request.setValue(v, forHTTPHeaderField: k)
|
|
}
|
|
if let body = body, method != "GET", method != "HEAD" {
|
|
request.httpBody = body
|
|
}
|
|
if let ms = timeoutMs, ms > 0 {
|
|
request.timeoutInterval = ms / 1000.0
|
|
}
|
|
|
|
return self.runFetch(ctx: ctx, request: request)
|
|
}
|
|
}
|
|
|
|
// MARK: - Internals
|
|
|
|
private func runFetch(ctx: JSContext, request: URLRequest) -> Any? {
|
|
// Build the promise factory once.
|
|
let promiseScript = """
|
|
(function(){
|
|
let resolveFn;
|
|
const p = new Promise(function(res){ resolveFn = res; });
|
|
p.__eliza_resolve = resolveFn;
|
|
return p;
|
|
})
|
|
"""
|
|
guard let promise = ctx.evaluateScript(promiseScript)?.call(withArguments: []) else {
|
|
return Self.rejectedPromise(in: ctx, error: "http_fetch: failed to construct promise")
|
|
}
|
|
let resolveValue = promise.forProperty("__eliza_resolve")
|
|
let managedResolve = resolveValue.flatMap { ManagedCallback(value: $0) }
|
|
|
|
let task = urlSession.dataTask(with: request) { data, response, error in
|
|
let result = Self.buildResultDict(data: data, response: response, error: error, ctx: ctx)
|
|
// Hop back onto the JS queue.
|
|
RuntimeQueue.dispatchOnJS {
|
|
guard let resolve = managedResolve else { return }
|
|
resolve.callSync(args: [result])
|
|
}
|
|
}
|
|
task.resume()
|
|
|
|
return promise
|
|
}
|
|
|
|
private static func isLocalOrPrivateNetwork(_ url: URL) -> Bool {
|
|
guard let host = url.host?.lowercased() else { return false }
|
|
return host == "localhost" ||
|
|
host == "127.0.0.1" ||
|
|
host == "0.0.0.0" ||
|
|
host == "::1" ||
|
|
host.hasPrefix("127.") ||
|
|
host.hasPrefix("10.") ||
|
|
host.hasPrefix("192.168.") ||
|
|
host.range(of: #"^172\.(1[6-9]|2[0-9]|3[0-1])\."#, options: .regularExpression) != nil ||
|
|
host.range(of: #"^100\.(6[4-9]|[7-9][0-9]|1[0-1][0-9]|12[0-7])\."#, options: .regularExpression) != nil ||
|
|
host.hasPrefix("169.254.") ||
|
|
(host.contains(":") &&
|
|
(host.hasPrefix("fe80:") ||
|
|
host.hasPrefix("fc") ||
|
|
host.hasPrefix("fd"))) ||
|
|
host == "local" ||
|
|
host == "internal" ||
|
|
host == "lan" ||
|
|
host == "ts.net" ||
|
|
host.hasSuffix(".local") ||
|
|
host.hasSuffix(".internal") ||
|
|
host.hasSuffix(".lan") ||
|
|
host.hasSuffix(".ts.net")
|
|
}
|
|
|
|
private static func buildResultDict(data: Data?, response: URLResponse?, error: Error?, ctx: JSContext) -> [String: Any] {
|
|
if let error = error {
|
|
return [
|
|
"status": 0,
|
|
"headers": [:] as [String: String],
|
|
"body": ctx.newUint8Array(Data()),
|
|
"error": error.localizedDescription,
|
|
]
|
|
}
|
|
guard let http = response as? HTTPURLResponse else {
|
|
return [
|
|
"status": 0,
|
|
"headers": [:] as [String: String],
|
|
"body": ctx.newUint8Array(Data()),
|
|
"error": "Non-HTTP response",
|
|
]
|
|
}
|
|
var headerDict: [String: String] = [:]
|
|
for (k, v) in http.allHeaderFields {
|
|
guard let key = k as? String else { continue }
|
|
headerDict[key.lowercased()] = String(describing: v)
|
|
}
|
|
let payload = data ?? Data()
|
|
return [
|
|
"status": http.statusCode,
|
|
"headers": headerDict,
|
|
"body": ctx.newUint8Array(payload),
|
|
]
|
|
}
|
|
|
|
private static func rejectedPromise(in ctx: JSContext, error: String) -> Any? {
|
|
// Promise.resolve({ error }) per contract — async errors are returned,
|
|
// not thrown.
|
|
let script = "(function(msg){return Promise.resolve({status:0,headers:{},body:new Uint8Array(),error:msg});})"
|
|
return ctx.evaluateScript(script)?.call(withArguments: [error])
|
|
}
|
|
}
|