diff --git a/.gitignore b/.gitignore index 000dfed..e56b869 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ .build/ .swiftpm/ +.local/ +.public-dns/ DerivedData/ *.xcuserstate .DS_Store diff --git a/swift/Examples/IOSUploaderDemo/README.md b/Examples/IOSUploaderDemo/README.md similarity index 97% rename from swift/Examples/IOSUploaderDemo/README.md rename to Examples/IOSUploaderDemo/README.md index e52c1f5..69c0e1b 100644 --- a/swift/Examples/IOSUploaderDemo/README.md +++ b/Examples/IOSUploaderDemo/README.md @@ -19,9 +19,10 @@ 7. 支持列出 `listPendingUploads()` 返回的本地待恢复任务。 8. 支持调用 `resumeUpload(logicalUploadID:)`、`abortUpload(logicalUploadID:)`、`deleteLocalSnapshot(logicalUploadID:)`。 -建议模拟器 smoke 命令: +建议从 package 根目录执行模拟器 smoke 命令: ```bash +cd data-sdk export DGW_LOCAL_AUTH_ENDPOINT='http://127.0.0.1:15055' export DGW_LOCAL_GATEWAY_ENDPOINT='http://127.0.0.1:15053' export DGW_LOCAL_INIT_ENDPOINT='http://127.0.0.1:15057' diff --git a/swift/Package.resolved b/Package.resolved similarity index 100% rename from swift/Package.resolved rename to Package.resolved diff --git a/swift/Package.swift b/Package.swift similarity index 98% rename from swift/Package.swift rename to Package.swift index 144de15..6cfc5bf 100644 --- a/swift/Package.swift +++ b/Package.swift @@ -82,6 +82,9 @@ let package = Package( "DGWProto", "DGWStore", .product(name: "GRPCCore", package: "grpc-swift-2"), + ], + resources: [ + .process("Resources/PublicEndpoints.json"), ] ), .testTarget( diff --git a/swift/README.md b/README.md similarity index 93% rename from swift/README.md rename to README.md index a2fb9a7..1b7a8e5 100644 --- a/swift/README.md +++ b/README.md @@ -28,7 +28,7 @@ SDK 提供以下能力: | macOS 开发环境 | `>= 15` 推荐 | | 并发模型 | Swift Concurrency,主要 API 使用 `async throws` 和 `AsyncThrowingStream` | -SDK 固定使用 Archebase 公共 HTTPS 服务域名,App 不需要也不能在 public API 中传入认证、上传网关或设备初始化端点。 +SDK 使用资源文件定义 Archebase 公共服务端点,App 不需要也不能在 public API 中传入认证、上传网关或设备初始化端点。 ## 3. 接入前需要准备 @@ -56,13 +56,13 @@ DGWStore ### 4.2 Package.swift 接入 -本 repo 是多语言布局,Swift package 根目录是 `data-sdk/swift`。SwiftPM 的 Git 依赖要求 `Package.swift` 位于被依赖 repo 根目录;如果需要远端 URL 接入,请将 `swift/` 发布或镜像为独立 Swift package repo。 +本 repo 根目录就是标准 SwiftPM package 根目录,`Package.swift` 位于 `data-sdk/Package.swift`。宿主 App 可以直接通过 Git URL 或本地 path 依赖本 repo。 -独立 Swift package repo 远端包示例: +远端包示例: ```swift dependencies: [ - .package(url: "https://.git", from: "0.1.0") + .package(url: "https://github.com//data-sdk.git", from: "0.1.0") ] ``` @@ -70,20 +70,20 @@ dependencies: [ ```swift dependencies: [ - .package(path: "../data-sdk/swift") + .package(path: "../data-sdk") ] ``` -target 依赖示例。以下 `package: "swift"` 匹配上面的本地 path 示例;如果使用独立远端 repo,请替换为 SwiftPM 解析出的 package identity。 +target 依赖示例。`package` 参数使用 SwiftPM package identity;Git URL 和上面的本地 path 示例通常解析为 `data-sdk`。 ```swift targets: [ .target( name: "YourAppCore", dependencies: [ - .product(name: "DataGatewayClient", package: "swift"), - .product(name: "DGWControlPlane", package: "swift"), - .product(name: "DGWStore", package: "swift") + .product(name: "DataGatewayClient", package: "data-sdk"), + .product(name: "DGWControlPlane", package: "data-sdk"), + .product(name: "DGWStore", package: "data-sdk") ] ) ] @@ -158,19 +158,7 @@ print(deviceConfig.tags) 2. 本地已经存在配置文件时,抛出 `DataGatewayClientError.alreadyInitialized(configURL:)`。 3. 写入成功后返回 `ArchebaseConfig`,其中包含 `API Key` 和设备 tags。 -默认构建固定使用生产公共初始化端点: - -```text -https://init-device.platform.archebase.ai -``` - -如果编译 Swift target 时定义 `DEV`,初始化端点会切换为: - -```text -https://dev-init-device.platform.archebase.ai -``` - -App 不需要自行配置初始化端点。 +初始化端点来自 `DataGatewayClient` target 的必需资源文件 `PublicEndpoints.json` 中的 `deviceInit` 配置。App 不需要在运行时自行配置初始化端点。 ### 7.2 重新初始化 @@ -249,27 +237,45 @@ let config = DataGatewayClientConfig.recommended( let client = try DataGatewayClient(config: config) ``` -### 8.3 固定公共服务域名 +### 8.3 公共服务端点资源 -SDK 内置以下公共 HTTPS 服务域名。默认构建使用生产域名;如果编译 Swift target 时定义 `DEV`,SDK 会在三个 hostname 前增加 `dev-` 前缀。URL 中不显式指定端口,HTTPS 默认端口为 `443`。 +认证、上传网关和设备初始化端点全部来自 SwiftPM 资源文件: -| 服务 | 默认域名 | `-DDEV` 域名 | -|---|---|---| -| 认证 | `https://auth.platform.archebase.ai` | `https://dev-auth.platform.archebase.ai` | -| 上传网关 | `https://gateway.platform.archebase.ai` | `https://dev-gateway.platform.archebase.ai` | -| 设备初始化 | `https://init-device.platform.archebase.ai` | `https://dev-init-device.platform.archebase.ai` | +资源文件路径: -SwiftPM 命令行启用 dev 域名示例: +```text +Sources/DataGatewayClient/Resources/PublicEndpoints.json +``` -```bash -cd data-sdk/swift -swift build -Xswiftc -DDEV -swift test -Xswiftc -DDEV +`Package.swift` 会精确处理这个文件;如果文件不存在,`swift build` 或 `swift test` 会在构建阶段失败。资源文件存在但格式不合法时,SDK 在解析端点时会失败。 + +示例: + +```json +{ + "auth": { "scheme": "http", "host": "nlb-example.cn-shanghai.nlb.aliyuncsslb.com", "port": 50051 }, + "gateway": { "scheme": "http", "host": "nlb-example.cn-shanghai.nlb.aliyuncsslb.com", "port": 50053 }, + "deviceInit": { "scheme": "http", "host": "nlb-example.cn-shanghai.nlb.aliyuncsslb.com", "port": 50057 } +} ``` -Xcode 打开本地 Swift package 调试时,可在 package target 的 `Other Swift Flags` 中加入 `-DDEV`。 +字段说明: -公共 DNS 集成测试脚本默认准备生产域名。如果要测试 `-DDEV` 构建,请在运行 `swift/Scripts/public_dns_path_test.sh` 或 `swift/Scripts/simulator_smoke.sh` 时同时设置 `DGW_PUBLIC_DNS_DEV=1`。 +| 字段 | 说明 | +|---|---| +| `scheme` | 只允许 `http` 或 `https` | +| `host` | DNS hostname、IPv4 或 IPv6,不包含 scheme 和 port | +| `port` | `1...65535` 的整数 | + +`http` 会使用 plaintext gRPC 连接,`https` 会使用 TLS gRPC 连接。认证、上传网关和设备初始化可以分别指定不同的 `scheme`、`host` 和 `port`。 + +SwiftPM 命令行构建示例: + +```bash +cd data-sdk +swift build +swift test +``` App 只负责提供 `deviceID`、本地配置文件路径、上传持久化目录和凭证来源。 @@ -863,6 +869,7 @@ public enum PersistedUploadPhase: String, Codable, Sendable, Equatable { ### 18.5 Configuration Types +```swift public struct DataGatewayClientConfig: Sendable { public static func recommended( credentialBase64: String, @@ -948,7 +955,7 @@ public enum DataGatewayClientError: Error, Sendable, Equatable { ## 19. 上线前检查清单 -1. 确认 App 网络环境可以访问 SDK 内置的 Archebase 公共 HTTPS 域名。 +1. 确认 `Sources/DataGatewayClient/Resources/PublicEndpoints.json` 中的认证、上传网关和设备初始化端点正确,App 网络环境可以访问这些端点。 2. `archebase-config.json` 写入 App 私有目录,不进入日志、备份导出或共享容器。 3. App 支持首次初始化、已初始化跳过、重新初始化和初始化失败提示。 4. 上传 UI 支持进度、成功、失败、重试、恢复和取消。 diff --git a/swift/Scripts/bootstrap_swift_proto_toolchain.sh b/Scripts/bootstrap_swift_proto_toolchain.sh similarity index 84% rename from swift/Scripts/bootstrap_swift_proto_toolchain.sh rename to Scripts/bootstrap_swift_proto_toolchain.sh index 3e34942..89d5e16 100755 --- a/swift/Scripts/bootstrap_swift_proto_toolchain.sh +++ b/Scripts/bootstrap_swift_proto_toolchain.sh @@ -3,10 +3,9 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PACKAGE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -REPO_ROOT="$(cd "$PACKAGE_ROOT/.." && pwd)" -TOOL_BIN="$REPO_ROOT/.local/toolchains/swift-proto/bin" +TOOL_BIN="$PACKAGE_ROOT/.local/toolchains/swift-proto/bin" -mkdir -p "$REPO_ROOT/.local/src" "$TOOL_BIN" +mkdir -p "$PACKAGE_ROOT/.local/src" "$TOOL_BIN" swift build -c release --product protoc-gen-swift --package-path "$PACKAGE_ROOT/.build/checkouts/swift-protobuf" swift build -c release --product protoc-gen-grpc-swift-2 --package-path "$PACKAGE_ROOT/.build/checkouts/grpc-swift-protobuf" diff --git a/swift/Scripts/check_swift_proto_freshness.sh b/Scripts/check_swift_proto_freshness.sh similarity index 71% rename from swift/Scripts/check_swift_proto_freshness.sh rename to Scripts/check_swift_proto_freshness.sh index 7d7b684..c7eaea5 100755 --- a/swift/Scripts/check_swift_proto_freshness.sh +++ b/Scripts/check_swift_proto_freshness.sh @@ -7,7 +7,7 @@ PACKAGE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" "$SCRIPT_DIR/gen_swift_proto.sh" if ! git -C "$PACKAGE_ROOT" diff --exit-code -- Sources/DGWProto/Generated; then - echo "Swift proto generated sources are stale. Run swift/Scripts/gen_swift_proto.sh and commit the updated files." >&2 + echo "Swift proto generated sources are stale. Run Scripts/gen_swift_proto.sh and commit the updated files." >&2 exit 1 fi diff --git a/swift/Scripts/gen_swift_proto.sh b/Scripts/gen_swift_proto.sh similarity index 76% rename from swift/Scripts/gen_swift_proto.sh rename to Scripts/gen_swift_proto.sh index 1d887f6..8e7b167 100755 --- a/swift/Scripts/gen_swift_proto.sh +++ b/Scripts/gen_swift_proto.sh @@ -3,23 +3,22 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" PACKAGE_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" -REPO_ROOT="$(cd "$PACKAGE_ROOT/.." && pwd)" -PROTO_ROOT="$REPO_ROOT/protos" +PROTO_ROOT="$PACKAGE_ROOT/protos" OUT_DIR="$PACKAGE_ROOT/Sources/DGWProto/Generated" -TOOL_BIN="$REPO_ROOT/.local/toolchains/swift-proto/bin" +TOOL_BIN="$PACKAGE_ROOT/.local/toolchains/swift-proto/bin" SWIFT_PLUGIN="$TOOL_BIN/protoc-gen-swift" GRPC_PLUGIN="$TOOL_BIN/protoc-gen-grpc-swift-2" if [[ ! -x "$SWIFT_PLUGIN" ]]; then echo "missing executable: $SWIFT_PLUGIN" >&2 - echo "run swift/Scripts/bootstrap_swift_proto_toolchain.sh first" >&2 + echo "run Scripts/bootstrap_swift_proto_toolchain.sh first" >&2 exit 1 fi if [[ ! -x "$GRPC_PLUGIN" ]]; then echo "missing executable: $GRPC_PLUGIN" >&2 - echo "run swift/Scripts/bootstrap_swift_proto_toolchain.sh first" >&2 + echo "run Scripts/bootstrap_swift_proto_toolchain.sh first" >&2 exit 1 fi diff --git a/swift/Scripts/local_integration_bootstrap.sh b/Scripts/local_integration_bootstrap.sh similarity index 85% rename from swift/Scripts/local_integration_bootstrap.sh rename to Scripts/local_integration_bootstrap.sh index b920d60..bcd824c 100755 --- a/swift/Scripts/local_integration_bootstrap.sh +++ b/Scripts/local_integration_bootstrap.sh @@ -1,9 +1,10 @@ #!/usr/bin/env bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -PACKAGE_DIR="${ROOT_DIR}/swift" -DEFAULT_DATA_PLATFORM_ROOT="$(cd "${ROOT_DIR}/../data-platform" 2>/dev/null && pwd || true)" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +WORKSPACE_DIR="$(cd "${PACKAGE_DIR}/.." && pwd)" +DEFAULT_DATA_PLATFORM_ROOT="$(cd "${WORKSPACE_DIR}/data-platform" 2>/dev/null && pwd || true)" DATA_PLATFORM_ROOT="${DATA_PLATFORM_ROOT:-$DEFAULT_DATA_PLATFORM_ROOT}" DATA_PLATFORM_PROTO_ROOT="${DATA_PLATFORM_PROTO_ROOT:-${DATA_PLATFORM_ROOT}/common/proto}" @@ -35,7 +36,7 @@ CURL_MAX_TIME_SECONDS="${DGW_LOCAL_BOOTSTRAP_MAX_TIME_SECONDS:-10}" usage() { cat <<'EOF' -Usage: swift/Scripts/local_integration_bootstrap.sh [--start-stack] [--run-tests] [--print-env-only] +Usage: Scripts/local_integration_bootstrap.sh [--start-stack] [--run-tests] [--print-env-only] Options: --start-stack Build and deploy the local Rust stack before bootstrapping credentials. @@ -215,6 +216,67 @@ EOF printf '%s\n' "$token" } +bootstrap_devices_via_grpc() { + if ! command_exists grpcurl; then + echo "grpcurl is required when HTTP device routes are unavailable" >&2 + exit 1 + fi + + local credential_base64="$1" + local site_id="$2" + local admin_token device_body device_response device_name device_id + local unbound_body unbound_response unbound_name unbound_device_id + local suite_body suite_response suite_name add_device_body add_device_response + admin_token=$(admin_bearer_token) + + device_body=$(cat <&2 + exit 1 + fi + device_id="${device_name#devices/}" + + unbound_body=$(cat <&2 + exit 1 + fi + unbound_device_id="${unbound_name#devices/}" + + suite_body=$(cat <&2 + exit 1 + fi + + add_device_body=$(cat <&2 + exit 1 + fi + + emit_exports "$credential_base64" "$device_id" "$unbound_device_id" +} + bootstrap_via_grpc() { if ! command_exists grpcurl; then echo "grpcurl is required when HTTP admin gateway is unavailable" >&2 @@ -416,14 +478,19 @@ curl -fsS \ --max-time "$CURL_MAX_TIME_SECONDS" \ "${GATEWAY_HTTP_BASE%/}/healthz" >/dev/null -if ! curl -fsS -o /dev/null \ +LOGIN_ROUTE_STATUS=$(curl -sS -o /dev/null -w '%{http_code}' \ --connect-timeout "$CURL_CONNECT_TIMEOUT_SECONDS" \ --max-time "$CURL_MAX_TIME_SECONDS" \ - "${GATEWAY_HTTP_BASE%/}/api/dataplatform/v1/auth/login"; then - echo "HTTP admin gateway routes are unavailable at ${GATEWAY_HTTP_BASE}; falling back to grpc bootstrap" >&2 - bootstrap_via_grpc - exit 0 -fi + "${GATEWAY_HTTP_BASE%/}/api/dataplatform/v1/auth/login" || true) +case "$LOGIN_ROUTE_STATUS" in + 200|204|400|401|403|405) + ;; + *) + echo "HTTP admin gateway routes are unavailable at ${GATEWAY_HTTP_BASE}; falling back to grpc bootstrap" >&2 + bootstrap_via_grpc + exit 0 + ;; +esac LOGIN_BODY=$(cat <&2 + bootstrap_devices_via_grpc "$CREDENTIAL_BASE64" "$SITE_ID" + exit 0 + fi echo "failed to register device: $DEVICE_RESPONSE" >&2 exit 1 fi diff --git a/swift/Scripts/public_dns_path_test.sh b/Scripts/public_dns_path_test.sh similarity index 62% rename from swift/Scripts/public_dns_path_test.sh rename to Scripts/public_dns_path_test.sh index f0803bb..38efe7f 100755 --- a/swift/Scripts/public_dns_path_test.sh +++ b/Scripts/public_dns_path_test.sh @@ -1,24 +1,47 @@ #!/usr/bin/env bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -SDK_DIR="${ROOT_DIR}/swift" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +SDK_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +PUBLIC_ENDPOINTS_RESOURCE="${DGW_PUBLIC_ENDPOINTS_RESOURCE:-${SDK_DIR}/Sources/DataGatewayClient/Resources/PublicEndpoints.json}" MARKER_BEGIN="# archebase-swift-sdk-public-dns begin" MARKER_END="# archebase-swift-sdk-public-dns end" LOCAL_IP="${DGW_PUBLIC_DNS_LOCAL_IP:-127.0.0.1}" -DOMAIN_PREFIX="" -if [[ "${DGW_PUBLIC_DNS_DEV:-}" == "1" ]]; then - DOMAIN_PREFIX="dev-" -fi -AUTH_DOMAIN="${DOMAIN_PREFIX}auth.platform.archebase.ai" -GATEWAY_DOMAIN="${DOMAIN_PREFIX}gateway.platform.archebase.ai" -INIT_DOMAIN="${DOMAIN_PREFIX}init-device.platform.archebase.ai" + +read_public_endpoint_field() { + python3 - "$PUBLIC_ENDPOINTS_RESOURCE" "$1" "$2" <<'PY' +import json +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +service = sys.argv[2] +field = sys.argv[3] +payload = json.loads(path.read_text()) +endpoint = payload[service] +value = endpoint.get(field) +if field == "scheme" and value is None: + value = endpoint.get("schema") +if value is None: + raise SystemExit(f"missing {service}.{field} in {path}") +if field == "scheme": + value = str(value).lower() +print(value) +PY +} + +AUTH_SCHEME="" +GATEWAY_SCHEME="" +INIT_SCHEME="" +AUTH_DOMAIN="" +GATEWAY_DOMAIN="" +INIT_DOMAIN="" +AUTH_PORT="" +GATEWAY_PORT="" +INIT_PORT="" AUTH_TARGET="${DGW_LOCAL_AUTH_ENDPOINT:-127.0.0.1:15055}" GATEWAY_TARGET="${DGW_LOCAL_GATEWAY_ENDPOINT:-127.0.0.1:15053}" INIT_TARGET="${DGW_LOCAL_INIT_ENDPOINT:-127.0.0.1:15057}" -AUTH_TLS_PORT="${DGW_PUBLIC_AUTH_TLS_PORT:-443}" -GATEWAY_TLS_PORT="${DGW_PUBLIC_GATEWAY_TLS_PORT:-8443}" -INIT_TLS_PORT="${DGW_PUBLIC_INIT_TLS_PORT:-9443}" CERT_DIR="${DGW_PUBLIC_DNS_CERT_DIR:-${SDK_DIR}/.public-dns}" CERT_FILE="${CERT_DIR}/archebase-public-domains.crt" KEY_FILE="${CERT_DIR}/archebase-public-domains.key" @@ -26,22 +49,23 @@ PID_DIR="${CERT_DIR}/pids" usage() { cat <<'USAGE' -Usage: swift/Scripts/public_dns_path_test.sh +Usage: Scripts/public_dns_path_test.sh Commands: prepare-hosts Add marked /etc/hosts entries for Archebase public SDK domains. - start-proxies Start local TLS TCP proxies for auth, gateway, and device init gRPC targets. - run-tests Run gated Swift tests through the fixed public endpoint SDK path. + start-proxies Start local TCP proxies for auth, gateway, and device init gRPC targets. + run-tests Run gated Swift tests through the resource-defined public endpoint SDK path. cleanup Stop proxies and remove marked /etc/hosts entries. Environment: DGW_PUBLIC_DNS_RUN=1 is required for prepare-hosts, start-proxies, and run-tests. - DGW_PUBLIC_DNS_DEV=1 prepares dev-prefixed domains and runs Swift tests with -DDEV. + DGW_PUBLIC_ENDPOINTS_RESOURCE can point to an alternate PublicEndpoints.json. DGW_LOCAL_AUTH_ENDPOINT, DGW_LOCAL_GATEWAY_ENDPOINT, and DGW_LOCAL_INIT_ENDPOINT point to local plaintext gRPC targets. DGW_LOCAL_CREDENTIAL_BASE64, DGW_LOCAL_DEVICE_ID, and DGW_LOCAL_PERSIST_ROOT are passed through to integration tests. Notes: This script is intentionally gated and does not affect normal swift test runs. + Endpoint hosts and ports are read from PublicEndpoints.json. prepare-hosts may require sudo because it edits /etc/hosts. start-proxies requires openssl and socat. USAGE @@ -54,6 +78,18 @@ require_gated() { fi } +load_public_endpoints() { + AUTH_SCHEME="$(read_public_endpoint_field auth scheme)" + GATEWAY_SCHEME="$(read_public_endpoint_field gateway scheme)" + INIT_SCHEME="$(read_public_endpoint_field deviceInit scheme)" + AUTH_DOMAIN="$(read_public_endpoint_field auth host)" + GATEWAY_DOMAIN="$(read_public_endpoint_field gateway host)" + INIT_DOMAIN="$(read_public_endpoint_field deviceInit host)" + AUTH_PORT="$(read_public_endpoint_field auth port)" + GATEWAY_PORT="$(read_public_endpoint_field gateway port)" + INIT_PORT="$(read_public_endpoint_field deviceInit port)" +} + normalize_target() { local value="$1" value="${value#http://}" @@ -75,6 +111,7 @@ ensure_cert() { prepare_hosts() { require_gated + load_public_endpoints local block block="${MARKER_BEGIN} ${LOCAL_IP} ${AUTH_DOMAIN} @@ -129,27 +166,44 @@ PY start_proxy() { local name="$1" - local listen_port="$2" - local target="$3" + local scheme="$2" + local listen_port="$3" + local target="$4" local pid_file="${PID_DIR}/${name}.pid" if [[ -f "$pid_file" ]] && kill -0 "$(cat "$pid_file")" >/dev/null 2>&1; then echo "${name} proxy already running on ${listen_port}" return fi - socat "OPENSSL-LISTEN:${listen_port},cert=${CERT_FILE},key=${KEY_FILE},reuseaddr,fork" "TCP:$(normalize_target "$target")" & + case "$scheme" in + https) + socat "OPENSSL-LISTEN:${listen_port},cert=${CERT_FILE},key=${KEY_FILE},reuseaddr,fork" "TCP:$(normalize_target "$target")" & + ;; + http) + socat "TCP-LISTEN:${listen_port},reuseaddr,fork" "TCP:$(normalize_target "$target")" & + ;; + *) + echo "Unsupported scheme for ${name}: ${scheme}" >&2 + exit 2 + ;; + esac echo "$!" > "$pid_file" - echo "Started ${name} TLS proxy on ${listen_port} -> $(normalize_target "$target")" + echo "Started ${name} ${scheme} proxy on ${listen_port} -> $(normalize_target "$target")" } start_proxies() { require_gated - command -v openssl >/dev/null || { echo "openssl is required" >&2; exit 2; } + load_public_endpoints command -v socat >/dev/null || { echo "socat is required" >&2; exit 2; } - ensure_cert - start_proxy auth "$AUTH_TLS_PORT" "$AUTH_TARGET" - start_proxy gateway "$GATEWAY_TLS_PORT" "$GATEWAY_TARGET" - start_proxy init "$INIT_TLS_PORT" "$INIT_TARGET" - echo "Trust ${CERT_FILE} locally before running TLS validation against these proxies." + if [[ "$AUTH_SCHEME" == "https" || "$GATEWAY_SCHEME" == "https" || "$INIT_SCHEME" == "https" ]]; then + command -v openssl >/dev/null || { echo "openssl is required" >&2; exit 2; } + ensure_cert + fi + start_proxy auth "$AUTH_SCHEME" "$AUTH_PORT" "$AUTH_TARGET" + start_proxy gateway "$GATEWAY_SCHEME" "$GATEWAY_PORT" "$GATEWAY_TARGET" + start_proxy init "$INIT_SCHEME" "$INIT_PORT" "$INIT_TARGET" + if [[ "$AUTH_SCHEME" == "https" || "$GATEWAY_SCHEME" == "https" || "$INIT_SCHEME" == "https" ]]; then + echo "Trust ${CERT_FILE} locally before running TLS validation against these proxies." + fi } stop_proxies() { @@ -178,11 +232,7 @@ run_tests() { export DGW_OSS_TEST_ACCESS_KEY_SECRET="${DGW_OSS_TEST_ACCESS_KEY_SECRET:-placeholder}" export DGW_OSS_TEST_SECURITY_TOKEN="${DGW_OSS_TEST_SECURITY_TOKEN:-placeholder}" export DGW_OSS_TEST_OBJECT_PREFIX="${DGW_OSS_TEST_OBJECT_PREFIX:-swift-public-dns}" - if [[ "${DGW_PUBLIC_DNS_DEV:-}" == "1" ]]; then - (cd "$SDK_DIR" && swift test -Xswiftc -DDEV --filter LocalStackHarnessTests) - else - (cd "$SDK_DIR" && swift test --filter LocalStackHarnessTests) - fi + (cd "$SDK_DIR" && swift test --filter LocalStackHarnessTests) } case "${1:-}" in diff --git a/swift/Scripts/simulator_smoke.sh b/Scripts/simulator_smoke.sh similarity index 91% rename from swift/Scripts/simulator_smoke.sh rename to Scripts/simulator_smoke.sh index 05d403c..4f8f6b3 100644 --- a/swift/Scripts/simulator_smoke.sh +++ b/Scripts/simulator_smoke.sh @@ -1,8 +1,9 @@ #!/usr/bin/env bash set -euo pipefail -ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" -PACKAGE_DIR="${ROOT_DIR}/swift" +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PACKAGE_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)" +PUBLIC_ENDPOINTS_RESOURCE="${DGW_PUBLIC_ENDPOINTS_RESOURCE:-${PACKAGE_DIR}/Sources/DataGatewayClient/Resources/PublicEndpoints.json}" SCHEME="${DGW_IOS_SMOKE_SCHEME:-SwiftDataGatewayClient-Package}" DESTINATION="${DGW_IOS_SMOKE_DESTINATION:-platform=iOS Simulator,name=iPhone 17}" DESTINATION_TIMEOUT_SECONDS="${DGW_IOS_SMOKE_DESTINATION_TIMEOUT_SECONDS:-30}" @@ -10,13 +11,33 @@ CHECK_COMMAND_TIMEOUT_SECONDS="${DGW_IOS_SMOKE_CHECK_COMMAND_TIMEOUT_SECONDS:-60 DEFAULT_TEST_TIMEOUT_SECONDS="${DGW_IOS_SMOKE_DEFAULT_TEST_TIMEOUT_SECONDS:-120}" MAX_TEST_TIMEOUT_SECONDS="${DGW_IOS_SMOKE_MAX_TEST_TIMEOUT_SECONDS:-300}" OTHER_SWIFT_FLAGS_VALUE="${DGW_IOS_SMOKE_OTHER_SWIFT_FLAGS:-}" -if [[ "${DGW_PUBLIC_DNS_DEV:-}" == "1" ]]; then - OTHER_SWIFT_FLAGS_VALUE="${OTHER_SWIFT_FLAGS_VALUE:+${OTHER_SWIFT_FLAGS_VALUE} }-DDEV" -fi XCODEBUILD_BUILD_SETTINGS=() if [[ -n "$OTHER_SWIFT_FLAGS_VALUE" ]]; then XCODEBUILD_BUILD_SETTINGS+=(OTHER_SWIFT_FLAGS="$OTHER_SWIFT_FLAGS_VALUE") fi + +read_public_endpoint_field() { + python3 - "$PUBLIC_ENDPOINTS_RESOURCE" "$1" "$2" <<'PY' +import json +import pathlib +import sys + +path = pathlib.Path(sys.argv[1]) +service = sys.argv[2] +field = sys.argv[3] +payload = json.loads(path.read_text()) +endpoint = payload[service] +value = endpoint.get(field) +if field == "scheme" and value is None: + value = endpoint.get("schema") +if value is None: + raise SystemExit(f"missing {service}.{field} in {path}") +if field == "scheme": + value = str(value).lower() +print(value) +PY +} + if [[ "${DGW_IOS_SMOKE_PUBLIC_PATH:-}" == "1" ]]; then SMOKE_TEST_ONE="${DGW_IOS_SMOKE_TEST_ONE:-DataGatewayClientIntegrationTests/LocalStackHarnessTests/publicPathCanExchangeForBearerToken()}" SMOKE_TEST_TWO="${DGW_IOS_SMOKE_TEST_TWO:-DataGatewayClientIntegrationTests/LocalStackHarnessTests/publicPathRuntimeBootstrapAndControlPlaneFlow()}" @@ -30,7 +51,7 @@ DERIVED_DATA_PATH="${DGW_IOS_SMOKE_DERIVED_DATA_PATH:-$(mktemp -d /tmp/swift-dgw usage() { cat <<'EOF' -Usage: swift/Scripts/simulator_smoke.sh [--check-only] [--list-destinations] +Usage: Scripts/simulator_smoke.sh [--check-only] [--list-destinations] Options: --check-only Validate the package scheme and simulator SDK prerequisites without running tests. @@ -48,9 +69,9 @@ Environment overrides: DGW_IOS_SMOKE_TEST_TWO DGW_IOS_SMOKE_TEST_THREE DGW_IOS_SMOKE_DERIVED_DATA_PATH - DGW_IOS_SMOKE_PUBLIC_PATH=1 (use fixed public endpoint tests) + DGW_IOS_SMOKE_PUBLIC_PATH=1 (use resource-defined public endpoint tests) DGW_IOS_SMOKE_OTHER_SWIFT_FLAGS (extra xcodebuild OTHER_SWIFT_FLAGS) - DGW_PUBLIC_DNS_DEV=1 (use dev-prefixed public domains and compile with -DDEV) + DGW_PUBLIC_ENDPOINTS_RESOURCE (alternate PublicEndpoints.json for public path readiness checks) Required environment for real smoke execution: DGW_LOCAL_AUTH_ENDPOINT (local mode only) @@ -63,8 +84,7 @@ Required environment for real smoke execution: Notes: - The script runs Swift package tests on the `SwiftDataGatewayClient-Package` scheme. - Local mode uses `build-for-testing` + patched `.xctestrun` so simulator-hosted tests receive `DGW_LOCAL_*` environment variables. - - Public path mode does not inject auth/gateway/init endpoints. Prepare hosts and local TLS trust with `public_dns_path_test.sh` first. - - For DEV public path mode, set DGW_PUBLIC_DNS_DEV=1 for both this script and public_dns_path_test.sh. + - Public path mode does not inject auth/gateway/init endpoints. Prepare hosts and local TLS trust with `Scripts/public_dns_path_test.sh` first. - Swift Testing method filters must include `()` in the final test identifier. - xcodebuild test timeouts are enabled so hangs surface as bounded failures instead of endless runs. EOF @@ -316,11 +336,8 @@ PY } require_public_path_ready() { - local domain_prefix="" - if [[ "${DGW_PUBLIC_DNS_DEV:-}" == "1" ]]; then - domain_prefix="dev-" - fi - for domain in "${domain_prefix}auth.platform.archebase.ai" "${domain_prefix}gateway.platform.archebase.ai" "${domain_prefix}init-device.platform.archebase.ai"; do + local domain + for domain in "$(read_public_endpoint_field auth host)" "$(read_public_endpoint_field gateway host)" "$(read_public_endpoint_field deviceInit host)"; do if ! grep -q "${domain}" /etc/hosts; then echo "${domain} is not mapped in /etc/hosts. Run public_dns_path_test.sh prepare-hosts first." >&2 exit 1 diff --git a/swift/Sources/DGWAuth/CredentialAuthProvider.swift b/Sources/DGWAuth/CredentialAuthProvider.swift similarity index 100% rename from swift/Sources/DGWAuth/CredentialAuthProvider.swift rename to Sources/DGWAuth/CredentialAuthProvider.swift diff --git a/swift/Sources/DGWControlPlane/ControlPlaneTransport.swift b/Sources/DGWControlPlane/ControlPlaneTransport.swift similarity index 100% rename from swift/Sources/DGWControlPlane/ControlPlaneTransport.swift rename to Sources/DGWControlPlane/ControlPlaneTransport.swift diff --git a/swift/Sources/DGWControlPlane/RetryExecutor.swift b/Sources/DGWControlPlane/RetryExecutor.swift similarity index 100% rename from swift/Sources/DGWControlPlane/RetryExecutor.swift rename to Sources/DGWControlPlane/RetryExecutor.swift diff --git a/swift/Sources/DGWCore/MultipartETagBuilder.swift b/Sources/DGWCore/MultipartETagBuilder.swift similarity index 100% rename from swift/Sources/DGWCore/MultipartETagBuilder.swift rename to Sources/DGWCore/MultipartETagBuilder.swift diff --git a/swift/Sources/DGWOss/OssMultipartClient.swift b/Sources/DGWOss/OssMultipartClient.swift similarity index 100% rename from swift/Sources/DGWOss/OssMultipartClient.swift rename to Sources/DGWOss/OssMultipartClient.swift diff --git a/swift/Sources/DGWProto/Generated/auth.grpc.swift b/Sources/DGWProto/Generated/auth.grpc.swift similarity index 100% rename from swift/Sources/DGWProto/Generated/auth.grpc.swift rename to Sources/DGWProto/Generated/auth.grpc.swift diff --git a/swift/Sources/DGWProto/Generated/auth.pb.swift b/Sources/DGWProto/Generated/auth.pb.swift similarity index 100% rename from swift/Sources/DGWProto/Generated/auth.pb.swift rename to Sources/DGWProto/Generated/auth.pb.swift diff --git a/swift/Sources/DGWProto/Generated/common.grpc.swift b/Sources/DGWProto/Generated/common.grpc.swift similarity index 100% rename from swift/Sources/DGWProto/Generated/common.grpc.swift rename to Sources/DGWProto/Generated/common.grpc.swift diff --git a/swift/Sources/DGWProto/Generated/common.pb.swift b/Sources/DGWProto/Generated/common.pb.swift similarity index 100% rename from swift/Sources/DGWProto/Generated/common.pb.swift rename to Sources/DGWProto/Generated/common.pb.swift diff --git a/swift/Sources/DGWProto/Generated/data_gateway.grpc.swift b/Sources/DGWProto/Generated/data_gateway.grpc.swift similarity index 100% rename from swift/Sources/DGWProto/Generated/data_gateway.grpc.swift rename to Sources/DGWProto/Generated/data_gateway.grpc.swift diff --git a/swift/Sources/DGWProto/Generated/data_gateway.pb.swift b/Sources/DGWProto/Generated/data_gateway.pb.swift similarity index 100% rename from swift/Sources/DGWProto/Generated/data_gateway.pb.swift rename to Sources/DGWProto/Generated/data_gateway.pb.swift diff --git a/swift/Sources/DGWProto/Placeholder.swift b/Sources/DGWProto/Placeholder.swift similarity index 100% rename from swift/Sources/DGWProto/Placeholder.swift rename to Sources/DGWProto/Placeholder.swift diff --git a/swift/Sources/DGWStore/ArchebaseConfig.swift b/Sources/DGWStore/ArchebaseConfig.swift similarity index 100% rename from swift/Sources/DGWStore/ArchebaseConfig.swift rename to Sources/DGWStore/ArchebaseConfig.swift diff --git a/swift/Sources/DGWStore/ArchebaseConfigStore.swift b/Sources/DGWStore/ArchebaseConfigStore.swift similarity index 100% rename from swift/Sources/DGWStore/ArchebaseConfigStore.swift rename to Sources/DGWStore/ArchebaseConfigStore.swift diff --git a/swift/Sources/DGWStore/PersistedUploadState.swift b/Sources/DGWStore/PersistedUploadState.swift similarity index 100% rename from swift/Sources/DGWStore/PersistedUploadState.swift rename to Sources/DGWStore/PersistedUploadState.swift diff --git a/swift/Sources/DGWStore/UploadStateStore.swift b/Sources/DGWStore/UploadStateStore.swift similarity index 97% rename from swift/Sources/DGWStore/UploadStateStore.swift rename to Sources/DGWStore/UploadStateStore.swift index 2bb8866..64e06cc 100644 --- a/swift/Sources/DGWStore/UploadStateStore.swift +++ b/Sources/DGWStore/UploadStateStore.swift @@ -295,7 +295,12 @@ public actor UploadStateStore { try self.fileManager.createDirectory(at: directory, withIntermediateDirectories: true) let temporaryURL = directory.appendingPathComponent("\(destination.lastPathComponent).tmp-\(UUID().uuidString)") - try data.write(to: temporaryURL, options: .completeFileProtectionUnlessOpen) + #if os(iOS) || os(tvOS) || os(watchOS) || os(visionOS) + let writeOptions: Data.WritingOptions = .completeFileProtectionUnlessOpen + #else + let writeOptions: Data.WritingOptions = [] + #endif + try data.write(to: temporaryURL, options: writeOptions) if self.fileManager.fileExists(atPath: destination.path()) { _ = try self.fileManager.replaceItemAt(destination, withItemAt: temporaryURL) diff --git a/swift/Sources/DataGatewayClient/FilePreparation.swift b/Sources/DataGatewayClient/FilePreparation.swift similarity index 93% rename from swift/Sources/DataGatewayClient/FilePreparation.swift rename to Sources/DataGatewayClient/FilePreparation.swift index bb8545a..8e755a0 100644 --- a/swift/Sources/DataGatewayClient/FilePreparation.swift +++ b/Sources/DataGatewayClient/FilePreparation.swift @@ -16,22 +16,117 @@ public enum DataGatewayClientModule { public static let version = "0.1.0" } -/// Fixed Archebase public service endpoints used by the SDK. +/// Archebase public service endpoints loaded from the required SDK resource. public enum ArchebasePublicEndpoints { - #if DEV - private static let hostPrefix = "dev-" - #else - private static let hostPrefix = "" - #endif + package struct Resolved: Sendable, Equatable { + package var auth: URL + package var gateway: URL + package var deviceInit: URL + package var authTLS: TLSMode + package var gatewayTLS: TLSMode + package var deviceInitTLS: TLSMode + } /// Public authentication service endpoint. - public static let auth = URL(string: "https://\(hostPrefix)auth.platform.archebase.ai")! + public static var auth: URL { resolved.auth } /// Public data gateway control-plane service endpoint. - public static let gateway = URL(string: "https://\(hostPrefix)gateway.platform.archebase.ai")! + public static var gateway: URL { resolved.gateway } /// Public device initialization service endpoint. - public static let deviceInit = URL(string: "https://\(hostPrefix)init-device.platform.archebase.ai")! + public static var deviceInit: URL { resolved.deviceInit } + + package static var authTLS: TLSMode { resolved.authTLS } + package static var gatewayTLS: TLSMode { resolved.gatewayTLS } + package static var deviceInitTLS: TLSMode { resolved.deviceInitTLS } + + package static let resourceName = "PublicEndpoints" + + private static let resolved = loadResource() + + private static func loadResource() -> Resolved { + guard let url = Bundle.module.url(forResource: resourceName, withExtension: "json") else { + fatalError("missing \(resourceName).json resource") + } + do { + let data = try Data(contentsOf: url) + return try decodeResource(data) + } catch { + fatalError("invalid \(resourceName).json: \(error)") + } + } + + package static func decodeResource(_ data: Data) throws -> Resolved { + let payload = try JSONDecoder().decode(ResourcePayload.self, from: data) + let auth = try payload.auth.resolvedURL(fieldName: "auth") + let gateway = try payload.gateway.resolvedURL(fieldName: "gateway") + let deviceInit = try payload.deviceInit.resolvedURL(fieldName: "deviceInit") + return Resolved( + auth: auth.url, + gateway: gateway.url, + deviceInit: deviceInit.url, + authTLS: auth.tls, + gatewayTLS: gateway.tls, + deviceInitTLS: deviceInit.tls + ) + } + + private struct ResourcePayload: Decodable { + var auth: ResourceEndpoint + var gateway: ResourceEndpoint + var deviceInit: ResourceEndpoint + } + + private struct ResourceEndpoint: Decodable { + var scheme: String + var host: String + var port: Int + + enum CodingKeys: String, CodingKey { + case scheme + case schema + case host + case port + } + + init(from decoder: Decoder) throws { + let container = try decoder.container(keyedBy: CodingKeys.self) + self.scheme = try container.decodeIfPresent(String.self, forKey: .scheme) + ?? container.decode(String.self, forKey: .schema) + self.host = try container.decode(String.self, forKey: .host) + self.port = try container.decode(Int.self, forKey: .port) + } + + func resolvedURL(fieldName: String) throws -> (url: URL, tls: TLSMode) { + let normalizedScheme = self.scheme.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() + let tls: TLSMode + switch normalizedScheme { + case "http": + tls = .plaintext + case "https": + tls = .tls + default: + throw DataGatewayClientError.invalidConfiguration("\(fieldName).scheme must be http or https") + } + + let normalizedHost = self.host.trimmingCharacters(in: .whitespacesAndNewlines) + guard !normalizedHost.isEmpty else { + throw DataGatewayClientError.invalidConfiguration("\(fieldName).host must not be empty") + } + guard (1 ... 65535).contains(self.port) else { + throw DataGatewayClientError.invalidConfiguration("\(fieldName).port must be between 1 and 65535") + } + + var components = URLComponents() + components.scheme = normalizedScheme + components.host = normalizedHost + components.port = self.port + guard let url = components.url else { + throw DataGatewayClientError.invalidConfiguration("\(fieldName) endpoint is not a valid URL") + } + return (url, tls) + } + } } /// Public configuration for device initialization and reinitialization. @@ -40,14 +135,14 @@ public struct DeviceInitClientConfig: Sendable { public var requestTimeout: Duration package var tls: TLSMode - /// Creates a device initialization configuration that uses the fixed public TLS endpoint. + /// Creates a device initialization configuration that uses the resource-defined public endpoint. public init( configURL: URL, requestTimeout: Duration = .seconds(10) ) { self.configURL = configURL self.requestTimeout = requestTimeout - self.tls = .tls + self.tls = ArchebasePublicEndpoints.deviceInitTLS } package init( @@ -265,10 +360,12 @@ public struct DataGatewayClientConfig: Sendable { public var persistRootURL: URL public var retryPolicy: RetryPolicySet public var execution: UploadExecutionPolicy - package var tls: TLSMode + package var authTLS: TLSMode + package var gatewayTLS: TLSMode + package var tls: TLSMode { self.authTLS } public var observability: DataGatewayClientObservability - /// Creates a client configuration that uses the fixed public TLS endpoints. + /// Creates a client configuration that uses the resource-defined public endpoints. public init( credentialBase64: String, authRefreshBefore: Duration, @@ -286,7 +383,8 @@ public struct DataGatewayClientConfig: Sendable { self.persistRootURL = persistRootURL self.retryPolicy = retryPolicy self.execution = execution - self.tls = .tls + self.authTLS = ArchebasePublicEndpoints.authTLS + self.gatewayTLS = ArchebasePublicEndpoints.gatewayTLS self.observability = observability } @@ -310,11 +408,12 @@ public struct DataGatewayClientConfig: Sendable { self.persistRootURL = persistRootURL self.retryPolicy = retryPolicy self.execution = execution - self.tls = tls + self.authTLS = tls + self.gatewayTLS = tls self.observability = observability } - /// Recommended defaults for production-safe behavior with fixed public endpoints. + /// Recommended defaults for resource-defined public endpoints. public static func recommended( credentialBase64: String, persistRootURL: URL, @@ -355,8 +454,8 @@ public struct DataGatewayClientConfig: Sendable { /// Validates endpoint, TLS, and local persistence constraints before client construction. public func validate() throws { - try Self.validate(endpoint: self.authEndpoint, tls: self.tls, fieldName: "authEndpoint") - try Self.validate(endpoint: self.gatewayEndpoint, tls: self.tls, fieldName: "gatewayEndpoint") + try Self.validate(endpoint: self.authEndpoint, tls: self.authTLS, fieldName: "authEndpoint") + try Self.validate(endpoint: self.gatewayEndpoint, tls: self.gatewayTLS, fieldName: "gatewayEndpoint") if self.credentialBase64.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { throw DataGatewayClientError.invalidConfiguration("credential_base64 must not be empty") @@ -433,7 +532,7 @@ public actor ArchebaseDeviceInitializer { private let sdkVersion: String private let platform: String - /// Creates an initializer that always targets the fixed public initialization endpoint. + /// Creates an initializer that targets the resource-defined public initialization endpoint. public init(config: DeviceInitClientConfig) throws { try self.init( config: config, @@ -1603,7 +1702,11 @@ public actor DataGatewayClient { try config.validate() try ArchebaseConfig.validateTags(configTags) - let security: ControlPlaneTransportSecurity = switch config.tls { + let authSecurity: ControlPlaneTransportSecurity = switch config.authTLS { + case .plaintext: .plaintext + case .tls: .tls + } + let gatewaySecurity: ControlPlaneTransportSecurity = switch config.gatewayTLS { case .plaintext: .plaintext case .tls: .tls } @@ -1611,7 +1714,7 @@ public actor DataGatewayClient { let authFactory = ControlPlaneClientFactory( configuration: ControlPlaneTransportConfiguration( endpoint: config.authEndpoint, - security: security, + security: authSecurity, requestTimeout: config.requestTimeout ) ) @@ -1625,7 +1728,7 @@ public actor DataGatewayClient { let gatewayTransport = try ManagedControlPlaneServiceClient(configuration: ControlPlaneTransportConfiguration( endpoint: config.gatewayEndpoint, - security: security, + security: gatewaySecurity, requestTimeout: config.requestTimeout )) { grpcClient in Archebase_DataGateway_V1_DataGatewayService.Client(wrapping: grpcClient) diff --git a/Sources/DataGatewayClient/Resources/PublicEndpoints.json b/Sources/DataGatewayClient/Resources/PublicEndpoints.json new file mode 100644 index 0000000..ce18973 --- /dev/null +++ b/Sources/DataGatewayClient/Resources/PublicEndpoints.json @@ -0,0 +1,5 @@ +{ + "auth": { "scheme": "http", "host": "nlb-isnnehtfmxqv70lvm9.cn-shanghai.nlb.aliyuncsslb.com", "port": 50051 }, + "gateway": { "scheme": "http", "host": "nlb-isnnehtfmxqv70lvm9.cn-shanghai.nlb.aliyuncsslb.com", "port": 50053 }, + "deviceInit": { "scheme": "http", "host": "nlb-isnnehtfmxqv70lvm9.cn-shanghai.nlb.aliyuncsslb.com", "port": 50057 } +} diff --git a/swift/Sources/DataGatewayClient/TestHarnessSupport.swift b/Sources/DataGatewayClient/TestHarnessSupport.swift similarity index 100% rename from swift/Sources/DataGatewayClient/TestHarnessSupport.swift rename to Sources/DataGatewayClient/TestHarnessSupport.swift diff --git a/swift/Tests/DGWAuthTests/CredentialAuthProviderTests.swift b/Tests/DGWAuthTests/CredentialAuthProviderTests.swift similarity index 100% rename from swift/Tests/DGWAuthTests/CredentialAuthProviderTests.swift rename to Tests/DGWAuthTests/CredentialAuthProviderTests.swift diff --git a/swift/Tests/DGWControlPlaneTests/ControlPlaneTransportTests.swift b/Tests/DGWControlPlaneTests/ControlPlaneTransportTests.swift similarity index 100% rename from swift/Tests/DGWControlPlaneTests/ControlPlaneTransportTests.swift rename to Tests/DGWControlPlaneTests/ControlPlaneTransportTests.swift diff --git a/swift/Tests/DGWControlPlaneTests/RetryExecutorTests.swift b/Tests/DGWControlPlaneTests/RetryExecutorTests.swift similarity index 100% rename from swift/Tests/DGWControlPlaneTests/RetryExecutorTests.swift rename to Tests/DGWControlPlaneTests/RetryExecutorTests.swift diff --git a/swift/Tests/DGWCoreTests/MultipartETagBuilderTests.swift b/Tests/DGWCoreTests/MultipartETagBuilderTests.swift similarity index 100% rename from swift/Tests/DGWCoreTests/MultipartETagBuilderTests.swift rename to Tests/DGWCoreTests/MultipartETagBuilderTests.swift diff --git a/swift/Tests/DGWOssTests/OssMultipartClientTests.swift b/Tests/DGWOssTests/OssMultipartClientTests.swift similarity index 100% rename from swift/Tests/DGWOssTests/OssMultipartClientTests.swift rename to Tests/DGWOssTests/OssMultipartClientTests.swift diff --git a/swift/Tests/DGWProtoTests/PlaceholderTests.swift b/Tests/DGWProtoTests/PlaceholderTests.swift similarity index 100% rename from swift/Tests/DGWProtoTests/PlaceholderTests.swift rename to Tests/DGWProtoTests/PlaceholderTests.swift diff --git a/swift/Tests/DGWStoreTests/ArchebaseConfigStoreTests.swift b/Tests/DGWStoreTests/ArchebaseConfigStoreTests.swift similarity index 100% rename from swift/Tests/DGWStoreTests/ArchebaseConfigStoreTests.swift rename to Tests/DGWStoreTests/ArchebaseConfigStoreTests.swift diff --git a/swift/Tests/DGWStoreTests/ArchebaseConfigTests.swift b/Tests/DGWStoreTests/ArchebaseConfigTests.swift similarity index 100% rename from swift/Tests/DGWStoreTests/ArchebaseConfigTests.swift rename to Tests/DGWStoreTests/ArchebaseConfigTests.swift diff --git a/swift/Tests/DGWStoreTests/PersistedUploadStateTests.swift b/Tests/DGWStoreTests/PersistedUploadStateTests.swift similarity index 100% rename from swift/Tests/DGWStoreTests/PersistedUploadStateTests.swift rename to Tests/DGWStoreTests/PersistedUploadStateTests.swift diff --git a/swift/Tests/DGWStoreTests/UploadStateStoreTests.swift b/Tests/DGWStoreTests/UploadStateStoreTests.swift similarity index 100% rename from swift/Tests/DGWStoreTests/UploadStateStoreTests.swift rename to Tests/DGWStoreTests/UploadStateStoreTests.swift diff --git a/swift/Tests/DataGatewayClientIntegrationTests/ArchebaseConfigClientTests.swift b/Tests/DataGatewayClientIntegrationTests/ArchebaseConfigClientTests.swift similarity index 100% rename from swift/Tests/DataGatewayClientIntegrationTests/ArchebaseConfigClientTests.swift rename to Tests/DataGatewayClientIntegrationTests/ArchebaseConfigClientTests.swift diff --git a/swift/Tests/DataGatewayClientIntegrationTests/DeviceInitializerTests.swift b/Tests/DataGatewayClientIntegrationTests/DeviceInitializerTests.swift similarity index 100% rename from swift/Tests/DataGatewayClientIntegrationTests/DeviceInitializerTests.swift rename to Tests/DataGatewayClientIntegrationTests/DeviceInitializerTests.swift diff --git a/swift/Tests/DataGatewayClientIntegrationTests/FilePreparationTests.swift b/Tests/DataGatewayClientIntegrationTests/FilePreparationTests.swift similarity index 79% rename from swift/Tests/DataGatewayClientIntegrationTests/FilePreparationTests.swift rename to Tests/DataGatewayClientIntegrationTests/FilePreparationTests.swift index d5ee3af..2332957 100644 --- a/swift/Tests/DataGatewayClientIntegrationTests/FilePreparationTests.swift +++ b/Tests/DataGatewayClientIntegrationTests/FilePreparationTests.swift @@ -96,25 +96,65 @@ func makePersistencePolicy(copyExternalFileIntoManagedStaging: Bool) -> LocalPer #expect(DataGatewayClientModule.name == "DataGatewayClient") } -@Test func publicEndpointsMatchHardcodedContract() { - #if DEV - let hostPrefix = "dev-" - #else - let hostPrefix = "" - #endif - - #expect(ArchebasePublicEndpoints.auth == URL(string: "https://\(hostPrefix)auth.platform.archebase.ai")!) - #expect(ArchebasePublicEndpoints.gateway == URL(string: "https://\(hostPrefix)gateway.platform.archebase.ai")!) - #expect(ArchebasePublicEndpoints.deviceInit == URL(string: "https://\(hostPrefix)init-device.platform.archebase.ai")!) +@Test func publicEndpointsLoadRequiredResourceContract() { + for endpoint in [ArchebasePublicEndpoints.auth, ArchebasePublicEndpoints.gateway, ArchebasePublicEndpoints.deviceInit] { + #expect(endpoint.scheme == "http" || endpoint.scheme == "https") + #expect(endpoint.host?.isEmpty == false) + #expect(endpoint.port != nil) + } +} + +@Test func publicEndpointResourceParsesHttpAndHttpsValues() throws { + let payload = Data(""" + { + "auth": { "schema": "http", "host": "nlb.example.com", "port": 50051 }, + "gateway": { "scheme": "http", "host": "nlb.example.com", "port": 50053 }, + "deviceInit": { "scheme": "https", "host": "init.example.com", "port": 443 } + } + """.utf8) + + let endpoints = try ArchebasePublicEndpoints.decodeResource(payload) + + #expect(endpoints.auth == URL(string: "http://nlb.example.com:50051")!) + #expect(endpoints.gateway == URL(string: "http://nlb.example.com:50053")!) + #expect(endpoints.deviceInit == URL(string: "https://init.example.com:443")!) + #expect(endpoints.authTLS == .plaintext) + #expect(endpoints.gatewayTLS == .plaintext) + #expect(endpoints.deviceInitTLS == .tls) +} + +@Test func publicEndpointResourceRejectsInvalidSchemeAndPort() { + let invalidScheme = Data(""" + { + "auth": { "scheme": "grpc", "host": "auth.example.com", "port": 50051 }, + "gateway": { "scheme": "http", "host": "gateway.example.com", "port": 50053 }, + "deviceInit": { "scheme": "http", "host": "init.example.com", "port": 50057 } + } + """.utf8) + let invalidPort = Data(""" + { + "auth": { "scheme": "http", "host": "auth.example.com", "port": 0 }, + "gateway": { "scheme": "http", "host": "gateway.example.com", "port": 50053 }, + "deviceInit": { "scheme": "http", "host": "init.example.com", "port": 50057 } + } + """.utf8) + + #expect(throws: DataGatewayClientError.self) { + try ArchebasePublicEndpoints.decodeResource(invalidScheme) + } + #expect(throws: DataGatewayClientError.self) { + try ArchebasePublicEndpoints.decodeResource(invalidPort) + } } -@Test func publicClientConfigUsesFixedTlsEndpoints() throws { +@Test func publicClientConfigUsesResourceEndpointsAndDerivedTls() throws { let root = URL(fileURLWithPath: "/tmp/archebase-public-config", isDirectory: true) let config = DataGatewayClientConfig.recommended(credentialBase64: "credential-base64", persistRootURL: root) #expect(config.authEndpoint == ArchebasePublicEndpoints.auth) #expect(config.gatewayEndpoint == ArchebasePublicEndpoints.gateway) - #expect(config.tls == .tls) + #expect(config.authTLS == ArchebasePublicEndpoints.authTLS) + #expect(config.gatewayTLS == ArchebasePublicEndpoints.gatewayTLS) #expect(config.credentialBase64 == "credential-base64") #expect(config.persistRootURL == root) #expect(throws: Never.self) { try config.validate() } @@ -125,7 +165,7 @@ func makePersistencePolicy(copyExternalFileIntoManagedStaging: Bool) -> LocalPer let config = DeviceInitClientConfig(configURL: configURL) #expect(config.configURL == configURL) - #expect(config.tls == .tls) + #expect(config.tls == ArchebasePublicEndpoints.deviceInitTLS) } private extension Dictionary { diff --git a/swift/Tests/DataGatewayClientIntegrationTests/LocalStackHarnessTests.swift b/Tests/DataGatewayClientIntegrationTests/LocalStackHarnessTests.swift similarity index 100% rename from swift/Tests/DataGatewayClientIntegrationTests/LocalStackHarnessTests.swift rename to Tests/DataGatewayClientIntegrationTests/LocalStackHarnessTests.swift diff --git a/swift/Tests/DataGatewayClientIntegrationTests/UploadCoordinatorTests.swift b/Tests/DataGatewayClientIntegrationTests/UploadCoordinatorTests.swift similarity index 100% rename from swift/Tests/DataGatewayClientIntegrationTests/UploadCoordinatorTests.swift rename to Tests/DataGatewayClientIntegrationTests/UploadCoordinatorTests.swift