chore: add missing source modules to version control
Deploy to Production / deploy (push) Failing after 7s
Deploy to Production / deploy (push) Failing after 7s
apix-demo, apix-portal/src, apix-spider/src, apix-registry/src, apix-common/src were never staged. Without them the CI build has no source to compile and the Docker images cannot be produced. Also adds docs/ (infrastructure notes) missed in prior commits. Co-Authored-By: Mira <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
|
||||
https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>org.botstandards</groupId>
|
||||
<artifactId>apix-parent</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>apix-demo</artifactId>
|
||||
<name>APIX :: Demo</name>
|
||||
<description>Sandbox-scoped mock service layer. Each sandbox configures realistic endpoints
|
||||
with declared latency, rate limits, and APX pricing. Powers the APIX demo ecosystem
|
||||
and third-party training environments.</description>
|
||||
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.botstandards</groupId>
|
||||
<artifactId>apix-common</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- REST server -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Persistence — owns demo_config + mock_service_configs tables -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-hibernate-orm-panache</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-jdbc-postgresql</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-liquibase</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Rate-limit bucket reset + cache invalidation -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-scheduler</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- Calls registry API to seed sandbox and register services -->
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-rest-client-jackson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-smallrye-health</artifactId>
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
<groupId>io.quarkus</groupId>
|
||||
<artifactId>quarkus-junit5</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
<build>
|
||||
<plugins>
|
||||
<plugin>
|
||||
<groupId>${quarkus.platform.group-id}</groupId>
|
||||
<artifactId>quarkus-maven-plugin</artifactId>
|
||||
<extensions>true</extensions>
|
||||
<executions>
|
||||
<execution>
|
||||
<goals>
|
||||
<goal>build</goal>
|
||||
<goal>generate-code</goal>
|
||||
<goal>generate-code-tests</goal>
|
||||
</goals>
|
||||
</execution>
|
||||
</executions>
|
||||
</plugin>
|
||||
</plugins>
|
||||
</build>
|
||||
</project>
|
||||
@@ -0,0 +1,34 @@
|
||||
package org.botstandards.apix.demo.client;
|
||||
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import org.botstandards.apix.common.BsmPayload;
|
||||
import org.botstandards.apix.demo.client.dto.RegistrationRequest;
|
||||
import org.botstandards.apix.demo.client.dto.SandboxCreated;
|
||||
import org.eclipse.microprofile.rest.client.inject.RegisterRestClient;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
@RegisterRestClient(configKey = "registry")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public interface RegistryClient {
|
||||
|
||||
@POST
|
||||
@Path("/sandbox/register")
|
||||
SandboxCreated register(RegistrationRequest request);
|
||||
|
||||
@POST
|
||||
@Path("/sandbox/{uuid}/services")
|
||||
Map<String, Object> registerService(
|
||||
@PathParam("uuid") String uuid,
|
||||
@HeaderParam("X-Api-Key") String apiKey,
|
||||
BsmPayload payload);
|
||||
|
||||
@PATCH
|
||||
@Path("/sandbox/admin/{uuid}/tier")
|
||||
Map<String, Object> promoteTier(
|
||||
@PathParam("uuid") String uuid,
|
||||
@HeaderParam("X-Admin-Key") String adminKey,
|
||||
Map<String, String> body);
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
package org.botstandards.apix.demo.client.dto;
|
||||
|
||||
/** Minimal registration payload sent to POST /sandbox/register. */
|
||||
public record RegistrationRequest(String name, String contactEmail, String location) {}
|
||||
@@ -0,0 +1,13 @@
|
||||
package org.botstandards.apix.demo.client.dto;
|
||||
|
||||
import java.time.Instant;
|
||||
|
||||
/** Response from POST /sandbox/register — only the fields the seed service needs. */
|
||||
public record SandboxCreated(
|
||||
String sandboxId,
|
||||
String name,
|
||||
String apiKey,
|
||||
String maintenanceKey,
|
||||
String tier,
|
||||
Instant expiresAt
|
||||
) {}
|
||||
@@ -0,0 +1,17 @@
|
||||
package org.botstandards.apix.demo.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.time.Instant;
|
||||
|
||||
@Entity
|
||||
@Table(name = "demo_config")
|
||||
public class DemoConfigEntry {
|
||||
|
||||
@Id
|
||||
public String key;
|
||||
|
||||
public String value;
|
||||
|
||||
@Column(name = "updated_at", nullable = false)
|
||||
public Instant updatedAt;
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package org.botstandards.apix.demo.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.UUID;
|
||||
|
||||
@Entity
|
||||
@Table(name = "mock_service_configs")
|
||||
public class MockServiceConfig {
|
||||
|
||||
@Id
|
||||
@Column(columnDefinition = "uuid")
|
||||
public UUID id;
|
||||
|
||||
@Column(name = "sandbox_id", nullable = false)
|
||||
public String sandboxId;
|
||||
|
||||
/** Leading-slash path, e.g. /v1/address/validate */
|
||||
@Column(nullable = false)
|
||||
public String path;
|
||||
|
||||
@Column(nullable = false)
|
||||
public String method;
|
||||
|
||||
@Column(name = "latency_ms", nullable = false)
|
||||
public int latencyMs;
|
||||
|
||||
@Column(name = "jitter_pct", nullable = false)
|
||||
public int jitterPct;
|
||||
|
||||
@Column(name = "rate_per_minute", nullable = false)
|
||||
public int ratePerMinute;
|
||||
|
||||
@Column(name = "status_code", nullable = false)
|
||||
public int statusCode;
|
||||
|
||||
/** Static JSON response body returned verbatim on every call. */
|
||||
@Column(name = "response_body", nullable = false)
|
||||
public String responseBody;
|
||||
|
||||
/** APX cost surfaced in X-APX-Cost response header. */
|
||||
@Column(name = "price_apx", nullable = false)
|
||||
public BigDecimal priceApx;
|
||||
|
||||
@Column(name = "created_at", nullable = false)
|
||||
public Instant createdAt;
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
package org.botstandards.apix.demo.resource;
|
||||
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.ws.rs.*;
|
||||
import jakarta.ws.rs.core.MediaType;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
import org.botstandards.apix.demo.service.MockDispatcherService;
|
||||
|
||||
/**
|
||||
* Catches all requests to /{sandboxId}/{path} and routes them through the
|
||||
* sandbox-scoped mock dispatcher. Any sandbox owner can register custom
|
||||
* mock endpoints; the APIX demo sandbox is pre-seeded on first boot.
|
||||
*/
|
||||
@Path("/")
|
||||
@Produces(MediaType.APPLICATION_JSON)
|
||||
@Consumes(MediaType.APPLICATION_JSON)
|
||||
public class MockDispatcherResource {
|
||||
|
||||
@Inject
|
||||
MockDispatcherService dispatcher;
|
||||
|
||||
@GET
|
||||
@Path("/{sandboxId}/{path: .+}")
|
||||
public Response handleGet(@PathParam("sandboxId") String sandboxId,
|
||||
@PathParam("path") String path) {
|
||||
return dispatcher.dispatch(sandboxId, path, "GET");
|
||||
}
|
||||
|
||||
@POST
|
||||
@Path("/{sandboxId}/{path: .+}")
|
||||
public Response handlePost(@PathParam("sandboxId") String sandboxId,
|
||||
@PathParam("path") String path,
|
||||
String body) {
|
||||
return dispatcher.dispatch(sandboxId, path, "POST");
|
||||
}
|
||||
|
||||
@PUT
|
||||
@Path("/{sandboxId}/{path: .+}")
|
||||
public Response handlePut(@PathParam("sandboxId") String sandboxId,
|
||||
@PathParam("path") String path,
|
||||
String body) {
|
||||
return dispatcher.dispatch(sandboxId, path, "PUT");
|
||||
}
|
||||
|
||||
@PATCH
|
||||
@Path("/{sandboxId}/{path: .+}")
|
||||
public Response handlePatch(@PathParam("sandboxId") String sandboxId,
|
||||
@PathParam("path") String path,
|
||||
String body) {
|
||||
return dispatcher.dispatch(sandboxId, path, "PATCH");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,327 @@
|
||||
package org.botstandards.apix.demo.service;
|
||||
|
||||
import io.quarkus.logging.Log;
|
||||
import io.quarkus.runtime.StartupEvent;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.enterprise.event.Observes;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.transaction.Transactional;
|
||||
import org.botstandards.apix.common.BsmPayload;
|
||||
import org.botstandards.apix.common.OrgType;
|
||||
import org.botstandards.apix.common.ServiceStage;
|
||||
import org.botstandards.apix.demo.client.RegistryClient;
|
||||
import org.botstandards.apix.demo.client.dto.RegistrationRequest;
|
||||
import org.botstandards.apix.demo.client.dto.SandboxCreated;
|
||||
import org.botstandards.apix.demo.entity.DemoConfigEntry;
|
||||
import org.botstandards.apix.demo.entity.MockServiceConfig;
|
||||
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||
import org.eclipse.microprofile.rest.client.inject.RestClient;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
|
||||
/**
|
||||
* Seeds the APIX demo ecosystem on first boot.
|
||||
*
|
||||
* Flow:
|
||||
* 1. Create demo sandbox via registry API (FREE tier).
|
||||
* 2. Promote to DEMO tier via registry admin endpoint (never expires).
|
||||
* 3. Register all 17 services in the sandbox via registry API.
|
||||
* 4. Insert corresponding mock_service_configs (ON CONFLICT DO NOTHING).
|
||||
* 5. Mark seeded in demo_config.
|
||||
*
|
||||
* Subsequent boots are no-ops. Step 3 tolerates partial failures — each
|
||||
* service is attempted independently, and mock configs use ON CONFLICT DO NOTHING.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class DemoSeedService {
|
||||
|
||||
@Inject
|
||||
EntityManager em;
|
||||
|
||||
@RestClient
|
||||
@Inject
|
||||
RegistryClient registryClient;
|
||||
|
||||
@ConfigProperty(name = "apix.demo.base-url")
|
||||
String demoBaseUrl;
|
||||
|
||||
@ConfigProperty(name = "apix.registry.admin-key")
|
||||
String adminKey;
|
||||
|
||||
void onStart(@Observes StartupEvent ev) {
|
||||
try {
|
||||
seed();
|
||||
} catch (Exception e) {
|
||||
Log.errorf(e, "Demo seed failed — will retry on next startup");
|
||||
}
|
||||
}
|
||||
|
||||
@Transactional
|
||||
public void seed() {
|
||||
String uuid = loadConfig("demo.sandbox.uuid");
|
||||
String apiKey;
|
||||
|
||||
if (uuid == null) {
|
||||
Log.info("Creating APIX demo sandbox...");
|
||||
SandboxCreated result = registryClient.register(new RegistrationRequest(
|
||||
"apix-demo-ecosystem",
|
||||
"demo@api-index.org",
|
||||
"Global Demo Ecosystem"));
|
||||
uuid = result.sandboxId();
|
||||
apiKey = result.apiKey();
|
||||
saveConfig("demo.sandbox.uuid", uuid);
|
||||
saveConfig("demo.sandbox.api-key", apiKey);
|
||||
saveConfig("demo.sandbox.maintenance-key", result.maintenanceKey());
|
||||
|
||||
registryClient.promoteTier(uuid, adminKey, Map.of("tier", "DEMO"));
|
||||
Log.infof("Demo sandbox created: %s", uuid);
|
||||
} else {
|
||||
apiKey = loadConfig("demo.sandbox.api-key");
|
||||
}
|
||||
|
||||
if ("true".equals(loadConfig("demo.seeded"))) {
|
||||
Log.infof("Demo ecosystem already seeded at sandbox %s", uuid);
|
||||
return;
|
||||
}
|
||||
|
||||
String base = demoBaseUrl + "/" + uuid;
|
||||
int servicesRegistered = 0;
|
||||
int configsInserted = 0;
|
||||
|
||||
for (EndpointSpec spec : ENDPOINTS) {
|
||||
try {
|
||||
registryClient.registerService(uuid, apiKey, buildBsm(spec, base));
|
||||
servicesRegistered++;
|
||||
} catch (Exception e) {
|
||||
Log.warnf("Service registration skipped (%s %s): %s", spec.method(), spec.path(), e.getMessage());
|
||||
}
|
||||
|
||||
int rows = em.createNativeQuery("""
|
||||
INSERT INTO mock_service_configs
|
||||
(id, sandbox_id, path, method, latency_ms, jitter_pct,
|
||||
rate_per_minute, status_code, response_body, price_apx, created_at)
|
||||
VALUES (gen_random_uuid(), :sid, :path, :method, :latency, :jitter,
|
||||
:rate, 200, :response, :price, now())
|
||||
ON CONFLICT (sandbox_id, path, method) DO NOTHING
|
||||
""")
|
||||
.setParameter("sid", uuid)
|
||||
.setParameter("path", spec.path())
|
||||
.setParameter("method", spec.method())
|
||||
.setParameter("latency", spec.latencyMs())
|
||||
.setParameter("jitter", spec.jitterPct())
|
||||
.setParameter("rate", spec.ratePm())
|
||||
.setParameter("response", spec.responseBody())
|
||||
.setParameter("price", spec.priceApx())
|
||||
.executeUpdate();
|
||||
configsInserted += rows;
|
||||
}
|
||||
|
||||
saveConfig("demo.seeded", "true");
|
||||
Log.infof("Demo ecosystem seeded: %d services registered, %d mock configs inserted at %s/%s",
|
||||
servicesRegistered, configsInserted, demoBaseUrl, uuid);
|
||||
}
|
||||
|
||||
// ── Config store ──────────────────────────────────────────────────────────
|
||||
|
||||
private String loadConfig(String key) {
|
||||
DemoConfigEntry e = em.find(DemoConfigEntry.class, key);
|
||||
return e == null ? null : e.value;
|
||||
}
|
||||
|
||||
private void saveConfig(String key, String value) {
|
||||
DemoConfigEntry e = em.find(DemoConfigEntry.class, key);
|
||||
if (e == null) {
|
||||
e = new DemoConfigEntry();
|
||||
e.key = key;
|
||||
}
|
||||
e.value = value;
|
||||
e.updatedAt = Instant.now();
|
||||
em.merge(e);
|
||||
}
|
||||
|
||||
// ── BSM builder ───────────────────────────────────────────────────────────
|
||||
|
||||
private static BsmPayload buildBsm(EndpointSpec spec, String base) {
|
||||
return new BsmPayload(
|
||||
spec.serviceName(),
|
||||
spec.description(),
|
||||
base + spec.path(),
|
||||
spec.capabilities(),
|
||||
spec.regEmail(),
|
||||
spec.regName(),
|
||||
spec.regJurisdiction(),
|
||||
OrgType.COMMERCIAL,
|
||||
null, null, null, null, null,
|
||||
new BsmPayload.Pricing("PER_CALL", spec.priceApx(), "APX", "per-call"),
|
||||
"0.1",
|
||||
ServiceStage.PRODUCTION,
|
||||
null, null, null, null,
|
||||
Map.of(
|
||||
"declaredLatencyMs", spec.latencyMs(),
|
||||
"ratePerMinute", spec.ratePm(),
|
||||
"workflow", spec.workflow(),
|
||||
"mockEndpoint", true));
|
||||
}
|
||||
|
||||
// ── Endpoint specs ────────────────────────────────────────────────────────
|
||||
|
||||
private record EndpointSpec(
|
||||
String method, String path,
|
||||
int latencyMs, int jitterPct, int ratePm,
|
||||
BigDecimal priceApx,
|
||||
String responseBody,
|
||||
String serviceName, String description,
|
||||
List<String> capabilities,
|
||||
String regEmail, String regName, String regJurisdiction,
|
||||
String workflow) {}
|
||||
|
||||
private static final List<EndpointSpec> ENDPOINTS = List.of(
|
||||
|
||||
// ── W1: Cross-border Fulfilment ───────────────────────────────────────
|
||||
|
||||
new EndpointSpec("POST", "/v1/address/validate",
|
||||
80, 10, 600, new BigDecimal("0.0010"),
|
||||
"""
|
||||
{"valid":true,"standardized":{"street":"Maximilianstra\\u00dfe 1","city":"M\\u00fcnchen","postalCode":"80539","countryCode":"DE","coordinates":{"lat":48.1391,"lon":11.5802}},"confidence":0.97,"normalizedFormat":"DIN_5008"}""",
|
||||
"Address Validation", "Validates and standardises postal addresses to ISO/DIN format. Returns geocoordinates and confidence score.",
|
||||
List.of("address-validation", "geocoding"),
|
||||
"demo-databridge@api-index.org", "DataBridge Inc", "US", "W1"),
|
||||
|
||||
new EndpointSpec("GET", "/v1/customs/tariff",
|
||||
300, 10, 120, new BigDecimal("0.0080"),
|
||||
"""
|
||||
{"hsCode":"8471.30","description":"Portable automatic data-processing machines","dutyRate":0.0,"vatRate":0.19,"specialMeasures":[],"regulation":"EU Tariff 2024/1234","currency":"EUR"}""",
|
||||
"Customs Tariff Lookup", "Returns HS tariff codes, duty rates, and VAT rates for cross-border goods classification.",
|
||||
List.of("customs-lookup", "trade-compliance", "hs-classification"),
|
||||
"demo-eutariff@api-index.org", "EuTariff BV", "NL", "W1"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/carrier/nord/quote",
|
||||
150, 10, 300, new BigDecimal("0.0120"),
|
||||
"""
|
||||
{"carrier":"NordLogistik GmbH","service":"Economy EU","estimatedDays":5,"price":{"amount":12.40,"currency":"APX"},"trackingAvailable":true,"cutoffTime":"16:00 CET","quoteId":"NL-Q-DEMO-0017"}""",
|
||||
"NordLogistik Shipping Quote", "Economy EU shipping quotes from NordLogistik. Specialised in intra-European road and rail freight.",
|
||||
List.of("shipping-quote", "logistics", "eu-delivery"),
|
||||
"demo-nordlogistik@api-index.org", "NordLogistik GmbH", "DE", "W1"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/carrier/swift/quote",
|
||||
120, 10, 300, new BigDecimal("0.0350"),
|
||||
"""
|
||||
{"carrier":"SwiftCargo Ltd","service":"Express Global","estimatedDays":2,"price":{"amount":35.00,"currency":"APX"},"trackingAvailable":true,"cutoffTime":"14:00 GMT","quoteId":"SC-Q-DEMO-0043"}""",
|
||||
"SwiftCargo Express Quote", "Global express shipping quotes. 2-day delivery to major hubs worldwide.",
|
||||
List.of("shipping-quote", "express-delivery", "global-logistics"),
|
||||
"demo-swiftcargo@api-index.org", "SwiftCargo Ltd", "GB", "W1"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/carrier/pacrim/quote",
|
||||
200, 10, 200, new BigDecimal("0.0180"),
|
||||
"""
|
||||
{"carrier":"PacRim Express Pte","service":"Asia-Pacific Economy","estimatedDays":7,"price":{"amount":18.20,"currency":"APX"},"trackingAvailable":true,"cutoffTime":"18:00 SGT","quoteId":"PR-Q-DEMO-0009"}""",
|
||||
"PacRim Express Quote", "Economy Asia-Pacific shipping quotes. Cost-optimised routes covering SEA, JP, KR, AU.",
|
||||
List.of("shipping-quote", "logistics", "asia-pacific-delivery"),
|
||||
"demo-pacrim@api-index.org", "PacRim Express Pte", "SG", "W1"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/shipment/label",
|
||||
250, 10, 200, new BigDecimal("0.0150"),
|
||||
"""
|
||||
{"labelId":"SHP-DEMO-7734","trackingNumber":"NL1234567890DE","carrier":"NordLogistik GmbH","labelFormat":"PDF","estimatedPickup":"2026-05-15T16:00:00Z"}""",
|
||||
"Shipment Label Generation", "Generates carrier-compliant shipping labels and assigns a tracking number.",
|
||||
List.of("shipment-label", "logistics", "label-generation"),
|
||||
"demo-nordlogistik@api-index.org", "NordLogistik GmbH", "DE", "W1"),
|
||||
|
||||
new EndpointSpec("GET", "/v1/shipment/track",
|
||||
100, 10, 500, new BigDecimal("0.0030"),
|
||||
"""
|
||||
{"trackingNumber":"NL1234567890DE","status":"IN_TRANSIT","currentLocation":"Frankfurt Hub, DE","estimatedDelivery":"2026-05-19T18:00:00Z","events":[{"timestamp":"2026-05-14T14:23:00Z","location":"Munich Depot","description":"Parcel collected"},{"timestamp":"2026-05-14T22:15:00Z","location":"Frankfurt Hub","description":"In transit to destination hub"}]}""",
|
||||
"Shipment Tracking", "Real-time shipment status and event history by tracking number.",
|
||||
List.of("shipment-tracking", "logistics"),
|
||||
"demo-nordlogistik@api-index.org", "NordLogistik GmbH", "DE", "W1"),
|
||||
|
||||
// ── W2: Micro-lending Decision ────────────────────────────────────────
|
||||
|
||||
new EndpointSpec("POST", "/v1/identity/enrich",
|
||||
350, 10, 200, new BigDecimal("0.0200"),
|
||||
"""
|
||||
{"enrichedName":"Johann K. M\\u00fcller","dateOfBirth":"1984-03-12","currentCity":"Berlin","country":"DE","verificationScore":0.91,"enrichmentId":"IE-DEMO-0552"}""",
|
||||
"Identity Enrichment", "Enriches a person's identity with verified structured data. Input: name + date of birth. Output: normalised profile with verification score.",
|
||||
List.of("identity-enrichment", "kyc", "data-enrichment"),
|
||||
"demo-databridge@api-index.org", "DataBridge Inc", "US", "W2"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/credit/signal",
|
||||
450, 10, 60, new BigDecimal("0.0250"),
|
||||
"""
|
||||
{"enrichmentId":"IE-DEMO-0552","creditScore":720,"riskBand":"LOW","factors":[{"key":"payment_history","score":0.95,"weight":0.35},{"key":"credit_utilization","score":0.72,"weight":0.30},{"key":"account_age","score":0.88,"weight":0.15}],"reportedAt":"2026-05-14T09:00:00Z"}""",
|
||||
"Credit Signal Aggregation", "Aggregates bureau signals into a single credit score and risk band. Requires prior identity enrichment ID.",
|
||||
List.of("credit-scoring", "risk-assessment", "financial-analytics"),
|
||||
"demo-trustscore@api-index.org", "TrustScore AG", "CH", "W2"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/fraud/score",
|
||||
380, 10, 90, new BigDecimal("0.0400"),
|
||||
"""
|
||||
{"requestId":"FS-DEMO-1103","fraudScore":12,"riskLevel":"MINIMAL","flags":[],"velocityCheck":"PASS","deviceFingerprint":"CONSISTENT","recommendedAction":"PROCEED"}""",
|
||||
"Fraud Scoring", "Scores a transaction or application for fraud risk. Returns 0–100 score, risk level, and triggered flags.",
|
||||
List.of("fraud-detection", "risk-scoring", "transaction-security"),
|
||||
"demo-trustscore@api-index.org", "TrustScore AG", "CH", "W2"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/lending/offer",
|
||||
200, 10, 120, new BigDecimal("0.0300"),
|
||||
"""
|
||||
{"offerId":"LO-DEMO-2287","approvedAmount":15000.00,"currency":"APX","interestRateAnnual":0.049,"termMonths":36,"monthlyPayment":448.32,"totalRepayable":16139.52,"validUntil":"2026-05-21T23:59:59Z"}""",
|
||||
"Lending Offer Engine", "Generates a personalised loan offer based on credit score and requested amount.",
|
||||
List.of("loan-origination", "lending", "credit-decision"),
|
||||
"demo-lendfast@api-index.org", "LendFast GmbH", "DE", "W2"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/contract/acknowledge",
|
||||
150, 10, 300, new BigDecimal("0.0050"),
|
||||
"""
|
||||
{"contractId":"CTR-DEMO-0091","offerId":"LO-DEMO-2287","status":"PENDING_SIGNATURE","expiresAt":"2026-05-21T23:59:59Z","signingProvider":"DemoSign"}""",
|
||||
"Contract Acknowledgement", "Initiates a digital contract signing workflow for an accepted lending offer.",
|
||||
List.of("contract-management", "e-signature", "document-workflow"),
|
||||
"demo-lendfast@api-index.org", "LendFast GmbH", "DE", "W2"),
|
||||
|
||||
// ── W3: Healthcare Referral ───────────────────────────────────────────
|
||||
|
||||
new EndpointSpec("POST", "/v1/symptom/triage",
|
||||
500, 10, 120, new BigDecimal("0.0150"),
|
||||
"""
|
||||
{"triageId":"TR-DEMO-0834","urgency":"ROUTINE","urgencyScore":3,"specialtyRequired":"CARDIOLOGY","recommendedTimeframe":"within_2_weeks","disclaimer":"Non-clinical triage assessment. Consult a physician for medical advice."}""",
|
||||
"Symptom Triage", "Non-clinical AI triage: maps reported symptoms to urgency level and required medical specialty.",
|
||||
List.of("medical-triage", "symptom-assessment", "healthcare"),
|
||||
"demo-mednet@api-index.org", "MedNet Systems", "NL", "W3"),
|
||||
|
||||
new EndpointSpec("GET", "/v1/specialist/availability",
|
||||
250, 10, 180, new BigDecimal("0.0100"),
|
||||
"""
|
||||
{"specialty":"CARDIOLOGY","slots":[{"slotId":"SLT-20260520-0900","practitioner":"Dr. Helena Brandt","datetime":"2026-05-20T09:00:00Z","location":"Charit\\u00e9 Outpatient, Berlin","type":"IN_PERSON"},{"slotId":"SLT-20260520-1400","practitioner":"Dr. Markus Frei","datetime":"2026-05-20T14:00:00Z","type":"VIDEO"}],"nextAvailable":"2026-05-20T09:00:00Z"}""",
|
||||
"Specialist Availability", "Returns available appointment slots for a given medical specialty and location.",
|
||||
List.of("appointment-booking", "specialist-scheduling", "healthcare"),
|
||||
"demo-mednet@api-index.org", "MedNet Systems", "NL", "W3"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/insurance/eligibility",
|
||||
320, 10, 150, new BigDecimal("0.0220"),
|
||||
"""
|
||||
{"covered":true,"coveragePercent":80,"copay":25.00,"currency":"APX","preAuthRequired":true,"preAuthCode":null,"planName":"DemoPlan Premium","validThrough":"2026-12-31"}""",
|
||||
"Insurance Eligibility Check", "Verifies patient insurance coverage for a given service code. Returns copay, coverage %, and pre-auth requirements.",
|
||||
List.of("insurance-verification", "coverage-check", "healthcare-billing"),
|
||||
"demo-insubridge@api-index.org", "InsuBridge AG", "CH", "W3"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/appointment/reserve",
|
||||
200, 10, 200, new BigDecimal("0.0180"),
|
||||
"""
|
||||
{"appointmentId":"APT-DEMO-0900","slotId":"SLT-20260520-0900","confirmationCode":"CONF-4471-X","practitioner":"Dr. Helena Brandt","datetime":"2026-05-20T09:00:00Z","location":"Charit\\u00e9 Outpatient, Charit\\u00e9platz 1, 10117 Berlin"}""",
|
||||
"Appointment Reservation", "Reserves a specialist appointment slot and returns a confirmation code.",
|
||||
List.of("appointment-booking", "scheduling", "healthcare"),
|
||||
"demo-insubridge@api-index.org", "InsuBridge AG", "CH", "W3"),
|
||||
|
||||
new EndpointSpec("POST", "/v1/prescription/preauth",
|
||||
600, 10, 30, new BigDecimal("0.0750"),
|
||||
"""
|
||||
{"authId":"PA-2026-48821","authorized":true,"medication":"Bisoprolol 5mg","diagnosis":"Suspected stable angina (I25.1)","authorizedQuantity":30,"refillsAllowed":2,"validUntil":"2026-06-14T23:59:59Z","dispensingInstructions":"Once daily with food. Blood pressure monitoring required."}""",
|
||||
"Prescription Pre-authorisation", "Issues pre-authorisation for prescribed medications. Highest latency — regulatory validation required. Rate-limited to 30/min.",
|
||||
List.of("prescription-authorization", "pharmacy", "healthcare"),
|
||||
"demo-rxchain@api-index.org", "RxChain Corp", "JP", "W3")
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
package org.botstandards.apix.demo.service;
|
||||
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
import jakarta.inject.Inject;
|
||||
import jakarta.persistence.EntityManager;
|
||||
import jakarta.ws.rs.core.Response;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.Random;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import org.botstandards.apix.demo.entity.MockServiceConfig;
|
||||
|
||||
@ApplicationScoped
|
||||
public class MockDispatcherService {
|
||||
|
||||
@Inject
|
||||
EntityManager em;
|
||||
|
||||
@Inject
|
||||
RateLimiterService rateLimiter;
|
||||
|
||||
private final Random rng = new Random();
|
||||
|
||||
/** (sandboxId:path:method) → Optional<config>. Empty means "confirmed not found". */
|
||||
private final ConcurrentHashMap<String, Optional<MockServiceConfig>> configCache =
|
||||
new ConcurrentHashMap<>();
|
||||
|
||||
@Scheduled(every = "5M")
|
||||
void invalidateCache() {
|
||||
configCache.clear();
|
||||
}
|
||||
|
||||
public Response dispatch(String sandboxId, String path, String method) {
|
||||
String fullPath = "/" + path;
|
||||
MockServiceConfig cfg = resolveConfig(sandboxId, fullPath, method.toUpperCase());
|
||||
|
||||
if (cfg == null) {
|
||||
return Response.status(404)
|
||||
.entity(Map.of(
|
||||
"message", method.toUpperCase() + " " + fullPath + " not configured in sandbox " + sandboxId,
|
||||
"hint", "Register a mock config via the APIX demo API"))
|
||||
.build();
|
||||
}
|
||||
|
||||
if (!rateLimiter.allow(sandboxId, fullPath, method.toUpperCase(), cfg.ratePerMinute)) {
|
||||
return Response.status(429)
|
||||
.header("Retry-After", "60")
|
||||
.entity(Map.of(
|
||||
"message", "Rate limit exceeded",
|
||||
"limitPerMinute", cfg.ratePerMinute,
|
||||
"retryAfterSeconds", 60))
|
||||
.build();
|
||||
}
|
||||
|
||||
int jitter = (int) (cfg.latencyMs * (cfg.jitterPct / 100.0) * (rng.nextDouble() * 2 - 1));
|
||||
int delay = Math.max(0, cfg.latencyMs + jitter);
|
||||
try {
|
||||
Thread.sleep(delay);
|
||||
} catch (InterruptedException e) {
|
||||
Thread.currentThread().interrupt();
|
||||
}
|
||||
|
||||
return Response.status(cfg.statusCode)
|
||||
.entity(cfg.responseBody)
|
||||
.header("Content-Type", "application/json")
|
||||
.header("X-APX-Cost", cfg.priceApx.toPlainString())
|
||||
.header("X-APX-Latency-Ms", String.valueOf(delay))
|
||||
.header("X-APX-Sandbox", sandboxId)
|
||||
.build();
|
||||
}
|
||||
|
||||
private MockServiceConfig resolveConfig(String sandboxId, String path, String method) {
|
||||
String key = sandboxId + ":" + path + ":" + method;
|
||||
return configCache.computeIfAbsent(key, k ->
|
||||
em.createQuery(
|
||||
"SELECT c FROM MockServiceConfig c " +
|
||||
"WHERE c.sandboxId = :sid AND c.path = :path AND c.method = :method",
|
||||
MockServiceConfig.class)
|
||||
.setParameter("sid", sandboxId)
|
||||
.setParameter("path", path)
|
||||
.setParameter("method", method)
|
||||
.getResultList()
|
||||
.stream().findFirst()
|
||||
).orElse(null);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
package org.botstandards.apix.demo.service;
|
||||
|
||||
import io.quarkus.scheduler.Scheduled;
|
||||
import jakarta.enterprise.context.ApplicationScoped;
|
||||
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
/**
|
||||
* Fixed-window per-minute rate limiter keyed by (sandboxId, path, method).
|
||||
* Resets all buckets at the start of each minute. Good enough for demo purposes —
|
||||
* a token-bucket implementation would reduce burst risk in production.
|
||||
*/
|
||||
@ApplicationScoped
|
||||
public class RateLimiterService {
|
||||
|
||||
private final ConcurrentHashMap<String, AtomicInteger> counters = new ConcurrentHashMap<>();
|
||||
|
||||
/** Returns true if the request is within the declared rate limit. */
|
||||
public boolean allow(String sandboxId, String path, String method, int ratePerMinute) {
|
||||
String key = sandboxId + ":" + path + ":" + method;
|
||||
return counters.computeIfAbsent(key, k -> new AtomicInteger(0))
|
||||
.incrementAndGet() <= ratePerMinute;
|
||||
}
|
||||
|
||||
@Scheduled(cron = "0 * * * * ?")
|
||||
void resetBuckets() {
|
||||
counters.clear();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
quarkus.http.port=8083
|
||||
quarkus.smallrye-health.root-path=/q/health
|
||||
quarkus.log.level=${LOG_LEVEL:INFO}
|
||||
|
||||
# DB — shares the same PostgreSQL instance as the registry
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.jdbc.url=${DB_URL:jdbc:postgresql://localhost:5432/apix}
|
||||
quarkus.datasource.username=${DB_USER:apix}
|
||||
quarkus.datasource.password=${DB_PASSWORD:apix}
|
||||
quarkus.hibernate-orm.database.generation=none
|
||||
|
||||
# Liquibase — demo owns demo_config + mock_service_configs
|
||||
quarkus.liquibase.change-log=db/changelog/db.changelog-master.xml
|
||||
quarkus.liquibase.migrate-at-start=true
|
||||
|
||||
# Registry REST client — seed service calls the registry to create sandbox and register services
|
||||
quarkus.rest-client.registry.url=${APIX_REGISTRY_URL:http://registry:8180}
|
||||
quarkus.rest-client.registry.connect-timeout=5000
|
||||
quarkus.rest-client.registry.read-timeout=10000
|
||||
|
||||
# Demo config
|
||||
apix.demo.base-url=${APIX_DEMO_BASE_URL:https://demo.api-index.org}
|
||||
apix.registry.admin-key=${APIX_API_KEY:dev-admin-key}
|
||||
@@ -0,0 +1,74 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="demo-001" author="apix-demo">
|
||||
|
||||
<!-- Key-value store for demo bootstrap state (sandbox UUID, seed flag, etc.) -->
|
||||
<createTable tableName="demo_config">
|
||||
<column name="key" type="varchar(100)">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="value" type="text"/>
|
||||
<column name="updated_at" type="timestamptz" defaultValueComputed="now()">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<!-- Sandbox-scoped mock endpoint configurations.
|
||||
Any sandbox owner can register mock endpoints here; the demo
|
||||
ecosystem seed pre-populates the APIX demo sandbox on first boot. -->
|
||||
<createTable tableName="mock_service_configs">
|
||||
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="sandbox_id" type="varchar(100)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Leading slash, e.g. /v1/address/validate -->
|
||||
<column name="path" type="varchar(255)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="method" type="varchar(10)" defaultValue="POST">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="latency_ms" type="int" defaultValueNumeric="200">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Applied as ±jitter_pct% random deviation around latency_ms -->
|
||||
<column name="jitter_pct" type="int" defaultValueNumeric="10">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="rate_per_minute" type="int" defaultValueNumeric="100">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="status_code" type="int" defaultValueNumeric="200">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Static JSON body returned verbatim on every call -->
|
||||
<column name="response_body" type="text">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Cost surfaced in X-APX-Cost response header -->
|
||||
<column name="price_apx" type="numeric(10,4)" defaultValueNumeric="0.0100">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="created_at" type="timestamptz" defaultValueComputed="now()">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<addUniqueConstraint tableName="mock_service_configs"
|
||||
columnNames="sandbox_id, path, method"
|
||||
constraintName="uq_mock_config_sandbox_path_method"/>
|
||||
|
||||
<createIndex tableName="mock_service_configs" indexName="idx_mock_configs_sandbox">
|
||||
<column name="sandbox_id"/>
|
||||
</createIndex>
|
||||
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<include file="changes/001-demo-schema.xml" relativeToChangelogFile="true"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user