diff --git a/services/api7__enterprise_v3-10-2/README.md b/services/api7__enterprise_v3-10-2/README.md new file mode 100644 index 00000000..cacf3e9e --- /dev/null +++ b/services/api7__enterprise_v3-10-2/README.md @@ -0,0 +1,132 @@ +# API7 Enterprise 3.10.2 + +OctoBus service package for API7 Enterprise Admin APIs with a phase-1 focus on security operations, API gateway governance, and read-oriented operational visibility. + +Service name: `api7-enterprise-v3-10-2`. + +Runtime: long-running Node.js service using `@chaitin-ai/octobus-sdk`. + +## Import + +Service root: `services/api7__enterprise_v3-10-2`. + +```bash +octobus service import --id api7-enterprise-v3-10-2 ./services/api7__enterprise_v3-10-2 +``` + +## Instance + +```bash +octobus instance create api7-enterprise \ + --service api7-enterprise-v3-10-2 \ + --config-json '{"api7_base_url":"https://api7.example.com","gateway_group_id":"gw-1","timeoutMs":5000}' \ + --secret-json '{"api7_api_key":""}' +``` + +Basic auth is also supported: + +```bash +octobus instance update-secret api7-enterprise \ + --secret-json '{"username":"admin@example.com","password":""}' +``` + +## Bindings + +Configuration: + +- `api7_base_url`: API7 Enterprise base URL. +- `baseUrl`, `restBaseUrl`: aliases for `api7_base_url`. +- `gateway_group_id`: optional default gateway group ID for APISIX admin list APIs. +- `timeoutMs`: optional timeout in milliseconds. Defaults to `5000`. +- `headers`: optional JSON object or JSON string with extra HTTP headers. +- `skipTlsVerify`, `tlsInsecureSkipVerify`, `insecureSkipVerify`: optional booleans for test environments with self-signed or untrusted TLS certificates. + +Secrets: + +- `api7_api_key`: API7 API key sent as `X-API-KEY`. +- `apiKey`, `xApiKey`: aliases for `api7_api_key`. +- `username` + `password`: fallback Basic Auth credentials when API key is not provided. + +## Phase-1 Methods + +### Security operations and governance + +- `ListAuditLogs` +- `ExportAuditLogs` +- `ListAlertPolicies` +- `ListAlertHistories` +- `ListUsers` +- `ListRoles` +- `ListTokens` +- `ListApprovals` + +### Consumer and credential visibility + +- `ListConsumers` +- `ListConsumerCredentials` + +### TLS and certificate visibility + +- `ListCertificates` +- `ListCACertificates` +- `ListSNIs` +- `GetCertificateUsage` +- `ParseCertificate` +- `ValidateCertificateKey` + +### Gateway configuration visibility + +- `ListGatewayGroups` +- `ListGatewayInstances` +- `ListRoutes` +- `ListServices` +- `ListGlobalRules` +- `ListPlugins` +- `GetPluginSchema` +- `ListSecretProviders` +- `GetSecretProviderUsage` + +### Debug and health visibility + +- `ListDebugSessions` +- `ListDebugTraces` +- `GetServiceHealthcheck` + +## Added write methods + +- `CreateToken` +- `CreateConsumer` +- `CreateConsumerCredential` +- `CreateCertificate` +- `CreateSNI` +- `CreateService` +- `CreateRoute` +- `CreateGlobalRule` + +## Behavior + +- The package talks directly to API7 Enterprise REST endpoints under `/api/*` and `/apisix/admin/*`. +- API key auth is preferred; Basic Auth is used only when `api7_api_key` is absent. +- When `skipTlsVerify` (or its aliases) is true, the package disables TLS certificate verification for upstream API7 HTTPS calls. Use only in test environments. +- The package now includes a small set of creation-oriented write APIs for test and controlled operations, in addition to low-risk parsing/validation helpers. +- `ListRoutes` now requires `service_id`, matching the behavior observed from the API7 3.10.2 test environment. +- Response mapping is intentionally generic to keep the surface stable across API7 minor revisions: + - list endpoints try to infer `results` from `list`, `items`, `results`, `data`, or top-level arrays + - `count` is inferred from `count`, `total`, `total_size`, `totalSize`, or the results length + - the original upstream payload is also preserved in `raw_json` + +## Risk and capset guidance + +Recommended capset scope for phase 1: + +- Read-only operations for security operations dashboards and agent workflows. +- Avoid exposing future write methods such as route patching, certificate creation, token rotation, or admin key updates in broad capsets. +- Use separate instances or credentials for production and test gateway groups. + +## Validation + +```bash +cd services +npm run validate -- --service-dir api7__enterprise_v3-10-2 +npm test -- --service-dir api7__enterprise_v3-10-2 +``` diff --git a/services/api7__enterprise_v3-10-2/bin/api7-enterprise-v3-10-2.js b/services/api7__enterprise_v3-10-2/bin/api7-enterprise-v3-10-2.js new file mode 100755 index 00000000..5f9a3061 --- /dev/null +++ b/services/api7__enterprise_v3-10-2/bin/api7-enterprise-v3-10-2.js @@ -0,0 +1,7 @@ +#!/usr/bin/env node + +import { runServiceMain } from '@chaitin-ai/octobus-sdk'; + +import { service } from '../src/service.js'; + +runServiceMain(service); diff --git a/services/api7__enterprise_v3-10-2/config.schema.json b/services/api7__enterprise_v3-10-2/config.schema.json new file mode 100644 index 00000000..a23ee3a6 --- /dev/null +++ b/services/api7__enterprise_v3-10-2/config.schema.json @@ -0,0 +1,41 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "api7_base_url": { + "type": "string", + "minLength": 1, + "description": "API7 Enterprise base URL, for example https://api7.example.com" + }, + "baseUrl": { + "type": "string" + }, + "restBaseUrl": { + "type": "string" + }, + "gateway_group_id": { + "type": "string", + "description": "Default gateway group ID for APISIX admin style list operations." + }, + "gatewayGroupId": { + "type": "string" + }, + "timeoutMs": { + "type": "integer", + "minimum": 1, + "default": 5000 + }, + "headers": { + "description": "Optional JSON object or JSON string with extra HTTP headers.", + "oneOf": [ + { "type": "object", "additionalProperties": { "type": ["string", "number", "boolean"] } }, + { "type": "string" } + ] + }, + "skipTlsVerify": { "type": "boolean" }, + "tlsInsecureSkipVerify": { "type": "boolean" }, + "insecureSkipVerify": { "type": "boolean" } + }, + "required": ["api7_base_url"] +} diff --git a/services/api7__enterprise_v3-10-2/package.json b/services/api7__enterprise_v3-10-2/package.json new file mode 100644 index 00000000..46411cc4 --- /dev/null +++ b/services/api7__enterprise_v3-10-2/package.json @@ -0,0 +1,13 @@ +{ + "name": "api7-enterprise-v3-10-2", + "version": "0.0.0", + "private": true, + "type": "module", + "bin": { + "api7-enterprise-v3-10-2": "bin/api7-enterprise-v3-10-2.js" + }, + "dependencies": { + "@chaitin-ai/octobus-sdk": "^0.6.0", + "undici": "^7.16.0" + } +} diff --git a/services/api7__enterprise_v3-10-2/proto/api7_enterprise_v3_10_2.proto b/services/api7__enterprise_v3-10-2/proto/api7_enterprise_v3_10_2.proto new file mode 100644 index 00000000..baa75571 --- /dev/null +++ b/services/api7__enterprise_v3-10-2/proto/api7_enterprise_v3_10_2.proto @@ -0,0 +1,419 @@ +syntax = "proto3"; + +package API7_Enterprise_V3_10_2; + +import "google/protobuf/struct.proto"; +import "google/protobuf/wrappers.proto"; + +option go_package = "miner/grpc-service/API7_Enterprise_V3_10_2"; + +service API7_Enterprise_V3_10_2 { + rpc ListAuditLogs(ListAuditLogsRequest) returns (ListResponse) {} + rpc ExportAuditLogs(ExportAuditLogsRequest) returns (BlobResponse) {} + rpc ListAlertPolicies(ListAlertPoliciesRequest) returns (ListResponse) {} + rpc ListAlertHistories(ListAlertHistoriesRequest) returns (ListResponse) {} + rpc ListUsers(ListUsersRequest) returns (ListResponse) {} + rpc ListRoles(ListRolesRequest) returns (ListResponse) {} + rpc ListTokens(ListTokensRequest) returns (ListResponse) {} + rpc ListConsumers(ListConsumersRequest) returns (ListResponse) {} + rpc ListConsumerCredentials(ListConsumerCredentialsRequest) returns (ListResponse) {} + rpc ListCertificates(ListCertificatesRequest) returns (ListResponse) {} + rpc ListCACertificates(ListCertificatesRequest) returns (ListResponse) {} + rpc ListSNIs(ListSNIsRequest) returns (ListResponse) {} + rpc GetCertificateUsage(GetCertificateUsageRequest) returns (ListResponse) {} + rpc ListGatewayGroups(ListGatewayGroupsRequest) returns (ListResponse) {} + rpc ListGatewayInstances(ListGatewayInstancesRequest) returns (ListResponse) {} + rpc ListRoutes(ListRoutesRequest) returns (ListResponse) {} + rpc ListServices(ListServicesRequest) returns (ListResponse) {} + rpc ListGlobalRules(ListGlobalRulesRequest) returns (ListResponse) {} + rpc ListPlugins(ListPluginsRequest) returns (ObjectResponse) {} + rpc GetPluginSchema(GetPluginSchemaRequest) returns (ObjectResponse) {} + rpc ListApprovals(ListApprovalsRequest) returns (ListResponse) {} + rpc ListSecretProviders(ListSecretProvidersRequest) returns (ListResponse) {} + rpc GetSecretProviderUsage(GetSecretProviderUsageRequest) returns (ListResponse) {} + rpc ParseCertificate(ParseCertificateRequest) returns (ObjectResponse) {} + rpc ValidateCertificateKey(ValidateCertificateKeyRequest) returns (ObjectResponse) {} + rpc ListDebugSessions(ListDebugSessionsRequest) returns (ListResponse) {} + rpc ListDebugTraces(ListDebugTracesRequest) returns (ListResponse) {} + rpc GetServiceHealthcheck(GetServiceHealthcheckRequest) returns (ObjectResponse) {} + rpc CreateToken(CreateTokenRequest) returns (ObjectResponse) {} + rpc CreateConsumer(CreateConsumerRequest) returns (ObjectResponse) {} + rpc CreateConsumerCredential(CreateConsumerCredentialRequest) returns (ObjectResponse) {} + rpc CreateCertificate(CreateCertificateRequest) returns (ObjectResponse) {} + rpc CreateSNI(CreateSNIRequest) returns (ObjectResponse) {} + rpc CreateService(CreateServiceRequest) returns (ObjectResponse) {} + rpc CreateRoute(CreateRouteRequest) returns (ObjectResponse) {} + rpc CreateGlobalRule(CreateGlobalRuleRequest) returns (ObjectResponse) {} +} + +message PagingRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2 [json_name = "pageSize"]; + string direction = 3; + string order_by = 4 [json_name = "orderBy"]; + string search = 5; +} + +message ListAuditLogsRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2 [json_name = "pageSize"]; + string direction = 3; + string order_by = 4 [json_name = "orderBy"]; + string event_type = 5 [json_name = "eventType"]; + string operator_id = 6 [json_name = "operatorId"]; + string gateway_group_id = 7 [json_name = "gatewayGroupId"]; + string resource_id = 8 [json_name = "resourceId"]; + google.protobuf.Int64Value start_at = 9 [json_name = "startAt"]; + google.protobuf.Int64Value end_at = 10 [json_name = "endAt"]; +} + +message ExportAuditLogsRequest { + string format = 1; + string event_type = 2 [json_name = "eventType"]; + string operator_id = 3 [json_name = "operatorId"]; + string gateway_group_id = 4 [json_name = "gatewayGroupId"]; + string resource_id = 5 [json_name = "resourceId"]; + google.protobuf.Int64Value start_at = 6 [json_name = "startAt"]; + google.protobuf.Int64Value end_at = 7 [json_name = "endAt"]; +} + +message ListAlertPoliciesRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2 [json_name = "pageSize"]; + string direction = 3; + string order_by = 4 [json_name = "orderBy"]; + string search = 5; + map labels = 6; + repeated string status = 7; + repeated string severity = 8; +} + +message ListAlertHistoriesRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2 [json_name = "pageSize"]; + string direction = 3; + string order_by = 4 [json_name = "orderBy"]; + string search = 5; + string alert_policy_id = 6 [json_name = "alertPolicyId"]; + repeated string severity = 7; + google.protobuf.Int64Value start_at = 8 [json_name = "startAt"]; + google.protobuf.Int64Value end_at = 9 [json_name = "endAt"]; + string gateway_group_id = 10 [json_name = "gatewayGroupId"]; +} + +message ListUsersRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2 [json_name = "pageSize"]; + string direction = 3; + string order_by = 4 [json_name = "orderBy"]; + repeated string roles = 5; +} + +message ListRolesRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2 [json_name = "pageSize"]; + string direction = 3; + string order_by = 4 [json_name = "orderBy"]; + string search = 5; + map labels = 6; +} + +message ListTokensRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2 [json_name = "pageSize"]; + string direction = 3; + string order_by = 4 [json_name = "orderBy"]; + string search = 5; +} + +message ListConsumersRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + google.protobuf.Int64Value page = 2; + google.protobuf.Int64Value page_size = 3 [json_name = "pageSize"]; + string direction = 4; + string order_by = 5 [json_name = "orderBy"]; + string search = 6; + map labels = 7; +} + +message ListConsumerCredentialsRequest { + string username = 1; + string gateway_group_id = 2 [json_name = "gatewayGroupId"]; + string plugin_name = 3 [json_name = "pluginName"]; + google.protobuf.Int64Value page = 4; + google.protobuf.Int64Value page_size = 5 [json_name = "pageSize"]; + string direction = 6; + string order_by = 7 [json_name = "orderBy"]; + string search = 8; + map labels = 9; +} + +message ListCertificatesRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + google.protobuf.Int64Value page = 2; + google.protobuf.Int64Value page_size = 3 [json_name = "pageSize"]; + string direction = 4; + string order_by = 5 [json_name = "orderBy"]; + string search = 6; + map labels = 7; + string sni_id = 8 [json_name = "sniId"]; + string sni_name = 9 [json_name = "sniName"]; + google.protobuf.Int64Value exptime = 10; +} + +message ListSNIsRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + google.protobuf.Int64Value page = 2; + google.protobuf.Int64Value page_size = 3 [json_name = "pageSize"]; + string direction = 4; + string order_by = 5 [json_name = "orderBy"]; + string search = 6; + map labels = 7; + string domain = 8; + google.protobuf.BoolValue mtls_enabled = 9 [json_name = "mtlsEnabled"]; +} + +message GetCertificateUsageRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string certificate_id = 2 [json_name = "certificateId"]; + string resource_type = 3 [json_name = "resourceType"]; + google.protobuf.Int64Value page = 4; + google.protobuf.Int64Value page_size = 5 [json_name = "pageSize"]; + string direction = 6; + string order_by = 7 [json_name = "orderBy"]; +} + +message ListGatewayGroupsRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2 [json_name = "pageSize"]; + string direction = 3; + string order_by = 4 [json_name = "orderBy"]; + string search = 5; + string name = 6; + map labels = 7; +} + +message ListGatewayInstancesRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + google.protobuf.Int64Value page = 2; + google.protobuf.Int64Value page_size = 3 [json_name = "pageSize"]; + string direction = 4; + string order_by = 5 [json_name = "orderBy"]; + string search = 6; + string status = 7; + string compatibility = 8; +} + +message ListRoutesRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string service_id = 2 [json_name = "serviceId"]; + google.protobuf.Int64Value page = 3; + google.protobuf.Int64Value page_size = 4 [json_name = "pageSize"]; + string direction = 5; + string order_by = 6 [json_name = "orderBy"]; + string search = 7; + google.protobuf.BoolValue with_publish_info = 8 [json_name = "withPublishInfo"]; +} + +message ListServicesRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + google.protobuf.Int64Value page = 2; + google.protobuf.Int64Value page_size = 3 [json_name = "pageSize"]; + string direction = 4; + string order_by = 5 [json_name = "orderBy"]; + string search = 6; + google.protobuf.BoolValue unhealthy_nodes = 7 [json_name = "unhealthyNodes"]; + map labels = 8; + string hosts = 9; + string type = 10; + google.protobuf.BoolValue with_publish_info = 11 [json_name = "withPublishInfo"]; +} + +message ListGlobalRulesRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + google.protobuf.Int64Value page = 2; + google.protobuf.Int64Value page_size = 3 [json_name = "pageSize"]; + string direction = 4; + string order_by = 5 [json_name = "orderBy"]; + string search = 6; +} + +message ListPluginsRequest { + string subsystem = 1; +} + +message GetPluginSchemaRequest { + string plugin_name = 1 [json_name = "pluginName"]; + string subsystem = 2; +} + +message ListApprovalsRequest { + google.protobuf.Int64Value page = 1; + google.protobuf.Int64Value page_size = 2 [json_name = "pageSize"]; + string direction = 3; + string order_by = 4 [json_name = "orderBy"]; + string search = 5; + string status = 6; + string result = 7; + string event = 8; + string resource_type = 9 [json_name = "resourceType"]; + string resource_name = 10 [json_name = "resourceName"]; + string operator_name = 11 [json_name = "operatorName"]; + string applicant_name = 12 [json_name = "applicantName"]; +} + +message ListSecretProvidersRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + google.protobuf.Int64Value page = 2; + google.protobuf.Int64Value page_size = 3 [json_name = "pageSize"]; + string direction = 4; + string order_by = 5 [json_name = "orderBy"]; + string search = 6; +} + +message GetSecretProviderUsageRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string secret_provider = 2 [json_name = "secretProvider"]; + string secret_provider_id = 3 [json_name = "secretProviderId"]; + string resource_type = 4 [json_name = "resourceType"]; + google.protobuf.Int64Value page = 5; + google.protobuf.Int64Value page_size = 6 [json_name = "pageSize"]; + string direction = 7; + string order_by = 8 [json_name = "orderBy"]; +} + +message ParseCertificateRequest { + string cert = 1; +} + +message ValidateCertificateKeyRequest { + string cert = 1; + string key = 2; +} + +message ListDebugSessionsRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + google.protobuf.Int64Value page = 2; + google.protobuf.Int64Value page_size = 3 [json_name = "pageSize"]; + string direction = 4; + string order_by = 5 [json_name = "orderBy"]; + string search = 6; +} + +message ListDebugTracesRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string debug_session_id = 2 [json_name = "debugSessionId"]; +} + +message GetServiceHealthcheckRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string apisix_service_id = 2 [json_name = "apisixServiceId"]; + string upstream_id = 3 [json_name = "upstreamId"]; +} + + +message CreateTokenRequest { + string name = 1; + int64 expires_at = 2 [json_name = "expiresAt"]; +} + +message CreateConsumerRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string username = 2; + string desc = 3; + map labels = 4; + google.protobuf.Struct plugins = 5; +} + +message CreateConsumerCredentialRequest { + string username = 1; + string gateway_group_id = 2 [json_name = "gatewayGroupId"]; + string name = 3; + string desc = 4; + map labels = 5; + google.protobuf.Struct plugins = 6; +} + +message CreateCertificateRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string name = 2; + string desc = 3; + map labels = 4; + string cert = 5; + string key = 6; +} + +message CreateSNIRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string name = 2; + string desc = 3; + string domain = 4; + repeated string certificates = 5; + google.protobuf.Struct mtls = 6; + google.protobuf.Struct plugins = 7; +} + +message UpstreamNode { + string host = 1; + int32 port = 2; + int32 weight = 3; + int32 priority = 4; +} + +message CreateServiceRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string name = 2; + string desc = 3; + map labels = 4; + string type = 5; + repeated string hosts = 6; + string path_prefix = 7 [json_name = "pathPrefix"]; + google.protobuf.BoolValue strip_path_prefix = 8 [json_name = "stripPathPrefix"]; + google.protobuf.Struct plugins = 9; + string upstream_scheme = 10 [json_name = "upstreamScheme"]; + string upstream_pass_host = 11 [json_name = "upstreamPassHost"]; + string upstream_host = 12 [json_name = "upstreamHost"]; + repeated UpstreamNode upstream_nodes = 13 [json_name = "upstreamNodes"]; +} + +message CreateRouteRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + string service_id = 2 [json_name = "serviceId"]; + string name = 3; + string desc = 4; + map labels = 5; + repeated string methods = 6; + repeated string paths = 7; + int32 priority = 8; + google.protobuf.BoolValue enable_websocket = 9 [json_name = "enableWebsocket"]; + google.protobuf.Struct plugins = 10; + google.protobuf.Struct timeout = 11; + google.protobuf.ListValue vars = 12; +} + +message CreateGlobalRuleRequest { + string gateway_group_id = 1 [json_name = "gatewayGroupId"]; + google.protobuf.Struct plugins = 2; +} + +message ListResponse { + int32 http_status = 1; + string raw_body = 2; + int64 count = 3; + repeated google.protobuf.Value results = 4; + google.protobuf.Value raw_json = 5; +} + +message ObjectResponse { + int32 http_status = 1; + string raw_body = 2; + google.protobuf.Value raw_json = 3; +} + +message BlobResponse { + int32 http_status = 1; + string raw_body = 2; + string content_type = 3 [json_name = "contentType"]; + string filename = 4; + google.protobuf.Value raw_json = 5; +} diff --git a/services/api7__enterprise_v3-10-2/secret.schema.json b/services/api7__enterprise_v3-10-2/secret.schema.json new file mode 100644 index 00000000..43600beb --- /dev/null +++ b/services/api7__enterprise_v3-10-2/secret.schema.json @@ -0,0 +1,25 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "type": "object", + "additionalProperties": true, + "properties": { + "api7_api_key": { + "type": "string", + "description": "API7 API key sent as X-API-KEY." + }, + "apiKey": { + "type": "string" + }, + "xApiKey": { + "type": "string" + }, + "username": { + "type": "string", + "description": "API7 username when using Basic Auth." + }, + "password": { + "type": "string", + "description": "API7 password when using Basic Auth." + } + } +} diff --git a/services/api7__enterprise_v3-10-2/service.json b/services/api7__enterprise_v3-10-2/service.json new file mode 100644 index 00000000..2c59792f --- /dev/null +++ b/services/api7__enterprise_v3-10-2/service.json @@ -0,0 +1,12 @@ +{ + "schema": "chaitin.octobus.service.v1", + "name": "api7-enterprise-v3-10-2", + "displayName": "API7 Enterprise 3.10.2", + "description": "OctoBus service package for API7 Enterprise Admin APIs focused on security operations and gateway governance.", + "proto": { + "roots": ["proto"], + "files": ["proto/api7_enterprise_v3_10_2.proto"] + }, + "configSchema": "config.schema.json", + "secretSchema": "secret.schema.json" +} diff --git a/services/api7__enterprise_v3-10-2/src/api7-enterprise-v3-10-2.js b/services/api7__enterprise_v3-10-2/src/api7-enterprise-v3-10-2.js new file mode 100644 index 00000000..42653316 --- /dev/null +++ b/services/api7__enterprise_v3-10-2/src/api7-enterprise-v3-10-2.js @@ -0,0 +1,892 @@ +import { GrpcError, grpcStatus } from '@chaitin-ai/octobus-sdk'; +import { Agent } from 'undici'; + +export const DEFAULT_TIMEOUT_MS = 5000; + +const METHOD_SPECS = [ + ['ListAuditLogs', '/api/audit_logs', 'list'], + ['ExportAuditLogs', '/api/audit_logs/export', 'blob'], + ['ListAlertPolicies', '/api/alert/policies', 'list'], + ['ListAlertHistories', '/api/alert/policies/histories', 'list'], + ['ListUsers', '/api/users', 'list'], + ['ListRoles', '/api/roles', 'list'], + ['ListTokens', '/api/tokens', 'list'], + ['ListConsumers', '/apisix/admin/consumers', 'list'], + ['ListConsumerCredentials', '/apisix/admin/consumers/{username}/credentials', 'list'], + ['ListCertificates', '/apisix/admin/certificates', 'list'], + ['ListCACertificates', '/apisix/admin/ca_certificates', 'list'], + ['ListSNIs', '/apisix/admin/snis', 'list'], + ['GetCertificateUsage', '/api/gateway_groups/{gateway_group_id}/certificates/{certificate_id}/usage', 'list'], + ['ListGatewayGroups', '/api/gateway_groups', 'list'], + ['ListGatewayInstances', '/api/gateway_groups/{gateway_group_id}/instances', 'list'], + ['ListRoutes', '/apisix/admin/routes', 'list'], + ['ListServices', '/apisix/admin/services', 'list'], + ['ListGlobalRules', '/apisix/admin/global_rules', 'list'], + ['ListPlugins', '/apisix/admin/plugins', 'object'], + ['GetPluginSchema', '/apisix/admin/schema/plugins/{plugin_name}', 'object'], + ['ListApprovals', '/api/approvals', 'list'], + ['ListSecretProviders', '/apisix/admin/secret_providers', 'list'], + ['GetSecretProviderUsage', '/api/gateway_groups/{gateway_group_id}/secret_providers/{secret_provider}/{secret_provider_id}/usage', 'list'], + ['ParseCertificate', '/api/parse_certificate', 'object', 'PUT'], + ['ValidateCertificateKey', '/api/validate_cert_key', 'object', 'PUT'], + ['ListDebugSessions', '/api/gateway_groups/{gateway_group_id}/debug_sessions', 'list'], + ['ListDebugTraces', '/api/gateway_groups/{gateway_group_id}/debug_sessions/{debug_session_id}/traces', 'list'], + ['GetServiceHealthcheck', '/api/gateway_groups/{gateway_group_id}/services/{apisix_service_id}/healthcheck', 'object'], + ['CreateToken', '/api/tokens', 'object', 'POST'], + ['CreateConsumer', '/apisix/admin/consumers', 'object', 'POST'], + ['CreateConsumerCredential', '/apisix/admin/consumers/{username}/credentials', 'object', 'POST'], + ['CreateCertificate', '/apisix/admin/certificates', 'object', 'POST'], + ['CreateSNI', '/apisix/admin/snis', 'object', 'POST'], + ['CreateService', '/apisix/admin/services', 'object', 'POST'], + ['CreateRoute', '/apisix/admin/routes', 'object', 'POST'], + ['CreateGlobalRule', '/apisix/admin/global_rules', 'object', 'POST'], +]; + +export const METHOD_PATHS = Object.fromEntries(METHOD_SPECS.map(([name]) => [name, `/API7_Enterprise_V3_10_2.API7_Enterprise_V3_10_2/${name}`])); +export const METHOD_FULLS = Object.fromEntries(METHOD_SPECS.map(([name]) => [name, `API7_Enterprise_V3_10_2.API7_Enterprise_V3_10_2/${name}`])); + +const grpcCodeFor = (code) => ({ + FAILED_PRECONDITION: grpcStatus.FAILED_PRECONDITION, + INVALID_ARGUMENT: grpcStatus.INVALID_ARGUMENT, + PERMISSION_DENIED: grpcStatus.PERMISSION_DENIED, + UNAVAILABLE: grpcStatus.UNAVAILABLE, + UNKNOWN: grpcStatus.UNKNOWN, +})[code] ?? grpcStatus.UNKNOWN; + +const errorWithCode = (code, message) => { + const err = new GrpcError(grpcCodeFor(code), String(message ?? '')); + err.legacyCode = code; + return err; +}; + +const hasOwn = (obj, key) => Object.prototype.hasOwnProperty.call(obj ?? {}, key); +const firstDefined = (...values) => values.find((value) => value !== undefined && value !== null); +const reqField = (req = {}, snakeName, camelName) => firstDefined(req[snakeName], camelName ? req[camelName] : undefined); + +const unwrapScalar = (value) => { + if (value === undefined || value === null) return undefined; + if (typeof value === 'object' && !Array.isArray(value) && hasOwn(value, 'value')) return unwrapScalar(value.value); + return value; +}; + +const toTrimmedString = (value) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null) return ''; + return String(raw).trim(); +}; + +const toOptionalInt = (value, options = {}) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null || raw === '') return undefined; + const num = Number(raw); + if (!Number.isInteger(num) || Number.isNaN(num)) return undefined; + if (options.min !== undefined && num < options.min) return undefined; + return num; +}; + +const toOptionalBool = (value) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null || raw === '') return undefined; + if (typeof raw === 'boolean') return raw; + if (typeof raw === 'number') { + if (raw === 1) return true; + if (raw === 0) return false; + return undefined; + } + const normalized = String(raw).trim().toLowerCase(); + if (normalized === 'true' || normalized === '1') return true; + if (normalized === 'false' || normalized === '0') return false; + return undefined; +}; + +const toStringArray = (value) => { + if (value === undefined || value === null || value === '') return []; + if (Array.isArray(value)) return value.map((item) => toTrimmedString(item)).filter(Boolean); + const raw = unwrapScalar(value); + if (Array.isArray(raw)) return raw.map((item) => toTrimmedString(item)).filter(Boolean); + return toTrimmedString(raw).split(',').map((item) => item.trim()).filter(Boolean); +}; + +const toStringMap = (value) => { + if (value === undefined || value === null || value === '') return {}; + if (typeof value !== 'object' || Array.isArray(value)) return {}; + const out = {}; + for (const [key, val] of Object.entries(value)) { + const stringVal = toTrimmedString(val); + if (stringVal) out[key] = stringVal; + } + return out; +}; + +const normalizeBaseUrl = (value) => { + const raw = toTrimmedString(value); + if (!/^https?:\/\//i.test(raw)) return ''; + return raw.replace(/\/+$/, ''); +}; + +const structToPlainObject = (value) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null || raw === '') return undefined; + if (typeof raw !== 'object' || Array.isArray(raw)) return undefined; + if (hasOwn(raw, 'fields') && typeof raw.fields === 'object') { + const out = {}; + for (const [key, item] of Object.entries(raw.fields)) out[key] = valueToPlain(item); + return out; + } + return raw; +}; + +const listValueToPlainArray = (value) => { + const raw = unwrapScalar(value); + if (raw === undefined || raw === null || raw === '') return undefined; + if (Array.isArray(raw)) return raw; + if (typeof raw === 'object' && raw && Array.isArray(raw.values)) return raw.values.map((item) => valueToPlain(item)); + return undefined; +}; + +const valueToPlain = (value) => { + if (value === undefined || value === null) return value; + if (typeof value !== 'object' || Array.isArray(value)) return value; + if (hasOwn(value, 'nullValue')) return null; + if (hasOwn(value, 'stringValue')) return value.stringValue; + if (hasOwn(value, 'numberValue')) return value.numberValue; + if (hasOwn(value, 'boolValue')) return value.boolValue; + if (hasOwn(value, 'listValue')) return (value.listValue?.values || []).map((item) => valueToPlain(item)); + if (hasOwn(value, 'structValue')) { + const fields = value.structValue?.fields || {}; + const out = {}; + for (const [key, item] of Object.entries(fields)) out[key] = valueToPlain(item); + return out; + } + if (hasOwn(value, 'value')) return valueToPlain(value.value); + return value; +}; + +const mergedBindings = (ctx = {}) => ({ + ...(ctx.config ?? {}), + ...(ctx.secret ?? {}), + ...(ctx.bindings ?? {}), +}); + +const resolveCallContext = (ctx = {}) => ({ + ...ctx, + bindings: mergedBindings(ctx), + limits: ctx.limits ?? {}, + meta: ctx.meta ?? {}, + req: ctx.req ?? ctx.request ?? {}, +}); + +const isSdkCallContext = (value) => ( + value != null + && typeof value === 'object' + && ( + hasOwn(value, 'request') + || hasOwn(value, 'config') + || hasOwn(value, 'secret') + || hasOwn(value, 'metadata') + || hasOwn(value, 'method') + || hasOwn(value, 'packageDir') + ) +); + +const resolveHandlerArgs = (reqOrCtx = {}, maybeCtx) => { + if (maybeCtx !== undefined) return { req: reqOrCtx ?? {}, ctx: maybeCtx ?? {} }; + if (isSdkCallContext(reqOrCtx)) return { req: reqOrCtx.request ?? reqOrCtx.req ?? {}, ctx: reqOrCtx }; + return { req: reqOrCtx ?? {}, ctx: {} }; +}; + +const resolveBaseUrl = (bindings = {}) => normalizeBaseUrl(firstDefined( + bindings.api7_base_url, + bindings.baseUrl, + bindings.restBaseUrl, + bindings.domain, +)); + +const resolveApiKey = (bindings = {}) => toTrimmedString(firstDefined( + bindings.api7_api_key, + bindings.apiKey, + bindings.xApiKey, +)); + +const resolveUsername = (bindings = {}) => toTrimmedString(bindings.username); +const resolvePassword = (bindings = {}) => toTrimmedString(bindings.password); +const resolveTimeoutMs = (ctx = {}) => { + const raw = Number(firstDefined(ctx.limits?.timeoutMs, ctx.bindings?.timeoutMs, DEFAULT_TIMEOUT_MS)); + return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_TIMEOUT_MS; +}; + +const resolveDefaultGatewayGroupId = (bindings = {}) => toTrimmedString(firstDefined( + bindings.gateway_group_id, + bindings.gatewayGroupId, +)); + +const parseHeaders = (value) => { + if (value === undefined || value === null || value === '') return {}; + if (typeof value === 'object' && !Array.isArray(value)) return value; + if (typeof value === 'string') { + try { + const parsed = JSON.parse(value); + if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed; + } catch { + return {}; + } + } + return {}; +}; + +const tlsSkipRequested = (bindings = {}) => Boolean(bindings.skipTlsVerify || bindings.tlsInsecureSkipVerify || bindings.insecureSkipVerify); + +const buildDispatcher = (bindings = {}) => { + if (!tlsSkipRequested(bindings)) return undefined; + return new Agent({ + connect: { + rejectUnauthorized: false, + }, + }); +}; + +const makeTimeoutSignal = (timeoutMs) => { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), timeoutMs); + return { signal: controller.signal, clear: () => clearTimeout(timeoutId) }; +}; + +const requireBaseUrl = (ctx = {}) => { + const baseUrl = resolveBaseUrl(ctx.bindings || {}); + if (!baseUrl) throw errorWithCode('INVALID_ARGUMENT', 'api7_base_url is required in bindings'); + return baseUrl; +}; + +const requireAuth = (ctx = {}) => { + const apiKey = resolveApiKey(ctx.bindings || {}); + const username = resolveUsername(ctx.bindings || {}); + const password = resolvePassword(ctx.bindings || {}); + if (apiKey) return { type: 'apiKey', apiKey }; + if (username && password) return { type: 'basic', username, password }; + throw errorWithCode('INVALID_ARGUMENT', 'either api7_api_key or username/password is required in bindings'); +}; + +const requireString = (value, fieldName) => { + const text = toTrimmedString(value); + if (!text) throw errorWithCode('INVALID_ARGUMENT', `${fieldName} is required`); + return text; +}; + +const requireNumber = (value, fieldName) => { + const raw = Number(unwrapScalar(value)); + if (!Number.isFinite(raw)) throw errorWithCode('INVALID_ARGUMENT', `${fieldName} must be a number`); + return raw; +}; + +const buildRequestHeaders = (ctx = {}, extra = {}) => { + const auth = requireAuth(ctx); + const headers = { + Accept: 'application/json', + ...parseHeaders(ctx.bindings?.headers), + ...extra, + }; + if (auth.type === 'apiKey') { + headers['X-API-KEY'] = auth.apiKey; + } else { + headers.Authorization = `Basic ${Buffer.from(`${auth.username}:${auth.password}`).toString('base64')}`; + } + return headers; +}; + +const encodeQueryPairs = (query = {}) => { + const parts = []; + for (const [key, value] of Object.entries(query)) { + if (value === undefined || value === null || value === '') continue; + if (Array.isArray(value)) { + for (const item of value) { + if (item === undefined || item === null || item === '') continue; + parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(item))}`); + } + continue; + } + if (typeof value === 'object') { + for (const [subKey, subValue] of Object.entries(value)) { + if (subValue === undefined || subValue === null || subValue === '') continue; + parts.push(`${encodeURIComponent(`${key}[${subKey}]`)}=${encodeURIComponent(String(subValue))}`); + } + continue; + } + parts.push(`${encodeURIComponent(key)}=${encodeURIComponent(String(value))}`); + } + return parts.join('&'); +}; + +const buildUrl = (baseUrl, path, query) => { + const base = String(baseUrl || '').replace(/\/+$/, ''); + const normalizedPath = String(path || '').replace(/^\/+/, ''); + const qs = encodeQueryPairs(query); + const joined = `${base}/${normalizedPath}`; + return qs ? `${joined}?${qs}` : joined; +}; + +const fillPath = (template, params = {}) => template.replace(/\{([^}]+)\}/g, (_, key) => encodeURIComponent(requireString(params[key], key))); + +const tryParseJson = (text) => { + try { + return { ok: true, value: JSON.parse(text) }; + } catch { + return { ok: false }; + } +}; + +const toValue = (value) => { + if (value === undefined || value === null) return undefined; + if (typeof value === 'string') return { stringValue: value }; + if (typeof value === 'boolean') return { boolValue: value }; + if (typeof value === 'number') return Number.isFinite(value) ? { numberValue: value } : { stringValue: String(value) }; + if (Array.isArray(value)) { + return { listValue: { values: value.map((item) => toValue(item) ?? { nullValue: 'NULL_VALUE' }) } }; + } + if (typeof value === 'object') { + const fields = {}; + for (const [key, item] of Object.entries(value)) { + fields[key] = toValue(item) ?? { nullValue: 'NULL_VALUE' }; + } + return { structValue: { fields } }; + } + return { stringValue: String(value) }; +}; + +const inferResultsArray = (value) => { + if (Array.isArray(value)) return value; + if (!value || typeof value !== 'object') return []; + if (Array.isArray(value.results)) return value.results; + if (Array.isArray(value.list)) return value.list; + if (Array.isArray(value.items)) return value.items; + if (Array.isArray(value.data)) return value.data; + if (value.data && typeof value.data === 'object') { + if (Array.isArray(value.data.items)) return value.data.items; + if (Array.isArray(value.data.list)) return value.data.list; + } + return []; +}; + +const inferCount = (value, fallbackLength) => { + if (value && typeof value === 'object') { + for (const key of ['count', 'total', 'total_size', 'totalSize']) { + const maybe = Number(value[key]); + if (Number.isFinite(maybe)) return maybe; + } + if (value.pagination && typeof value.pagination === 'object') { + for (const key of ['count', 'total', 'total_size', 'totalSize']) { + const maybe = Number(value.pagination[key]); + if (Number.isFinite(maybe)) return maybe; + } + } + if (value.data && typeof value.data === 'object') { + for (const key of ['count', 'total', 'total_size', 'totalSize']) { + const maybe = Number(value.data[key]); + if (Number.isFinite(maybe)) return maybe; + } + } + } + return fallbackLength; +}; + +const parseFilename = (contentDisposition = '') => { + const match = /filename\*=UTF-8''([^;]+)|filename="?([^";]+)"?/i.exec(String(contentDisposition)); + const raw = match?.[1] || match?.[2] || ''; + try { + return decodeURIComponent(raw); + } catch { + return raw; + } +}; + +const KEEP_EMPTY_OBJECT = Symbol('keepEmptyObject'); +const preserveEmptyObjectTree = (value) => { + if (Array.isArray(value)) return value.map((item) => preserveEmptyObjectTree(item)); + if (value && typeof value === 'object') { + const out = Object.fromEntries(Object.entries(value).map(([key, item]) => [key, preserveEmptyObjectTree(item)])); + out[KEEP_EMPTY_OBJECT] = true; + return out; + } + return value; +}; + +const sanitizeObject = (value) => { + if (Array.isArray(value)) return value.map(sanitizeObject).filter((item) => item !== undefined); + if (value && typeof value === 'object') { + const preserveEmpty = value[KEEP_EMPTY_OBJECT] === true; + const out = {}; + for (const [key, item] of Object.entries(value)) { + const cleaned = sanitizeObject(item); + if (cleaned === undefined) continue; + if (Array.isArray(cleaned) && cleaned.length === 0) continue; + if (typeof cleaned === 'object' && cleaned && !Array.isArray(cleaned) && Object.keys(cleaned).length === 0 && !preserveEmpty && cleaned[KEEP_EMPTY_OBJECT] !== true) continue; + out[key] = cleaned; + } + if (preserveEmpty) out[KEEP_EMPTY_OBJECT] = true; + return out; + } + if (value === undefined || value === null || value === '') return undefined; + return value; +}; + +const parseListResponse = (result) => { + const parsed = tryParseJson(result.body); + const rawJson = parsed.ok ? parsed.value : undefined; + const results = inferResultsArray(rawJson); + return { + http_status: result.httpStatus, + raw_body: result.body, + count: inferCount(rawJson, results.length), + results: results.map((item) => toValue(item)), + raw_json: toValue(rawJson), + }; +}; + +const parseObjectResponse = (result) => { + const parsed = tryParseJson(result.body); + return { + http_status: result.httpStatus, + raw_body: result.body, + raw_json: toValue(parsed.ok ? parsed.value : undefined), + }; +}; + +const parseBlobResponse = (result) => { + const parsed = tryParseJson(result.body); + return { + http_status: result.httpStatus, + raw_body: result.body, + content_type: result.headers.get?.('content-type') || '', + filename: parseFilename(result.headers.get?.('content-disposition') || ''), + raw_json: toValue(parsed.ok ? parsed.value : undefined), + }; +}; + +const fetchUpstream = async (url, ctx = {}, options = {}) => { + const dispatcher = buildDispatcher(ctx.bindings || {}); + const timeoutMs = resolveTimeoutMs(ctx); + const timeout = makeTimeoutSignal(timeoutMs); + try { + const response = await fetch(url, { + method: options.method || 'GET', + headers: buildRequestHeaders(ctx, options.headers || {}), + body: options.body, + signal: timeout.signal, + dispatcher, + }); + const body = await response.text(); + if (response.status >= 400) { + const code = response.status === 401 || response.status === 403 + ? 'PERMISSION_DENIED' + : response.status >= 500 + ? 'UNAVAILABLE' + : 'FAILED_PRECONDITION'; + throw errorWithCode(code, `API7 request failed with HTTP ${response.status}: ${body || '(empty body)'}`); + } + return { + httpStatus: response.status, + body, + headers: response.headers, + }; + } catch (error) { + if (error instanceof GrpcError) throw error; + if (error?.name === 'AbortError') throw errorWithCode('UNAVAILABLE', `API7 request timed out after ${timeoutMs}ms`); + throw errorWithCode('UNAVAILABLE', `API7 request failed: ${error?.message || error}`); + } finally { + timeout.clear(); + await dispatcher?.close?.(); + } +}; + +const addPagingQuery = (req, query) => { + const page = toOptionalInt(reqField(req, 'page', 'page'), { min: 1 }); + const pageSize = toOptionalInt(reqField(req, 'page_size', 'pageSize'), { min: 1 }); + const direction = toTrimmedString(reqField(req, 'direction', 'direction')); + const orderBy = toTrimmedString(reqField(req, 'order_by', 'orderBy')); + const search = toTrimmedString(reqField(req, 'search', 'search')); + if (page !== undefined) query.page = page; + if (pageSize !== undefined) query.page_size = pageSize; + if (direction) query.direction = direction; + if (orderBy) query.order_by = orderBy; + if (search) query.search = search; + return query; +}; + +const gatewayGroupQuery = (req, ctx) => { + const value = toTrimmedString(reqField(req, 'gateway_group_id', 'gatewayGroupId')) || resolveDefaultGatewayGroupId(ctx.bindings || {}); + return value ? { gateway_group_id: value } : {}; +}; + +const withLabels = (req, query) => { + const labels = req.labels && typeof req.labels === 'object' ? toStringMap(req.labels) : {}; + if (Object.keys(labels).length > 0) query.labels = labels; + return query; +}; + +const operation = (method, responseKind, buildRequest) => ({ method, responseKind, buildRequest }); + +const OPERATIONS = { + ListAuditLogs: operation('GET', 'list', (req, ctx) => ({ + path: '/api/audit_logs', + query: addPagingQuery(req, { + event_type: toTrimmedString(reqField(req, 'event_type', 'eventType')), + operator_id: toTrimmedString(reqField(req, 'operator_id', 'operatorId')), + gateway_group_id: toTrimmedString(reqField(req, 'gateway_group_id', 'gatewayGroupId')), + resource_id: toTrimmedString(reqField(req, 'resource_id', 'resourceId')), + start_at: toOptionalInt(reqField(req, 'start_at', 'startAt')), + end_at: toOptionalInt(reqField(req, 'end_at', 'endAt')), + }), + })), + ExportAuditLogs: operation('GET', 'blob', (req) => ({ + path: '/api/audit_logs/export', + query: { + format: requireString(reqField(req, 'format', 'format'), 'format'), + event_type: toTrimmedString(reqField(req, 'event_type', 'eventType')), + operator_id: toTrimmedString(reqField(req, 'operator_id', 'operatorId')), + gateway_group_id: toTrimmedString(reqField(req, 'gateway_group_id', 'gatewayGroupId')), + resource_id: toTrimmedString(reqField(req, 'resource_id', 'resourceId')), + start_at: toOptionalInt(reqField(req, 'start_at', 'startAt')), + end_at: toOptionalInt(reqField(req, 'end_at', 'endAt')), + }, + })), + ListAlertPolicies: operation('GET', 'list', (req) => ({ + path: '/api/alert/policies', + query: withLabels(req, addPagingQuery(req, { + status: toStringArray(req.status), + severity: toStringArray(req.severity), + })), + })), + ListAlertHistories: operation('GET', 'list', (req) => ({ + path: '/api/alert/policies/histories', + query: addPagingQuery(req, { + alert_policy_id: toTrimmedString(reqField(req, 'alert_policy_id', 'alertPolicyId')), + severity: toStringArray(req.severity), + start_at: toOptionalInt(reqField(req, 'start_at', 'startAt')), + end_at: toOptionalInt(reqField(req, 'end_at', 'endAt')), + gateway_group_id: toTrimmedString(reqField(req, 'gateway_group_id', 'gatewayGroupId')), + }), + })), + ListUsers: operation('GET', 'list', (req) => ({ + path: '/api/users', + query: addPagingQuery(req, { roles: toStringArray(req.roles) }), + })), + ListRoles: operation('GET', 'list', (req) => ({ + path: '/api/roles', + query: withLabels(req, addPagingQuery(req, {})), + })), + ListTokens: operation('GET', 'list', (req) => ({ path: '/api/tokens', query: addPagingQuery(req, {}) })), + ListConsumers: operation('GET', 'list', (req, ctx) => ({ + path: '/apisix/admin/consumers', + query: withLabels(req, addPagingQuery(req, gatewayGroupQuery(req, ctx))), + })), + ListConsumerCredentials: operation('GET', 'list', (req, ctx) => ({ + path: fillPath('/apisix/admin/consumers/{username}/credentials', { + username: requireString(reqField(req, 'username', 'username'), 'username'), + }), + query: withLabels(req, addPagingQuery(req, { + ...gatewayGroupQuery(req, ctx), + plugin_name: toTrimmedString(reqField(req, 'plugin_name', 'pluginName')), + })), + })), + ListCertificates: operation('GET', 'list', (req, ctx) => ({ + path: '/apisix/admin/certificates', + query: withLabels(req, addPagingQuery(req, { + ...gatewayGroupQuery(req, ctx), + sni_id: toTrimmedString(reqField(req, 'sni_id', 'sniId')), + sni_name: toTrimmedString(reqField(req, 'sni_name', 'sniName')), + exptime: toOptionalInt(reqField(req, 'exptime', 'exptime')), + })), + })), + ListCACertificates: operation('GET', 'list', (req, ctx) => ({ + path: '/apisix/admin/ca_certificates', + query: withLabels(req, addPagingQuery(req, { + ...gatewayGroupQuery(req, ctx), + sni_id: toTrimmedString(reqField(req, 'sni_id', 'sniId')), + sni_name: toTrimmedString(reqField(req, 'sni_name', 'sniName')), + exptime: toOptionalInt(reqField(req, 'exptime', 'exptime')), + })), + })), + ListSNIs: operation('GET', 'list', (req, ctx) => ({ + path: '/apisix/admin/snis', + query: withLabels(req, addPagingQuery(req, { + ...gatewayGroupQuery(req, ctx), + domain: toTrimmedString(reqField(req, 'domain', 'domain')), + mtls_enabled: toOptionalBool(reqField(req, 'mtls_enabled', 'mtlsEnabled')), + })), + })), + GetCertificateUsage: operation('GET', 'list', (req) => ({ + path: fillPath('/api/gateway_groups/{gateway_group_id}/certificates/{certificate_id}/usage', { + gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId'), 'gateway_group_id'), + certificate_id: requireString(reqField(req, 'certificate_id', 'certificateId'), 'certificate_id'), + }), + query: addPagingQuery(req, { + resource_type: toTrimmedString(reqField(req, 'resource_type', 'resourceType')), + }), + })), + ListGatewayGroups: operation('GET', 'list', (req) => ({ + path: '/api/gateway_groups', + query: withLabels(req, addPagingQuery(req, { name: toTrimmedString(req.name) })), + })), + ListGatewayInstances: operation('GET', 'list', (req) => ({ + path: fillPath('/api/gateway_groups/{gateway_group_id}/instances', { + gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId'), 'gateway_group_id'), + }), + query: addPagingQuery(req, { + status: toTrimmedString(req.status), + compatibility: toTrimmedString(req.compatibility), + }), + })), + ListRoutes: operation('GET', 'list', (req, ctx) => ({ + path: '/apisix/admin/routes', + query: addPagingQuery(req, { + ...gatewayGroupQuery(req, ctx), + service_id: requireString(reqField(req, 'service_id', 'serviceId'), 'service_id'), + with_publish_info: toOptionalBool(reqField(req, 'with_publish_info', 'withPublishInfo')), + }), + })), + ListServices: operation('GET', 'list', (req, ctx) => ({ + path: '/apisix/admin/services', + query: withLabels(req, addPagingQuery(req, { + ...gatewayGroupQuery(req, ctx), + unhealthy_nodes: toOptionalBool(reqField(req, 'unhealthy_nodes', 'unhealthyNodes')), + hosts: toTrimmedString(req.hosts), + type: toTrimmedString(req.type), + with_publish_info: toOptionalBool(reqField(req, 'with_publish_info', 'withPublishInfo')), + })), + })), + ListGlobalRules: operation('GET', 'list', (req, ctx) => ({ + path: '/apisix/admin/global_rules', + query: addPagingQuery(req, gatewayGroupQuery(req, ctx)), + })), + ListPlugins: operation('GET', 'object', (req) => ({ + path: '/apisix/admin/plugins', + query: { subsystem: toTrimmedString(req.subsystem) }, + })), + GetPluginSchema: operation('GET', 'object', (req) => ({ + path: fillPath('/apisix/admin/schema/plugins/{plugin_name}', { + plugin_name: requireString(reqField(req, 'plugin_name', 'pluginName'), 'plugin_name'), + }), + query: { subsystem: toTrimmedString(req.subsystem) }, + })), + ListApprovals: operation('GET', 'list', (req) => ({ + path: '/api/approvals', + query: addPagingQuery(req, { + status: toTrimmedString(req.status), + result: toTrimmedString(req.result), + event: toTrimmedString(req.event), + resource_type: toTrimmedString(reqField(req, 'resource_type', 'resourceType')), + resource_name: toTrimmedString(reqField(req, 'resource_name', 'resourceName')), + operator_name: toTrimmedString(reqField(req, 'operator_name', 'operatorName')), + applicant_name: toTrimmedString(reqField(req, 'applicant_name', 'applicantName')), + }), + })), + ListSecretProviders: operation('GET', 'list', (req, ctx) => ({ + path: '/apisix/admin/secret_providers', + query: addPagingQuery(req, gatewayGroupQuery(req, ctx)), + })), + GetSecretProviderUsage: operation('GET', 'list', (req) => ({ + path: fillPath('/api/gateway_groups/{gateway_group_id}/secret_providers/{secret_provider}/{secret_provider_id}/usage', { + gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId'), 'gateway_group_id'), + secret_provider: requireString(reqField(req, 'secret_provider', 'secretProvider'), 'secret_provider'), + secret_provider_id: requireString(reqField(req, 'secret_provider_id', 'secretProviderId'), 'secret_provider_id'), + }), + query: addPagingQuery(req, { + resource_type: toTrimmedString(reqField(req, 'resource_type', 'resourceType')), + }), + })), + ParseCertificate: operation('PUT', 'object', (req) => ({ + path: '/api/parse_certificate', + body: { cert: requireString(req.cert, 'cert') }, + })), + ValidateCertificateKey: operation('PUT', 'object', (req) => ({ + path: '/api/validate_cert_key', + body: { + cert: requireString(req.cert, 'cert'), + key: requireString(req.key, 'key'), + }, + })), + ListDebugSessions: operation('GET', 'list', (req) => ({ + path: fillPath('/api/gateway_groups/{gateway_group_id}/debug_sessions', { + gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId'), 'gateway_group_id'), + }), + query: addPagingQuery(req, {}), + })), + ListDebugTraces: operation('GET', 'list', (req) => ({ + path: fillPath('/api/gateway_groups/{gateway_group_id}/debug_sessions/{debug_session_id}/traces', { + gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId'), 'gateway_group_id'), + debug_session_id: requireString(reqField(req, 'debug_session_id', 'debugSessionId'), 'debug_session_id'), + }), + })), + GetServiceHealthcheck: operation('GET', 'object', (req) => ({ + path: fillPath('/api/gateway_groups/{gateway_group_id}/services/{apisix_service_id}/healthcheck', { + gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId'), 'gateway_group_id'), + apisix_service_id: requireString(reqField(req, 'apisix_service_id', 'apisixServiceId'), 'apisix_service_id'), + }), + query: { + upstream_id: toTrimmedString(reqField(req, 'upstream_id', 'upstreamId')), + }, + })), + + CreateToken: operation('POST', 'object', (req) => ({ + path: '/api/tokens', + body: { + name: requireString(req.name, 'name'), + expires_at: firstDefined(toOptionalInt(reqField(req, 'expires_at', 'expiresAt')), Number(unwrapScalar(reqField(req, 'expires_at', 'expiresAt')))), + }, + })), + CreateConsumer: operation('POST', 'object', (req, ctx) => ({ + path: '/apisix/admin/consumers', + query: { gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId') || resolveDefaultGatewayGroupId(ctx.bindings || {}), 'gateway_group_id') }, + body: { + username: requireString(req.username, 'username'), + desc: toTrimmedString(req.desc), + labels: toStringMap(req.labels), + plugins: preserveEmptyObjectTree(structToPlainObject(req.plugins)), + }, + })), + CreateConsumerCredential: operation('POST', 'object', (req, ctx) => ({ + path: fillPath('/apisix/admin/consumers/{username}/credentials', { + username: requireString(req.username, 'username'), + }), + query: { gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId') || resolveDefaultGatewayGroupId(ctx.bindings || {}), 'gateway_group_id') }, + body: { + name: requireString(req.name, 'name'), + desc: toTrimmedString(req.desc), + labels: toStringMap(req.labels), + plugins: preserveEmptyObjectTree(structToPlainObject(req.plugins) ?? {}), + }, + })), + CreateCertificate: operation('POST', 'object', (req, ctx) => ({ + path: '/apisix/admin/certificates', + query: { gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId') || resolveDefaultGatewayGroupId(ctx.bindings || {}), 'gateway_group_id') }, + body: { + name: toTrimmedString(req.name), + desc: toTrimmedString(req.desc), + labels: toStringMap(req.labels), + cert: requireString(req.cert, 'cert'), + key: requireString(req.key, 'key'), + }, + })), + CreateSNI: operation('POST', 'object', (req, ctx) => ({ + path: '/apisix/admin/snis', + query: { gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId') || resolveDefaultGatewayGroupId(ctx.bindings || {}), 'gateway_group_id') }, + body: { + name: toTrimmedString(req.name), + desc: toTrimmedString(req.desc), + domain: requireString(req.domain, 'domain'), + certificates: toStringArray(req.certificates), + mtls: structToPlainObject(req.mtls), + plugins: preserveEmptyObjectTree(structToPlainObject(req.plugins)), + }, + })), + CreateService: operation('POST', 'object', (req, ctx) => { + const upstreamNodes = firstDefined(req.upstream_nodes, req.upstreamNodes); + const normalizedUpstreamNodes = (Array.isArray(upstreamNodes) ? upstreamNodes : []).map((node) => ({ + host: requireString(node.host, 'upstream_nodes.host'), + port: requireNumber(node.port, 'upstream_nodes.port'), + weight: requireNumber(node.weight, 'upstream_nodes.weight'), + ...(toOptionalInt(node.priority) !== undefined ? { priority: toOptionalInt(node.priority) } : {}), + })); + if (normalizedUpstreamNodes.length === 0) { + throw errorWithCode('INVALID_ARGUMENT', 'CreateService requires upstream_nodes/upstreamNodes with at least one node'); + } + return ({ + path: '/apisix/admin/services', + query: { gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId') || resolveDefaultGatewayGroupId(ctx.bindings || {}), 'gateway_group_id') }, + body: { + name: requireString(req.name, 'name'), + desc: toTrimmedString(req.desc), + labels: toStringMap(req.labels), + type: requireString(req.type || 'http', 'type'), + hosts: toStringArray(req.hosts), + path_prefix: toTrimmedString(reqField(req, 'path_prefix', 'pathPrefix')), + strip_path_prefix: toOptionalBool(reqField(req, 'strip_path_prefix', 'stripPathPrefix')), + plugins: preserveEmptyObjectTree(structToPlainObject(req.plugins)), + upstream: { + scheme: toTrimmedString(reqField(req, 'upstream_scheme', 'upstreamScheme')) || 'http', + pass_host: toTrimmedString(reqField(req, 'upstream_pass_host', 'upstreamPassHost')) || 'pass', + upstream_host: toTrimmedString(reqField(req, 'upstream_host', 'upstreamHost')), + nodes: normalizedUpstreamNodes, + }, + }, + }); + }), + CreateRoute: operation('POST', 'object', (req, ctx) => ({ + path: '/apisix/admin/routes', + query: { gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId') || resolveDefaultGatewayGroupId(ctx.bindings || {}), 'gateway_group_id') }, + body: { + service_id: requireString(reqField(req, 'service_id', 'serviceId'), 'service_id'), + name: requireString(req.name, 'name'), + desc: toTrimmedString(req.desc), + labels: toStringMap(req.labels), + methods: toStringArray(req.methods), + paths: toStringArray(req.paths), + priority: toOptionalInt(req.priority) ?? 0, + enable_websocket: toOptionalBool(reqField(req, 'enable_websocket', 'enableWebsocket')), + plugins: preserveEmptyObjectTree(structToPlainObject(req.plugins)), + timeout: structToPlainObject(req.timeout), + vars: listValueToPlainArray(req.vars), + }, + })), + CreateGlobalRule: operation('POST', 'object', (req, ctx) => ({ + path: '/apisix/admin/global_rules', + query: { gateway_group_id: requireString(reqField(req, 'gateway_group_id', 'gatewayGroupId') || resolveDefaultGatewayGroupId(ctx.bindings || {}), 'gateway_group_id') }, + body: { + plugins: preserveEmptyObjectTree(structToPlainObject(req.plugins) ?? {}), + }, + })), +}; + +const executeOperation = async (name, req = {}, ctx = {}) => { + const callCtx = resolveCallContext(ctx); + const op = OPERATIONS[name]; + if (!op) throw errorWithCode('UNKNOWN', `unsupported API7 operation: ${name}`); + const built = op.buildRequest(req, callCtx); + const url = buildUrl(requireBaseUrl(callCtx), built.path, built.query); + const result = await fetchUpstream(url, callCtx, { + method: op.method, + body: built.body ? JSON.stringify(sanitizeObject(built.body)) : undefined, + headers: built.body ? { 'Content-Type': 'application/json' } : {}, + }); + if (op.responseKind === 'blob') return parseBlobResponse(result); + if (op.responseKind === 'object') return parseObjectResponse(result); + return parseListResponse(result); +}; + +export function rpcdef(ctx = {}) { + const callCtx = resolveCallContext(ctx); + return Object.fromEntries(METHOD_SPECS.map(([name]) => [METHOD_PATHS[name], async (req) => executeOperation(name, req ?? callCtx.req ?? {}, callCtx)])); +} + +const wrapHandler = (name) => async (reqOrCtx = {}, maybeCtx) => { + const { req, ctx } = resolveHandlerArgs(reqOrCtx, maybeCtx); + return executeOperation(name, req, ctx); +}; + +export const handlers = Object.fromEntries(METHOD_SPECS.map(([name]) => [METHOD_FULLS[name], wrapHandler(name)])); + +export const _test = { + addPagingQuery, + buildDispatcher, + buildRequestHeaders, + buildUrl, + encodeQueryPairs, + fillPath, + inferCount, + inferResultsArray, + mergedBindings, + normalizeBaseUrl, + parseHeaders, + resolveApiKey, + resolveBaseUrl, + resolveDefaultGatewayGroupId, + resolveUsername, + resolvePassword, + toOptionalBool, + toOptionalInt, + toStringArray, + toStringMap, + toTrimmedString, + structToPlainObject, + listValueToPlainArray, + sanitizeObject, +}; diff --git a/services/api7__enterprise_v3-10-2/src/service.js b/services/api7__enterprise_v3-10-2/src/service.js new file mode 100644 index 00000000..5dd43587 --- /dev/null +++ b/services/api7__enterprise_v3-10-2/src/service.js @@ -0,0 +1,7 @@ +import { defineService } from '@chaitin-ai/octobus-sdk'; + +import { handlers } from './api7-enterprise-v3-10-2.js'; + +export { handlers } from './api7-enterprise-v3-10-2.js'; + +export const service = defineService({ handlers }); diff --git a/services/api7__enterprise_v3-10-2/test/api7-enterprise-v3-10-2.test.js b/services/api7__enterprise_v3-10-2/test/api7-enterprise-v3-10-2.test.js new file mode 100644 index 00000000..474b2993 --- /dev/null +++ b/services/api7__enterprise_v3-10-2/test/api7-enterprise-v3-10-2.test.js @@ -0,0 +1,364 @@ +import test from 'node:test'; +import assert from 'node:assert/strict'; + +const method = (name) => `/API7_Enterprise_V3_10_2.API7_Enterprise_V3_10_2/${name}`; + +const buildCtx = (req = {}, overrides = {}) => ({ + bindings: { + api7_base_url: 'https://api7.example.test', + api7_api_key: 'secret-key', + gateway_group_id: 'gw-default', + headers: { 'X-Test': 'yes' }, + ...overrides.bindings, + }, + limits: { timeoutMs: 10_000, ...overrides.limits }, + meta: { instance_id: 'inst', request_id: 'req', ...overrides.meta }, + req, +}); + +const loadHandler = async (name, req, overrides = {}) => { + const { rpcdef } = await import('../src/api7-enterprise-v3-10-2.js'); + const ctx = buildCtx(req, overrides); + return rpcdef(ctx)[method(name)]; +}; + +const setFetch = (impl) => { + global.fetch = async (...args) => impl(...args); +}; + +const response = (status, body, headers = {}) => ({ + status, + headers: { get: (name) => headers[name.toLowerCase()] ?? headers[name] ?? null }, + text: async () => body, +}); + +test('helpers normalize bindings, auth, query strings, and TLS options', async () => { + const { _test } = await import('../src/api7-enterprise-v3-10-2.js'); + + assert.deepEqual(_test.mergedBindings({ + config: { api7_base_url: 'https://config', keep: 'config' }, + secret: { api7_api_key: 'secret' }, + bindings: { api7_base_url: 'https://binding' }, + }), { + api7_base_url: 'https://binding', + keep: 'config', + api7_api_key: 'secret', + }); + assert.equal(_test.normalizeBaseUrl('https://api7.example.test/'), 'https://api7.example.test'); + assert.equal(_test.resolveApiKey({ apiKey: 'abc' }), 'abc'); + assert.equal(_test.resolveDefaultGatewayGroupId({ gatewayGroupId: 'gw-1' }), 'gw-1'); + assert.deepEqual(_test.toStringArray(['a', ' b ']), ['a', 'b']); + assert.deepEqual(_test.toStringMap({ env: 'prod', empty: '' }), { env: 'prod' }); + assert.equal(_test.encodeQueryPairs({ status: ['enabled', 'disabled'], labels: { env: 'prod' } }), 'status=enabled&status=disabled&labels%5Benv%5D=prod'); + assert.equal(_test.buildUrl('https://api7.example.test/', '/api/audit_logs', { page: 1 }), 'https://api7.example.test/api/audit_logs?page=1'); + assert.deepEqual(_test.buildRequestHeaders(buildCtx()), { + Accept: 'application/json', + 'X-Test': 'yes', + 'X-API-KEY': 'secret-key', + }); + assert.equal(_test.buildDispatcher({}), undefined); + const dispatcher = _test.buildDispatcher({ skipTlsVerify: true }); + assert.ok(dispatcher); + await dispatcher.close(); +}); + +test('ListAuditLogs forwards filters and maps list results', async () => { + let captured; + setFetch(async (url, init) => { + captured = { url, init }; + return response(200, JSON.stringify({ total: 1, list: [{ id: 'audit-1', event_type: 'route.update' }] })); + }); + + const handler = await loadHandler('ListAuditLogs', { + event_type: 'route.update', + operator_id: 'user-1', + page_size: { value: 20 }, + page: { value: 2 }, + }); + const res = await handler(); + + assert.equal(captured.url, 'https://api7.example.test/api/audit_logs?event_type=route.update&operator_id=user-1&page=2&page_size=20'); + assert.equal(captured.init.method, 'GET'); + assert.equal(captured.init.headers['X-API-KEY'], 'secret-key'); + assert.equal(res.count, 1); + assert.equal(res.results[0].structValue.fields.id.stringValue, 'audit-1'); +}); + +test('ListConsumers uses default gateway group binding when request omits it', async () => { + let capturedUrl; + setFetch(async (url) => { + capturedUrl = url; + return response(200, JSON.stringify({ total_size: 0, list: [] })); + }); + + const handler = await loadHandler('ListConsumers', { search: 'demo' }); + await handler(); + + assert.equal(capturedUrl, 'https://api7.example.test/apisix/admin/consumers?gateway_group_id=gw-default&search=demo'); +}); + +test('ListConsumerCredentials requires username and supports lowerCamelCase fields', async () => { + const badHandler = await loadHandler('ListConsumerCredentials', {}); + await assert.rejects(() => badHandler(), /username is required/); + + let capturedUrl; + setFetch(async (url) => { + capturedUrl = url; + return response(200, JSON.stringify({ count: 1, items: [{ id: 'cred-1' }] })); + }); + + const handler = await loadHandler('ListConsumerCredentials', { + username: 'alice', + pluginName: 'key-auth', + gatewayGroupId: 'gw-1', + }); + const res = await handler(); + + assert.equal(capturedUrl, 'https://api7.example.test/apisix/admin/consumers/alice/credentials?gateway_group_id=gw-1&plugin_name=key-auth'); + assert.equal(res.results[0].structValue.fields.id.stringValue, 'cred-1'); +}); + +test('ExportAuditLogs preserves content type and filename metadata', async () => { + setFetch(async () => response(200, 'id,event\n1,route.update\n', { + 'content-type': 'text/csv', + 'content-disposition': 'attachment; filename="audit.csv"', + })); + + const handler = await loadHandler('ExportAuditLogs', { format: 'csv' }); + const res = await handler(); + + assert.equal(res.content_type, 'text/csv'); + assert.equal(res.filename, 'audit.csv'); + assert.match(res.raw_body, /route.update/); +}); + +test('GetPluginSchema builds path params and supports subsystem filter', async () => { + let capturedUrl; + setFetch(async (url) => { + capturedUrl = url; + return response(200, JSON.stringify({ properties: { key: { type: 'string' } } })); + }); + + const handler = await loadHandler('GetPluginSchema', { pluginName: 'key-auth', subsystem: 'http' }); + const res = await handler(); + + assert.equal(capturedUrl, 'https://api7.example.test/apisix/admin/schema/plugins/key-auth?subsystem=http'); + assert.equal(res.raw_json.structValue.fields.properties.structValue.fields.key.structValue.fields.type.stringValue, 'string'); +}); + + +test('ExportAuditLogs falls back when Content-Disposition filename is not URI-encoded', async () => { + setFetch(async () => response(200, 'id,event\n1,route.update\n', { + 'content-type': 'text/csv', + 'content-disposition': 'attachment; filename="report_100%.csv"', + })); + + const handler = await loadHandler('ExportAuditLogs', { format: 'csv' }); + const res = await handler(); + + assert.equal(res.filename, 'report_100%.csv'); +}); + +test('ParseCertificate and ValidateCertificateKey send JSON bodies', async () => { + const captured = []; + setFetch(async (url, init) => { + captured.push({ url, init, body: init.body ? JSON.parse(init.body) : undefined }); + return response(200, JSON.stringify({ ok: true })); + }); + + const parseHandler = await loadHandler('ParseCertificate', { cert: '---CERT---' }); + await parseHandler(); + const validateHandler = await loadHandler('ValidateCertificateKey', { cert: '---CERT---', key: '---KEY---' }); + await validateHandler(); + + assert.equal(captured[0].url, 'https://api7.example.test/api/parse_certificate'); + assert.equal(captured[0].init.method, 'PUT'); + assert.deepEqual(captured[0].body, { cert: '---CERT---' }); + assert.equal(captured[1].url, 'https://api7.example.test/api/validate_cert_key'); + assert.deepEqual(captured[1].body, { cert: '---CERT---', key: '---KEY---' }); +}); + +test('service supports Basic Auth when API key is absent', async () => { + let authHeader = ''; + setFetch(async (url, init) => { + authHeader = init.headers.Authorization; + return response(200, JSON.stringify([])); + }); + + const handler = await loadHandler('ListPlugins', {}, { + bindings: { + api7_base_url: 'https://api7.example.test', + username: 'admin', + password: 'pass', + api7_api_key: '', + }, + }); + await handler(); + + assert.equal(authHeader, `Basic ${Buffer.from('admin:pass').toString('base64')}`); +}); + +test('GetServiceHealthcheck requires path parameters and forwards upstream_id', async () => { + let capturedUrl; + setFetch(async (url) => { + capturedUrl = url; + return response(200, JSON.stringify({ status: 'healthy' })); + }); + + const handler = await loadHandler('GetServiceHealthcheck', { + gatewayGroupId: 'gw-1', + apisixServiceId: 'svc-1', + upstreamId: 'upstream-1', + }); + const res = await handler(); + + assert.equal(capturedUrl, 'https://api7.example.test/api/gateway_groups/gw-1/services/svc-1/healthcheck?upstream_id=upstream-1'); + assert.equal(res.raw_json.structValue.fields.status.stringValue, 'healthy'); +}); + +test('ListRoutes now requires service_id', async () => { + const handler = await loadHandler('ListRoutes', { gatewayGroupId: 'default' }); + await assert.rejects(() => handler(), /service_id is required/); +}); + +test('CreateToken sends POST body', async () => { + let captured; + setFetch(async (url, init) => { + captured = { url, init, body: JSON.parse(init.body) }; + return response(200, JSON.stringify({ id: 'token-1', name: 'octobus-test-token' })); + }); + + const handler = await loadHandler('CreateToken', { name: 'octobus-test-token', expiresAt: 0 }); + const res = await handler(); + + assert.equal(captured.url, 'https://api7.example.test/api/tokens'); + assert.equal(captured.init.method, 'POST'); + assert.deepEqual(captured.body, { name: 'octobus-test-token', expires_at: 0 }); + assert.equal(res.raw_json.structValue.fields.name.stringValue, 'octobus-test-token'); +}); + +test('CreateConsumer uses default gateway group and sends labels/plugins', async () => { + let captured; + setFetch(async (url, init) => { + captured = { url, init, body: JSON.parse(init.body) }; + return response(200, JSON.stringify({ id: 'consumer-1', username: 'octobus-user' })); + }); + + const handler = await loadHandler('CreateConsumer', { + username: 'octobus-user', + desc: 'test consumer', + labels: { env: 'test' }, + plugins: { 'key-auth': {} }, + }); + await handler(); + + assert.equal(captured.url, 'https://api7.example.test/apisix/admin/consumers?gateway_group_id=gw-default'); + assert.equal(captured.init.method, 'POST'); + assert.deepEqual(captured.body, { + username: 'octobus-user', + desc: 'test consumer', + labels: { env: 'test' }, + plugins: { 'key-auth': {} }, + }); +}); + + + +test('CreateConsumerCredential preserves empty plugins object', async () => { + let captured; + setFetch(async (url, init) => { + captured = { url, init, body: JSON.parse(init.body) }; + return response(200, JSON.stringify({ id: 'cred-1', name: 'cred-name' })); + }); + + const handler = await loadHandler('CreateConsumerCredential', { + gatewayGroupId: 'default', + username: 'alice', + name: 'cred-name', + }); + await handler(); + + assert.equal(captured.url, 'https://api7.example.test/apisix/admin/consumers/alice/credentials?gateway_group_id=default'); + assert.deepEqual(captured.body, { + name: 'cred-name', + plugins: {}, + }); +}); + +test('CreateGlobalRule preserves empty plugins object', async () => { + let captured; + setFetch(async (url, init) => { + captured = { url, init, body: JSON.parse(init.body) }; + return response(200, JSON.stringify({ id: 'global-rule-1' })); + }); + + const handler = await loadHandler('CreateGlobalRule', { + gatewayGroupId: 'default', + }); + await handler(); + + assert.equal(captured.url, 'https://api7.example.test/apisix/admin/global_rules?gateway_group_id=default'); + assert.deepEqual(captured.body, { + plugins: {}, + }); +}); + +test('CreateService accepts camelCase upstreamNodes and sends upstream.nodes', async () => { + let captured; + setFetch(async (url, init) => { + captured = { url, init, body: JSON.parse(init.body) }; + return response(200, JSON.stringify({ id: 'svc-1', name: 'octobus-svc' })); + }); + + const handler = await loadHandler('CreateService', { + gatewayGroupId: 'default', + name: 'octobus-svc', + type: 'http', + hosts: ['svc.example.test'], + upstreamScheme: 'http', + upstreamPassHost: 'pass', + upstreamNodes: [{ host: '127.0.0.1', port: 8080, weight: 100 }], + }); + await handler(); + + assert.equal(captured.url, 'https://api7.example.test/apisix/admin/services?gateway_group_id=default'); + assert.equal(captured.init.method, 'POST'); + assert.deepEqual(captured.body, { + name: 'octobus-svc', + type: 'http', + hosts: ['svc.example.test'], + upstream: { + scheme: 'http', + pass_host: 'pass', + nodes: [{ host: '127.0.0.1', port: 8080, weight: 100 }], + }, + }); +}); + +test('CreateRoute sends required service_id, name, and paths', async () => { + let captured; + setFetch(async (url, init) => { + captured = { url, init, body: JSON.parse(init.body) }; + return response(200, JSON.stringify({ id: 'route-1', name: 'octobus-route' })); + }); + + const handler = await loadHandler('CreateRoute', { + gatewayGroupId: 'default', + serviceId: 'svc-1', + name: 'octobus-route', + paths: ['/octobus'], + methods: ['GET'], + priority: 10, + }); + await handler(); + + assert.equal(captured.url, 'https://api7.example.test/apisix/admin/routes?gateway_group_id=default'); + assert.equal(captured.init.method, 'POST'); + assert.deepEqual(captured.body, { + service_id: 'svc-1', + name: 'octobus-route', + methods: ['GET'], + paths: ['/octobus'], + priority: 10, + }); +});