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:
+30
-28
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user