From 912b6c76727f53a06a860decd7553cfa475a103d Mon Sep 17 00:00:00 2001 From: pawmmm Date: Fri, 28 Aug 2026 15:34:31 -0600 Subject: [PATCH 1/2] fix: parse live service availability --- src/lib/api.ts | 18 ++++- test/service-catalog.test.mjs | 101 +++++++-------------------- test/vectors/public-v3-services.json | 94 +++++++++++++++++++++++++ 3 files changed, 136 insertions(+), 77 deletions(-) create mode 100644 test/vectors/public-v3-services.json diff --git a/src/lib/api.ts b/src/lib/api.ts index 4c5459c..c0d094c 100644 --- a/src/lib/api.ts +++ b/src/lib/api.ts @@ -198,6 +198,18 @@ function text( return value; } +function multilineText( + value: unknown, + label: string, + maximum = 32_000, +): string { + if ( + typeof value !== 'string' || value.length === 0 || value.length > maximum || + /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/.test(value) + ) throw new Error(`gateway ${label} is invalid`); + return value; +} + function decimal(value: unknown, label: string): string { const found = text(value, label, 80); if (!/^\d+$/.test(found)) throw new Error(`gateway ${label} is invalid`); @@ -290,7 +302,7 @@ function parsePublicSkill(value: unknown): PublicSkill { id: text(listing.listingId, 'listing ID', 64), skillId: text(skill.skillId, 'skill ID', 96), name: text(presentation.name, 'skill name', 160), - description: text(presentation.description, 'skill description'), + description: multilineText(presentation.description, 'skill description'), tags: strings(presentation.tags, 'skill tags', 64), documentationUrl: httpsUrl( presentation.documentationUrl, @@ -301,7 +313,7 @@ function parsePublicSkill(value: unknown): PublicSkill { variable: paymentRequired && fixedAmountAtomic === null, paymentRequired, acceptingNewOrders: booleanValue( - contract.acceptingNewOrders, + skill.acceptingNewOrders, 'skill acceptingNewOrders', ), fulfillmentMode: fulfillmentMode as PublicSkill['fulfillmentMode'], @@ -360,7 +372,7 @@ function parsePublicService(value: unknown): PublicService { categoryFamily: text(service.categoryFamily, 'category family', 128), serviceType: text(service.serviceType, 'service type', 128), jurisdictions: strings(service.jurisdictions, 'service jurisdictions', 64), - serviceDescription: text(item.description, 'service description'), + serviceDescription: multilineText(item.description, 'service description'), serviceLifecycle: text(service.lifecycle, 'service lifecycle', 128), turnaroundEstimate: text( service.turnaroundEstimate, diff --git a/test/service-catalog.test.mjs b/test/service-catalog.test.mjs index a089307..49b8192 100644 --- a/test/service-catalog.test.mjs +++ b/test/service-catalog.test.mjs @@ -1,4 +1,5 @@ import assert from 'node:assert/strict'; +import { readFileSync } from 'node:fs'; import test from 'node:test'; import { parseServiceIndex, @@ -7,88 +8,40 @@ import { servicePath, } from '../src/lib/api.ts'; -const service = { - gatewayRegistrationId: '4ce2f9c8-2980-49ad-a919-3e0acf1ea4e7', - providerAgentId: '41', - serviceId: `0x${'ab'.repeat(32)}`, - agentCardUrl: 'https://provider.example/agent-cards/lab-analysis.json', - providerPayee: `0x${'12'.repeat(20)}`, - legal: { - marketplaceTermsUrl: 'https://daski.example/terms', - marketplacePrivacyUrl: 'https://daski.example/privacy', - providerLegalName: 'Example Research LLC', - providerTermsUrl: 'https://provider.example/terms', - providerPrivacyUrl: 'https://provider.example/privacy', - }, - name: 'Laboratory Analysis', - description: 'Independent laboratory analysis for submitted samples.', - service: { - serviceId: `0x${'ab'.repeat(32)}`, - slug: 'lab-analysis', - version: '1', - categoryFamily: 'scientific-services', - serviceType: 'laboratory-analysis', - jurisdictions: ['US'], - lifecycle: 'active', - turnaroundEstimate: '3-5 business days', - acceptingNewOrders: true, - }, - standardRail: { - origin: 'https://provider.example/', - providerAudience: 'https://provider.example', - quoteUrl: 'https://provider.example/standard-rail/quote', - dispatchUrl: 'https://provider.example/standard-rail/dispatch', - dispatchStatusUrl: 'https://provider.example/standard-rail/status', - lifecycleUrl: 'https://provider.example/standard-rail/lifecycle', - assetQueryUrl: 'https://provider.example/standard-rail/assets', - assetActionUrl: 'https://provider.example/standard-rail/actions', - }, - skills: [{ - skillId: 'analyze-sample', - skillContractHash: `0x${'cd'.repeat(32)}`, - presentation: { - name: 'Analyze sample', - description: 'Analyze one submitted sample.', - examples: [], - tags: ['laboratory'], - documentationUrl: 'https://provider.example/docs/analyze-sample', - }, - contract: { - inputSchema: { type: 'object' }, - resultSchema: { type: 'object' }, - pricing: { USDC: { fixed_amount: '2500000' } }, - paymentRequired: true, - requiresAssetOwnership: false, - assetType: null, - fulfillmentMode: 'human', - acceptingNewOrders: true, - capacity: { maxOpenOrders: 4 }, - deadlines: {}, - assetAction: null, - }, - listing: { - listingId: 'listing-lab-analysis', - listingKey: `0x${'ef'.repeat(32)}`, - paymentRequired: true, - splitterAddress: `0x${'34'.repeat(20)}`, - }, - }], - freshness: { - lastValidatedAt: '2026-08-26T12:00:00.000Z', - presentationStaleAfterSeconds: 86400, - commerceFreshnessSeconds: 300, - }, -}; +const serviceIndex = JSON.parse(readFileSync( + new URL('./vectors/public-v3-services.json', import.meta.url), + 'utf8', +)); +const service = serviceIndex.services[0]; +const copyServiceIndex = () => structuredClone(serviceIndex); -test('parses the service-first gateway catalog without category allowlists', () => { - const parsed = parseServiceIndex({ services: [service] }); +test('parses the gateway-owned public services contract', () => { + const parsed = parseServiceIndex(copyServiceIndex()); assert.equal(parsed.services.length, 1); assert.equal(parsed.services[0].categoryFamily, 'scientific-services'); assert.equal(parsed.services[0].providerName, 'Example Research LLC'); assert.equal(parsed.services[0].skills[0].skillId, 'analyze-sample'); + assert.equal(parsed.services[0].skills[0].acceptingNewOrders, true); + assert.match(parsed.services[0].skills[0].description, /\nReturns a signed result\./); assert.equal(priceRange(parsed.services[0]), '2.5 USDC'); assert.equal(servicePath(parsed.services[0]), `/service/${service.serviceId}`); + assert.equal(service.skills[0].contract.acceptingNewOrders, undefined); +}); + +test('reads mutable skill availability outside the hashed contract', () => { + const paused = copyServiceIndex(); + paused.services[0].skills[0].acceptingNewOrders = false; + paused.services[0].skills[0].contract.acceptingNewOrders = true; + assert.equal(parseServiceIndex(paused).services[0].skills[0].acceptingNewOrders, false); + + const nestedOnly = copyServiceIndex(); + delete nestedOnly.services[0].skills[0].acceptingNewOrders; + nestedOnly.services[0].skills[0].contract.acceptingNewOrders = true; + assert.throws( + () => parseServiceIndex(nestedOnly), + /skill acceptingNewOrders is invalid/, + ); }); test('rejects duplicate canonical service ids', () => { diff --git a/test/vectors/public-v3-services.json b/test/vectors/public-v3-services.json new file mode 100644 index 0000000..1f3c2e8 --- /dev/null +++ b/test/vectors/public-v3-services.json @@ -0,0 +1,94 @@ +{ + "services": [ + { + "gatewayRegistrationId": "4ce2f9c8-2980-49ad-a919-3e0acf1ea4e7", + "providerAgentId": "41", + "serviceId": "0xabababababababababababababababababababababababababababababababab", + "agentCardUrl": "https://provider.example/agent-cards/lab-analysis.json", + "providerPayee": "0x1212121212121212121212121212121212121212", + "legal": { + "marketplaceTermsUrl": "https://daski.example/terms", + "marketplacePrivacyUrl": "https://daski.example/privacy", + "providerLegalName": "Example Research LLC", + "providerTermsUrl": "https://provider.example/terms", + "providerPrivacyUrl": "https://provider.example/privacy" + }, + "name": "Laboratory Analysis", + "description": "Independent laboratory analysis for submitted samples.\nResults are returned as structured data.", + "service": { + "serviceId": "0xabababababababababababababababababababababababababababababababab", + "slug": "lab-analysis", + "version": "1", + "categoryFamily": "scientific-services", + "serviceType": "laboratory-analysis", + "jurisdictions": [ + "US" + ], + "lifecycle": "active", + "turnaroundEstimate": "3-5 business days", + "acceptingNewOrders": true + }, + "standardRail": { + "origin": "https://provider.example/", + "providerAudience": "https://provider.example", + "quoteUrl": "https://provider.example/standard-rail/quote", + "dispatchUrl": "https://provider.example/standard-rail/dispatch", + "dispatchStatusUrl": "https://provider.example/standard-rail/status", + "lifecycleUrl": "https://provider.example/standard-rail/lifecycle", + "assetQueryUrl": "https://provider.example/standard-rail/assets", + "assetActionUrl": "https://provider.example/standard-rail/actions" + }, + "skills": [ + { + "skillId": "analyze-sample", + "skillContractHash": "0xcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcdcd", + "acceptingNewOrders": true, + "presentation": { + "name": "Analyze sample", + "description": "Analyze one submitted sample.\nReturns a signed result.", + "examples": [], + "tags": [ + "laboratory" + ], + "documentationUrl": "https://provider.example/docs/analyze-sample" + }, + "contract": { + "inputSchema": { + "type": "object" + }, + "resultSchema": { + "type": "object" + }, + "pricing": { + "USDC": { + "fixed_amount": "2500000" + } + }, + "paymentRequired": true, + "requiresAssetOwnership": false, + "assetType": null, + "fulfillmentMode": "human", + "capacity": { + "maxOpenOrders": 4 + }, + "deadlines": {}, + "assetAction": null + }, + "listing": { + "listingId": "listing-lab-analysis", + "listingKey": "0xefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefefef", + "paymentRequired": true, + "splitterAddress": "0x3434343434343434343434343434343434343434", + "runtimeCommitmentHash": null, + "runtimeCommitment": null + } + } + ], + "freshness": { + "lastValidatedAt": "2026-08-26T12:00:00.000Z", + "presentationStaleAfterSeconds": 86400, + "commerceFreshnessSeconds": 300 + } + } + ] +} From f1a7033c16465ba0db5370225e0b8e95339a4117 Mon Sep 17 00:00:00 2001 From: pawmmm Date: Fri, 28 Aug 2026 16:09:08 -0600 Subject: [PATCH 2/2] ci: verify lint, tests, and build on develop and main --- .github/workflows/verify.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 .github/workflows/verify.yml diff --git a/.github/workflows/verify.yml b/.github/workflows/verify.yml new file mode 100644 index 0000000..1c93152 --- /dev/null +++ b/.github/workflows/verify.yml @@ -0,0 +1,21 @@ +name: verify + +on: + push: + branches: [develop, main] + pull_request: + branches: [develop, main] + +jobs: + verify: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm run lint + - run: npm test + - run: npm run build