feat(apix-demo): working Node.js discovery demo + sandbox cleanup

- apix-demo.mjs: all 6 steps run end-to-end — HATEOAS root, sandbox
  creation, BSM registration (3 carriers), capability search, parallel
  quote calls, best-price selection; all field names aligned to BsmPayload
- SandboxService: add deleteSandbox() — removes services then sandbox,
  DEMO tier protected
- AdminResource: add DELETE /admin/sandboxes/{uuid} backed by deleteSandbox
- cleanup-sandboxes.mjs: admin script to purge test sandboxes while
  preserving DEMO-tier and named exceptions (apix-demo-ecosystem)

Co-Authored-By: Mira Rehfeld <noreply@anthropic.com>
This commit is contained in:
Carsten Rehfeld
2026-05-17 18:04:06 +02:00
parent b85c24d9bf
commit 02420fcbc4
4 changed files with 154 additions and 29 deletions
+30 -28
View File
@@ -76,63 +76,65 @@ async function main() {
const carriers = [
{
serviceName: 'NordLogistik Economy EU',
name: 'NordLogistik Economy EU',
description: 'Economy EU road & rail freight. 5-day delivery. DE/AT/CH specialist.',
endpoint: 'https://demo.api-index.org/carriers/nord/quote',
capabilities: ['shipping-quote', 'logistics', 'eu-delivery'],
registrantEmail: 'api@nordlogistik.example',
registrantName: 'NordLogistik GmbH',
registrantJurisdiction:'DE',
orgType: 'COMMERCIAL',
pricing: { model: 'PER_CALL', amount: 0.012, currency: 'APX', unit: 'per-call' },
registrantOrgType: 'COMMERCIAL',
pricing: { billingModel: 'PER_CALL', pricePerCall: 0.012, currency: 'APX', billingUnit: 'per-call' },
bsmVersion: '0.1',
stage: 'PRODUCTION',
serviceStage: 'PRODUCTION',
},
{
serviceName: 'SwiftCargo Express',
name: 'SwiftCargo Express',
description: 'Global express shipping. 2-day delivery to major hubs worldwide.',
endpoint: 'https://demo.api-index.org/carriers/swift/quote',
capabilities: ['shipping-quote', 'express-delivery', 'global-logistics'],
registrantEmail: 'api@swiftcargo.example',
registrantName: 'SwiftCargo Ltd',
registrantJurisdiction:'GB',
orgType: 'COMMERCIAL',
pricing: { model: 'PER_CALL', amount: 0.035, currency: 'APX', unit: 'per-call' },
registrantOrgType: 'COMMERCIAL',
pricing: { billingModel: 'PER_CALL', pricePerCall: 0.035, currency: 'APX', billingUnit: 'per-call' },
bsmVersion: '0.1',
stage: 'PRODUCTION',
serviceStage: 'PRODUCTION',
},
{
serviceName: 'PacRim Express',
name: 'PacRim Express',
description: 'Cost-optimised Asia-Pacific shipping. SEA, JP, KR, AU routes.',
endpoint: 'https://demo.api-index.org/carriers/pacrim/quote',
capabilities: ['shipping-quote', 'logistics', 'asia-pacific-delivery'],
registrantEmail: 'api@pacrim.example',
registrantName: 'PacRim Express Pte',
registrantJurisdiction:'SG',
orgType: 'COMMERCIAL',
pricing: { model: 'PER_CALL', amount: 0.018, currency: 'APX', unit: 'per-call' },
registrantOrgType: 'COMMERCIAL',
pricing: { billingModel: 'PER_CALL', pricePerCall: 0.018, currency: 'APX', billingUnit: 'per-call' },
bsmVersion: '0.1',
stage: 'PRODUCTION',
serviceStage: 'PRODUCTION',
},
];
for (const manifest of carriers) {
await post(servicesHref, manifest, { 'X-Api-Key': apiKey });
console.log(` ✓ Registered: ${manifest.serviceName} (${manifest.registrantJurisdiction})`);
console.log(` ✓ Registered: ${manifest.name} (${manifest.registrantJurisdiction})`);
}
console.log('\n → Any provider self-registers. The agent discovers them automatically.');
// ── Step 4: Agent discovers carriers ──────────────────────────────────────
hdr('Step 4 Agent queries: "who can provide shipping-quote?"');
const searchUrl = searchHrefTpl.replace('{?capability,stage}', '')
+ '?capability=shipping-quote';
// Strip the URI template expression {?...} and build the URL properly
const searchBase = searchHrefTpl.replace(/\{[^}]+\}/, '');
const searchUrl = `${searchBase}?capability=shipping-quote&stage=PRODUCTION`;
console.log(` Query: GET ${searchUrl}\n`);
const found = await get(searchUrl);
console.log(` Found ${found.length} carrier service(s):\n`);
for (const svc of found) {
const price = svc.pricing ? `${svc.pricing.amount} ${svc.pricing.currency}/${svc.pricing.unit}` : 'unknown';
console.log(` ┌─ ${svc.serviceName}`);
const p = svc.pricing;
const price = p ? `${p.pricePerCall} ${p.currency}/${p.billingUnit}` : 'unknown';
console.log(` ┌─ ${svc.name}`);
console.log(` │ Jurisdiction : ${svc.registrantJurisdiction}`);
console.log(` │ Capabilities : ${svc.capabilities?.join(', ')}`);
console.log(` │ Price : ${price}`);
@@ -151,23 +153,23 @@ async function main() {
const quoteRequests = found.map(async svc => {
const start = Date.now();
try {
const quote = await post(svc.endpoint, shipment).catch(() => mockQuote(svc));
return { name: svc.serviceName, quote, ms: Date.now() - start };
const quote = await post(svc.endpoint, shipment).catch(() => mockQuote(svc.name));
return { svcName: svc.name, quote, ms: Date.now() - start };
} catch {
return { name: svc.serviceName, quote: mockQuote(svc), ms: Date.now() - start };
return { svcName: svc.name, quote: mockQuote(svc.name), ms: Date.now() - start };
}
});
const results = await Promise.all(quoteRequests);
results.sort((a, b) => a.quote.price.amount - b.quote.price.amount);
for (const { name, quote, ms } of results) {
const best = results[0].name === name ? ' ← best price' : '';
console.log(` ${name.padEnd(28)} ${quote.price.amount.toFixed(2)} ${quote.price.currency} · ${quote.estimatedDays}d${best}`);
for (const { svcName, quote } of results) {
const best = results[0].svcName === svcName ? ' ← best price' : '';
console.log(` ${svcName.padEnd(28)} ${quote.price.amount.toFixed(2)} ${quote.price.currency} · ${quote.estimatedDays}d${best}`);
}
const winner = results[0];
console.log(`\n Selected: ${winner.name} (${winner.quote.price.amount.toFixed(2)} ${winner.quote.price.currency})`);
console.log(`\n Selected: ${winner.svcName} (${winner.quote.price.amount.toFixed(2)} ${winner.quote.price.currency})`);
// ── Summary ────────────────────────────────────────────────────────────────
hdr('Summary');
@@ -189,7 +191,7 @@ async function main() {
}
// Fallback mock quotes (in case demo endpoints are not reachable)
function mockQuote(svc) {
function mockQuote(name) {
const prices = {
'NordLogistik Economy EU': { amount: 12.40, currency: 'APX' },
'SwiftCargo Express': { amount: 35.00, currency: 'APX' },
@@ -197,9 +199,9 @@ function mockQuote(svc) {
};
const days = { 'NordLogistik Economy EU': 5, 'SwiftCargo Express': 2, 'PacRim Express': 7 };
return {
carrier: svc.serviceName,
price: prices[svc.serviceName] ?? { amount: 20.00, currency: 'APX' },
estimatedDays: days[svc.serviceName] ?? 5,
carrier: name,
price: prices[name] ?? { amount: 20.00, currency: 'APX' },
estimatedDays: days[name] ?? 5,
trackingAvailable: true,
};
}