From 1dc802439758d9885c91fba0f875daaef168d2db Mon Sep 17 00:00:00 2001 From: Daev Mithran Date: Tue, 6 Jan 2026 14:16:54 +0530 Subject: [PATCH 1/4] feat: Demo with agntcy --- package-lock.json | 401 +++++++++++++- package.json | 1 + src/app.ts | 5 + src/controllers/api/agntcy.ts | 409 ++++++++++++++ src/middleware/auth/routes/api/record-auth.ts | 10 + src/middleware/authentication.ts | 2 + src/services/api/agntcy.ts | 498 ++++++++++++++++++ src/static/swagger-api.json | 425 +++++++++++++++ src/types/record.ts | 335 ++++++++++++ src/types/swagger-api-types.ts | 95 ++++ 10 files changed, 2175 insertions(+), 6 deletions(-) create mode 100644 src/controllers/api/agntcy.ts create mode 100644 src/middleware/auth/routes/api/record-auth.ts create mode 100644 src/services/api/agntcy.ts create mode 100644 src/types/record.ts diff --git a/package-lock.json b/package-lock.json index 2d2d3b36..d641c2c0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@cheqd/studio", - "version": "3.16.0-develop.11", + "version": "3.16.0-develop.12", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@cheqd/studio", - "version": "3.16.0-develop.11", + "version": "3.16.0-develop.12", "license": "Apache-2.0", "dependencies": { "@cheqd/did-provider-cheqd": "^4.7.0-develop.1", @@ -33,6 +33,7 @@ "@verida/encryption-utils": "^3.0.1", "@verida/types": "^3.0.2", "@verida/vda-did-resolver": "^4.4.5", + "agntcy-dir": "^0.5.6", "bcrypt": "^5.1.1", "bs58": "^6.0.0", "cookie-parser": "^1.4.7", @@ -2264,6 +2265,28 @@ "pbts": "bin/pbts" } }, + "node_modules/@connectrpc/connect": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@connectrpc/connect/-/connect-2.1.1.tgz", + "integrity": "sha512-JzhkaTvM73m2K1URT6tv53k2RwngSmCXLZJgK580qNQOXRzZRR/BCMfZw3h+90JpnG6XksP5bYT+cz0rpUzUWQ==", + "license": "Apache-2.0", + "peerDependencies": { + "@bufbuild/protobuf": "^2.7.0" + } + }, + "node_modules/@connectrpc/connect-node": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@connectrpc/connect-node/-/connect-node-2.1.1.tgz", + "integrity": "sha512-s3TfsI1XF+n+1z6MBS9rTnFsxxR4Rw5wmdEnkQINli81ESGxcsfaEet8duzq8LVuuCupmhUsgpRo0Nv9pZkufg==", + "license": "Apache-2.0", + "engines": { + "node": ">=20" + }, + "peerDependencies": { + "@bufbuild/protobuf": "^2.7.0", + "@connectrpc/connect": "2.1.1" + } + }, "node_modules/@cosmjs/amino": { "version": "0.36.2", "resolved": "https://registry.npmjs.org/@cosmjs/amino/-/amino-0.36.2.tgz", @@ -6428,6 +6451,69 @@ "license": "MIT", "optional": true }, + "node_modules/@grpc/grpc-js": { + "version": "1.14.3", + "resolved": "https://registry.npmjs.org/@grpc/grpc-js/-/grpc-js-1.14.3.tgz", + "integrity": "sha512-Iq8QQQ/7X3Sac15oB6p0FmUg/klxQvXLeileoqrTRGJYLV+/9tubbr9ipz0GKHjmXVsgFPo/+W+2cA8eNcR+XA==", + "license": "Apache-2.0", + "dependencies": { + "@grpc/proto-loader": "^0.8.0", + "@js-sdsl/ordered-map": "^4.4.2" + }, + "engines": { + "node": ">=12.10.0" + } + }, + "node_modules/@grpc/proto-loader": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@grpc/proto-loader/-/proto-loader-0.8.0.tgz", + "integrity": "sha512-rc1hOQtjIWGxcxpb9aHAfLpIctjEnsDehj0DAiVfBlmT84uvR0uUtN2hEi/ecvWVjXUGf5qPF4qEgiLOx1YIMQ==", + "license": "Apache-2.0", + "dependencies": { + "lodash.camelcase": "^4.3.0", + "long": "^5.0.0", + "protobufjs": "^7.5.3", + "yargs": "^17.7.2" + }, + "bin": { + "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@grpc/proto-loader/node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@grpc/proto-loader/node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/@humanfs/core": { "version": "0.19.1", "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", @@ -7208,6 +7294,16 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@js-sdsl/ordered-map": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@js-sdsl/ordered-map/-/ordered-map-4.4.2.tgz", + "integrity": "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/js-sdsl" + } + }, "node_modules/@jsdevtools/ono": { "version": "7.1.3", "resolved": "https://registry.npmjs.org/@jsdevtools/ono/-/ono-7.1.3.tgz", @@ -15032,13 +15128,146 @@ "@octokit/openapi-types": "^25.0.0" } }, + "node_modules/@peculiar/asn1-cms": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-cms/-/asn1-cms-2.6.0.tgz", + "integrity": "sha512-2uZqP+ggSncESeUF/9Su8rWqGclEfEiz1SyU02WX5fUONFfkjzS2Z/F1Li0ofSmf4JqYXIOdCAZqIXAIBAT1OA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-cms/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-csr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-csr/-/asn1-csr-2.6.0.tgz", + "integrity": "sha512-BeWIu5VpTIhfRysfEp73SGbwjjoLL/JWXhJ/9mo4vXnz3tRGm+NGm3KNcRzQ9VMVqwYS2RHlolz21svzRXIHPQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-csr/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-ecc": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-ecc/-/asn1-ecc-2.6.0.tgz", + "integrity": "sha512-FF3LMGq6SfAOwUG2sKpPXblibn6XnEIKa+SryvUl5Pik+WR9rmRA3OCiwz8R3lVXnYnyRkSZsSLdml8H3UiOcw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-ecc/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-pfx": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pfx/-/asn1-pfx-2.6.0.tgz", + "integrity": "sha512-rtUvtf+tyKGgokHHmZzeUojRZJYPxoD/jaN1+VAB4kKR7tXrnDCA/RAWXAIhMJJC+7W27IIRGe9djvxKgsldCQ==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-rsa": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pfx/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-pkcs8": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs8/-/asn1-pkcs8-2.6.0.tgz", + "integrity": "sha512-KyQ4D8G/NrS7Fw3XCJrngxmjwO/3htnA0lL9gDICvEQ+GJ+EPFqldcJQTwPIdvx98Tua+WjkdKHSC0/Km7T+lA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs8/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-pkcs9": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-pkcs9/-/asn1-pkcs9-2.6.0.tgz", + "integrity": "sha512-b78OQ6OciW0aqZxdzliXGYHASeCvvw5caqidbpQRYW2mBtXIX2WhofNXTEe7NyxTb0P6J62kAAWLwn0HuMF1Fw==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.6.0", + "@peculiar/asn1-pfx": "^2.6.0", + "@peculiar/asn1-pkcs8": "^2.6.0", + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "@peculiar/asn1-x509-attr": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-pkcs9/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-rsa": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-rsa/-/asn1-rsa-2.6.0.tgz", + "integrity": "sha512-Nu4C19tsrTsCp9fDrH+sdcOKoVfdfoQQ7S3VqjJU6vedR7tY3RLkQ5oguOIB3zFW33USDUuYZnPEQYySlgha4w==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-rsa/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@peculiar/asn1-schema": { - "version": "2.3.15", - "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.3.15.tgz", - "integrity": "sha512-QPeD8UA8axQREpgR5UTAfu2mqQmm97oUqahDtNdBcfj3qAnoXzFdQW+aNf/tD2WVXF8Fhmftxoj0eMIT++gX2w==", + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-schema/-/asn1-schema-2.6.0.tgz", + "integrity": "sha512-xNLYLBFTBKkCzEZIw842BxytQQATQv+lDTCEMZ8C196iJcJJMBUZxrhSTxLaohMyKK8QlzRNTRkUmanucnDSqg==", "license": "MIT", "dependencies": { - "asn1js": "^3.0.5", + "asn1js": "^3.0.6", "pvtsutils": "^1.3.6", "tslib": "^2.8.1" } @@ -15049,6 +15278,42 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/@peculiar/asn1-x509": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509/-/asn1-x509-2.6.0.tgz", + "integrity": "sha512-uzYbPEpoQiBoTq0/+jZtpM6Gq6zADBx+JNFP3yqRgziWBxQ/Dt/HcuvRfm9zJTPdRcBqPNdaRHTVwpyiq6iNMA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "asn1js": "^3.0.6", + "pvtsutils": "^1.3.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/@peculiar/asn1-x509-attr/-/asn1-x509-attr-2.6.0.tgz", + "integrity": "sha512-MuIAXFX3/dc8gmoZBkwJWxUWOSvG4MMDntXhrOZpJVMkYX+MYc/rUAU2uJOved9iJEoiUx7//3D8oG83a78UJA==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-schema": "^2.6.0", + "@peculiar/asn1-x509": "^2.6.0", + "asn1js": "^3.0.6", + "tslib": "^2.8.1" + } + }, + "node_modules/@peculiar/asn1-x509-attr/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/@peculiar/asn1-x509/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@peculiar/json-schema": { "version": "1.1.12", "resolved": "https://registry.npmjs.org/@peculiar/json-schema/-/json-schema-1.1.12.tgz", @@ -15083,6 +15348,31 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/@peculiar/x509": { + "version": "1.14.0", + "resolved": "https://registry.npmjs.org/@peculiar/x509/-/x509-1.14.0.tgz", + "integrity": "sha512-Yc4PDxN3OrxUPiXgU63c+ZRXKGE8YKF2McTciYhUHFtHVB0KMnjeFSU0qpztGhsp4P0uKix4+J2xEpIEDu8oXg==", + "license": "MIT", + "dependencies": { + "@peculiar/asn1-cms": "^2.5.0", + "@peculiar/asn1-csr": "^2.5.0", + "@peculiar/asn1-ecc": "^2.5.0", + "@peculiar/asn1-pkcs9": "^2.5.0", + "@peculiar/asn1-rsa": "^2.5.0", + "@peculiar/asn1-schema": "^2.5.0", + "@peculiar/asn1-x509": "^2.5.0", + "pvtsutils": "^1.3.6", + "reflect-metadata": "^0.2.2", + "tslib": "^2.8.1", + "tsyringe": "^4.10.0" + } + }, + "node_modules/@peculiar/x509/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, "node_modules/@pkgjs/parseargs": { "version": "0.11.0", "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", @@ -15154,6 +15444,34 @@ "node": ">=12" } }, + "node_modules/@protobuf-ts/grpc-transport": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/grpc-transport/-/grpc-transport-2.11.1.tgz", + "integrity": "sha512-l6wrcFffY+tuNnuyrNCkRM8hDIsAZVLA8Mn7PKdVyYxITosYh60qW663p9kL6TWXYuDCL3oxH8ih3vLKTDyhtg==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1", + "@protobuf-ts/runtime-rpc": "^2.11.1" + }, + "peerDependencies": { + "@grpc/grpc-js": "^1.6.0" + } + }, + "node_modules/@protobuf-ts/runtime": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime/-/runtime-2.11.1.tgz", + "integrity": "sha512-KuDaT1IfHkugM2pyz+FwiY80ejWrkH1pAtOBOZFuR6SXEFTsnb/jiQWQ1rCIrcKx2BtyxnxW6BWwsVSA/Ie+WQ==", + "license": "(Apache-2.0 AND BSD-3-Clause)" + }, + "node_modules/@protobuf-ts/runtime-rpc": { + "version": "2.11.1", + "resolved": "https://registry.npmjs.org/@protobuf-ts/runtime-rpc/-/runtime-rpc-2.11.1.tgz", + "integrity": "sha512-4CqqUmNA+/uMz00+d3CYKgElXO9VrEbucjnBFEjqI4GuDrEQ32MaI3q+9qPBvIGOlL4PmHXrzM32vBPWRhQKWQ==", + "license": "Apache-2.0", + "dependencies": { + "@protobuf-ts/runtime": "^2.11.1" + } + }, "node_modules/@protobufjs/aspromise": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", @@ -15638,6 +15956,19 @@ } } }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.50.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.50.2.tgz", + "integrity": "sha512-9Jie/At6qk70dNIcopcL4p+1UirusEtznpNtcq/u/C5cC4HBX7qSGsYIcG6bdxj15EYWhHiu02YvmdPzylIZlA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, "node_modules/@scarf/scarf": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/@scarf/scarf/-/scarf-1.4.0.tgz", @@ -23157,6 +23488,25 @@ "node": ">=8" } }, + "node_modules/agntcy-dir": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/agntcy-dir/-/agntcy-dir-0.5.6.tgz", + "integrity": "sha512-TPZP1ObFkc6XHTvLu87eoDPQpeW7FJiNydHQRLCmPcm6hTcTbf79HMvmvvf5rS20oO0eb7AwpXciAzvHm+9X9A==", + "license": "Apache-2.0", + "dependencies": { + "@bufbuild/protobuf": "^2.9.0", + "@connectrpc/connect": "^2.1.0", + "@connectrpc/connect-node": "^2.1.0", + "@grpc/grpc-js": "^1.13.4", + "spiffe": "^0.4.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-linux-x64-gnu": "4.50.2" + } + }, "node_modules/ajv": { "version": "8.17.1", "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", @@ -32180,6 +32530,12 @@ "dev": true, "license": "MIT" }, + "node_modules/lodash.camelcase": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", + "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", + "license": "MIT" + }, "node_modules/lodash.capitalize": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/lodash.capitalize/-/lodash.capitalize-4.2.1.tgz", @@ -42908,6 +43264,21 @@ "dev": true, "license": "CC0-1.0" }, + "node_modules/spiffe": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/spiffe/-/spiffe-0.4.0.tgz", + "integrity": "sha512-yjHNB+r2h/ybIIjdwl7TPRHNPfi95oNflRDfUo70w9nV7hrSluPfFecKHDx3X1j3rkO4iuwbssTrwwyJIjpWPw==", + "license": "MIT", + "dependencies": { + "@grpc/grpc-js": "^1.9.11", + "@peculiar/webcrypto": "^1.4.3", + "@peculiar/x509": "^1.9.5", + "@protobuf-ts/grpc-transport": "^2.9.1", + "@protobuf-ts/runtime": "^2.9.1", + "@protobuf-ts/runtime-rpc": "^2.9.1", + "protobufjs": "^7.2.5" + } + }, "node_modules/split-on-first": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/split-on-first/-/split-on-first-1.1.0.tgz", @@ -44382,6 +44753,24 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/tsyringe": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/tsyringe/-/tsyringe-4.10.0.tgz", + "integrity": "sha512-axr3IdNuVIxnaK5XGEUFTu3YmAQ6lllgrvqfEoR16g/HGnYY/6We4oWENtAnzK6/LpJ2ur9PAb80RBt7/U4ugw==", + "license": "MIT", + "dependencies": { + "tslib": "^1.9.3" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/tsyringe/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, "node_modules/tunnel": { "version": "0.0.6", "resolved": "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz", diff --git a/package.json b/package.json index 8b69ed69..e13598d0 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "@verida/encryption-utils": "^3.0.1", "@verida/types": "^3.0.2", "@verida/vda-did-resolver": "^4.4.5", + "agntcy-dir": "^0.5.6", "bcrypt": "^5.1.1", "bs58": "^6.0.0", "cookie-parser": "^1.4.7", diff --git a/src/app.ts b/src/app.ts index 9b79afeb..23c66fd5 100644 --- a/src/app.ts +++ b/src/app.ts @@ -33,6 +33,7 @@ import { OrganisationController } from './controllers/admin/organisation.js'; import { AccreditationController } from './controllers/api/accreditation.js'; import { OperationController } from './controllers/api/operation.js'; import { ProvidersController } from './controllers/api/providers.controller.js'; +import { AgntcyController } from './controllers/api/agntcy.js'; dotenv.config(); @@ -405,6 +406,10 @@ class App { app.get('/admin/organisation/get', new OrganisationController().get); } + app.post('/record/publish', AgntcyController.recordPublishValidator, new AgntcyController().publishRecord); + app.get('/record/search', AgntcyController.recordSearchValidator, new AgntcyController().searchRecord); + app.get('/record/:cid', AgntcyController.recordGetValidator, new AgntcyController().getRecord); + // 404 for all other requests app.all('*', (_req, res) => res.status(StatusCodes.BAD_REQUEST).send('Bad request')); } diff --git a/src/controllers/api/agntcy.ts b/src/controllers/api/agntcy.ts new file mode 100644 index 00000000..c218f92b --- /dev/null +++ b/src/controllers/api/agntcy.ts @@ -0,0 +1,409 @@ +import type { Request, Response } from 'express'; +import { StatusCodes } from 'http-status-codes'; +import { check, query } from 'express-validator'; +import { validate } from '../validator/decorator.js'; +import type { + PublishRecordRequestBody, + PublishRecordResponseBody, + UnsuccessfulPublishRecordResponseBody, + SearchRecordQuery, + SearchRecordResponseBody, + UnsuccessfulSearchRecordResponseBody, + GetRecordParams, + GetRecordQuery, + GetRecordResponseBody, + UnsuccessfulGetRecordResponseBody, +} from '../../types/record.js'; +import { AgntcyService } from '../../services/api/agntcy.js'; + +export class AgntcyController { + // Validators + public static recordPublishValidator = [ + check('data').exists().withMessage('data field is required').bail(), + check('data.name').exists().withMessage('name is required').isString().withMessage('name must be a string').bail(), + check('data.version') + .exists() + .withMessage('version is required') + .matches(/^\d+\.\d+\.\d+$/) + .withMessage('version must follow semantic versioning (e.g., 1.0.0)') + .bail(), + check('data.schema_version') + .exists() + .withMessage('schema_version is required') + .isString() + .withMessage('schema_version must be a string') + .bail(), + check('data.description').optional().isString().withMessage('description must be a string').bail(), + check('data.authors').optional().isArray().withMessage('authors must be an array').bail(), + check('data.type') + .optional() + .isString() + .isIn(['agent', 'organization', 'service', 'mcp-server']) + .withMessage('Invalid record type') + .bail(), + check('data.skills').optional().isArray().withMessage('skills must be an array').bail(), + check('data.locators').optional().isArray().withMessage('locators must be an array').bail(), + check('data.domains').optional().isArray().withMessage('domains must be an array').bail(), + check('data.modules').optional().isArray().withMessage('modules must be an array').bail(), + ]; + + public static recordSearchValidator = [ + query('name').optional().isString().withMessage('name must be a string').bail(), + query('version').optional().isString().withMessage('version must be a string').bail(), + query('skill').optional().isString().withMessage('skill must be a string').bail(), + query('skill_id').optional().isInt().withMessage('skill_id must be an integer').bail(), + query('domain').optional().isString().withMessage('domain must be a string').bail(), + query('locator').optional().isString().withMessage('locator must be a string').bail(), + query('module').optional().isString().withMessage('module must be a string').bail(), + query('type') + .optional() + .isString() + .isIn(['agent', 'organization', 'service', 'mcp-server']) + .withMessage('Invalid record type') + .bail(), + query('page').optional().isInt({ min: 1 }).withMessage('page must be a positive integer').bail(), + query('limit') + .optional() + .isInt({ min: 1, max: 100 }) + .withMessage('limit must be between 1 and 100') + .bail(), + ]; + + public static recordGetValidator = [ + check('cid') + .exists() + .withMessage('cid was not provided') + .isString() + .withMessage('cid must be a string') + .notEmpty() + .withMessage('cid cannot be empty') + .bail(), + query('verify').optional().isBoolean().withMessage('verify must be a boolean').bail(), + ]; + + /** + * @openapi + * + * /record/publish: + * post: + * tags: [ Records ] + * summary: Publish an OASF record to the directory. + * description: This endpoint publishes an OASF-compliant record to the Agent Directory. The record can represent an agent, organization, service, or MCP server. + * requestBody: + * content: + * application/x-www-form-urlencoded: + * schema: + * $ref: '#/components/schemas/PublishRecordRequest' + * application/json: + * schema: + * $ref: '#/components/schemas/PublishRecordRequest' + * responses: + * 201: + * description: The record was successfully published. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/PublishRecordResult' + * 400: + * description: A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidRequest' + * example: + * error: InvalidRequest + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * description: An internal error has occurred. Additional state information plus metadata may be available in the response body. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidRequest' + * example: + * error: Internal Error + */ + @validate + public async publishRecord(request: Request, response: Response) { + const record = request.body as PublishRecordRequestBody; + + // Get directory service + const agntcyService = new AgntcyService(); + + try { + // Validate against OASF schema (optional) + const isValid = await agntcyService.validateOASFSchema(record); + if (!isValid) { + return response.status(StatusCodes.BAD_REQUEST).json({ + error: 'Record does not conform to OASF schema', + } satisfies UnsuccessfulPublishRecordResponseBody); + } + + // Publish to directory + const result = await agntcyService.publish(response.locals.customer, record); + + // Return the response + return response.status(StatusCodes.CREATED).json({ + success: true, + cid: result.cid, + message: 'Record published successfully', + data: { + name: record.data.name, + version: record.data.version, + cid: result.cid, + published_at: new Date().toISOString(), + }, + } satisfies PublishRecordResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies UnsuccessfulPublishRecordResponseBody); + } + } + + /** + * @openapi + * + * /record/search: + * get: + * tags: [ Records ] + * summary: Search for records in the directory. + * description: This endpoint searches for OASF records in the Agent Directory based on various criteria such as name, version, skills, domains, and more. + * parameters: + * - name: name + * description: Name of the record to search for. + * in: query + * schema: + * type: string + * - name: version + * description: Version of the record to search for. + * in: query + * schema: + * type: string + * - name: skill + * description: Skill name to filter by. + * in: query + * schema: + * type: string + * - name: skill_id + * description: Skill ID to filter by. + * in: query + * schema: + * type: integer + * - name: domain + * description: Domain to filter by. + * in: query + * schema: + * type: string + * - name: locator + * description: Locator type to filter by. + * in: query + * schema: + * type: string + * - name: module + * description: Module name to filter by. + * in: query + * schema: + * type: string + * - name: type + * description: Type of record to filter by. + * in: query + * schema: + * type: string + * enum: + * - agent + * - organization + * - service + * - mcp-server + * - name: page + * description: Page number for pagination (default 1). + * in: query + * schema: + * type: integer + * minimum: 1 + * - name: limit + * description: Number of results per page (default 20, max 100). + * in: query + * schema: + * type: integer + * minimum: 1 + * maximum: 100 + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/SearchRecordResult' + * 400: + * description: A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidRequest' + * example: + * error: InvalidRequest + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 500: + * description: An internal error has occurred. Additional state information plus metadata may be available in the response body. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidRequest' + * example: + * error: Internal Error + */ + @validate + public async searchRecord(request: Request, response: Response) { + const query = request.query as SearchRecordQuery; + + // Get directory service + const agntcyService = new AgntcyService(); + + try { + // Set defaults for pagination + const { page = 1, limit = 20 } = request.query as SearchRecordQuery; + + // Search directory + const results = await agntcyService.search( + response.locals.customer, + { + ...query, + page, + limit, + }); + + // Return paginated results + return response.status(StatusCodes.OK).json({ + success: true, + data: results.records, + pagination: { + page, + limit, + total: results.total, + total_pages: Math.ceil(results.total / limit), + }, + filters_applied: this.getAppliedFilters(query), + } satisfies SearchRecordResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies UnsuccessfulSearchRecordResponseBody); + } + } + + /** + * @openapi + * + * /record/{cid}: + * get: + * tags: [ Records ] + * summary: Fetch a record by CID. + * description: This endpoint fetches a specific OASF record from the Agent Directory using its Content Identifier (CID). Optionally, the record's signature can be verified. + * parameters: + * - name: cid + * description: Content Identifier (CID) of the record to fetch. + * in: path + * schema: + * type: string + * required: true + * - name: verify + * description: Whether to verify the record's signature. + * in: query + * schema: + * type: boolean + * responses: + * 200: + * description: The request was successful. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/GetRecordResult' + * 400: + * description: A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidRequest' + * example: + * error: InvalidRequest + * 401: + * $ref: '#/components/schemas/UnauthorizedError' + * 404: + * description: The record was not found. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidRequest' + * example: + * error: Record not found + * 500: + * description: An internal error has occurred. Additional state information plus metadata may be available in the response body. + * content: + * application/json: + * schema: + * $ref: '#/components/schemas/InvalidRequest' + * example: + * error: Internal Error + */ + @validate + public async getRecord(request: Request, response: Response) { + const { cid } = request.params as unknown as GetRecordParams; + const { verify } = request.query as GetRecordQuery; + + // Get directory service + const agntcyService = new AgntcyService(); + + try { + // Fetch record from directory + const record = await agntcyService.getRecord(response.locals.customer, cid); + + if (!record) { + return response.status(StatusCodes.NOT_FOUND).json({ + error: `Record with CID: ${cid} not found`, + } satisfies UnsuccessfulGetRecordResponseBody); + } + + // Optional: Verify signature + let verificationResult = null; + if (verify === 'true' || verify === true) { + verificationResult = await agntcyService.verifyRecord(response.locals.customer, cid); + } + + // Return record with metadata + return response.status(StatusCodes.OK).json({ + success: true, + data: record, + metadata: { + cid, + retrieved_at: new Date().toISOString(), + verified: verificationResult?.verified || null, + signature: verificationResult || null, + }, + } satisfies GetRecordResponseBody); + } catch (error) { + return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ + error: `Internal error: ${(error as Error)?.message || error}`, + } satisfies UnsuccessfulGetRecordResponseBody); + } + } + + /** + * Helper method to extract applied filters from query + */ + private getAppliedFilters(query: SearchRecordQuery): Record { + const filters: Record = {}; + + if (query.name) filters.name = query.name; + if (query.version) filters.version = query.version; + if (query.skill) filters.skill = query.skill; + if (query.skill_id) filters.skill_id = query.skill_id; + if (query.domain) filters.domain = query.domain; + if (query.locator) filters.locator = query.locator; + if (query.module) filters.module = query.module; + if (query.type) filters.type = query.type; + + return filters; + } +} \ No newline at end of file diff --git a/src/middleware/auth/routes/api/record-auth.ts b/src/middleware/auth/routes/api/record-auth.ts new file mode 100644 index 00000000..2f5f28f7 --- /dev/null +++ b/src/middleware/auth/routes/api/record-auth.ts @@ -0,0 +1,10 @@ +import { AuthRuleProvider } from '../auth-rule-provider.js'; + +export class RecordAuthProvider extends AuthRuleProvider { + constructor() { + super(); + this.registerRule('/record/search', 'GET', 'read:account', { skipNamespace: true }); + this.registerRule('/record/publish', 'POST', 'create:account', { skipNamespace: true }); + this.registerRule('/record/:cid', 'GET', 'read:account', { skipNamespace: true }); + } +} diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index 6dfe4759..b6ac0bdd 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -22,6 +22,7 @@ import type { UnsuccessfulResponseBody } from '../types/shared.js'; import { AccreditationAuthRuleProvider } from './auth/routes/api/accreditation-auth.js'; import { EventAuthRuleProvider } from './auth/routes/api/event-auth.js'; import { ProvidersAuthRuleProvider } from './auth/routes/api/provider-auth.js'; +import { RecordAuthProvider } from './auth/routes/api/record-auth.js'; dotenv.config(); @@ -52,6 +53,7 @@ export class Authentication { authRuleRepository.push(new AdminAuthRuleProvider()); authRuleRepository.push(new ProvidersAuthRuleProvider()); + authRuleRepository.push(new RecordAuthProvider()); this.apiGuardian = new APIGuard(authRuleRepository, this.oauthProvider); // Initial auth handler diff --git a/src/services/api/agntcy.ts b/src/services/api/agntcy.ts new file mode 100644 index 00000000..8f71f9c4 --- /dev/null +++ b/src/services/api/agntcy.ts @@ -0,0 +1,498 @@ +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { writeFile, unlink } from 'fs/promises'; +import { CustomerEntity } from '../../database/entities/customer.entity.js'; +import type { PublishRecordRequestBody, SearchRecordQuery, VerificationResult } from '../../types/record.js'; + +const execFileAsync = promisify(execFile); + +/** + * AgntcyService - Wraps dirctl CLI for self-hosted AGNTCY Directory + */ +export class AgntcyService { + private directoryUrl: string; + private oasfSchemaUrl: string; + private dirctlCommand: string; + + constructor() { + this.directoryUrl = process.env.DIRECTORY_SERVER_URL || 'localhost:8888'; + this.oasfSchemaUrl = process.env.OASF_SCHEMA_SERVER_URL || 'https://schema.oasf.outshift.com'; + this.dirctlCommand = process.env.DIRCTL_COMMAND || 'dirctl'; + } + + /** + * Execute dirctl command with proper flags + */ + private async execDirctl(args: string[]): Promise { + try { + const fullArgs = ['--server', this.directoryUrl, ...args]; + + const { stdout, stderr } = await execFileAsync(this.dirctlCommand, fullArgs, { + maxBuffer: 10 * 1024 * 1024, + }); + + if (stderr && !stderr.includes('[Info]') && !stderr.includes('INFO')) { + console.warn('dirctl stderr:', stderr); + } + + return stdout; + } catch (error: any) { + console.error('dirctl error:', error); + const errorMsg = error.stderr || error.message || 'Unknown error'; + throw new Error(`dirctl command failed: ${errorMsg}`); + } + } + + /** + * Convert request body to OASF record format + * Uses data from request instead of hardcoding + */ + private toOASFRecord(body: PublishRecordRequestBody): any { + const { data } = body; + + return { + metadata: { + version: data.schema_version || '1.0.0', + uid: data.uid || data.name, // Use uid if provided, fallback to name + product: { + name: data.name, + vendor_name: data.authors?.[0] || 'Unknown', + version: data.version, + lang: 'en', + url: data.locators?.find((l) => l.type === 'api_endpoint')?.url, + }, + labels: data.type ? [data.type] : [], + }, + record: { + type_uid: '100001', + type_name: data.type === 'mcp-server' ? 'MCP Server' : 'AI Agent', + category_uid: '1', + category_name: 'Agent', + class_uid: '1001', + class_name: data.type || 'Agent', + severity_id: 1, + time: data.created_at || new Date().toISOString(), + }, + // Use data directly from request + skills: data.skills?.map((skill) => ({ + skill_id: skill.name, + skill_name: skill.name, + confidence: 1.0, + })) || [], + domains: data.domains?.map((domain) => ({ + domain_id: domain.name, + domain_name: domain.name, + })) || [], + locators: data.locators || [], + modules: data.modules || [], + }; + } + + /** + * Convert OASF record back to response format + */ + private fromOASFRecord(oasf: any): PublishRecordRequestBody { + return { + data: { + name: oasf.metadata.product.name, + version: oasf.metadata.product.version, + schema_version: oasf.metadata.version, + uid: oasf.metadata.uid, // Preserve identity + description: oasf.metadata.product.url, + authors: [oasf.metadata.product.vendor_name], + created_at: oasf.record.time, + type: oasf.metadata.labels?.[0] as any, + skills: oasf.skills?.map((s: any) => ({ + name: s.skill_name || s.skill_id, + id: 0, + })) || [], + domains: oasf.domains?.map((d: any) => ({ + name: d.domain_name || d.domain_id, + id: 0, + })) || [], + locators: oasf.locators || [], + modules: oasf.modules || [], + }, + }; + } + + /** + * Validate OASF record against schema server + */ + async validateOASFSchema(record: any): Promise { + try { + if (process.env.SKIP_OASF_VALIDATION === 'true') { + return true; + } + + const response = await fetch(`${this.oasfSchemaUrl}/api/validate`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + schema_version: record.metadata.version, + record: record, + }), + signal: AbortSignal.timeout(10000), + }); + + const result = await response.json(); + + if (response.ok && result.valid === true) { + return true; + } + + if (result.errors) { + console.error('OASF validation errors:', result.errors); + } + + return false; + } catch (error) { + console.error('Error validating OASF schema:', error); + console.warn('OASF schema server unreachable, skipping validation'); + return true; + } + } + + /** + * Publish record to directory + */ + async publish(customer: CustomerEntity, record: PublishRecordRequestBody): Promise<{ cid: string }> { + try { + // Convert request body to OASF format + const oasfRecord = this.toOASFRecord(record); + + // Validate + const isValid = await this.validateOASFSchema(oasfRecord); + if (!isValid) { + throw new Error('OASF validation failed'); + } + + // Write to temp file + const tempFile = `/tmp/agntcy-record-${Date.now()}.json`; + await writeFile(tempFile, JSON.stringify(oasfRecord, null, 2)); + + try { + // Push to directory + const output = await this.execDirctl(['push', tempFile, '--output', 'raw']); + const cid = output.trim(); + + if (!cid) { + throw new Error('No CID returned from directory server'); + } + + // Auto-publish to DHT + try { + await this.execDirctl(['routing', 'publish', cid]); + } catch (err) { + console.warn('Failed to publish to DHT:', err); + } + + return { cid }; + } finally { + await unlink(tempFile).catch(() => {}); + } + } catch (error) { + console.error('Error publishing record:', error); + throw new Error(`Failed to publish record: ${(error as Error).message}`); + } + } + + /** + * Search for records in directory + */ + async search(customer: CustomerEntity, query: SearchRecordQuery): Promise<{ records: PublishRecordRequestBody[]; total: number }> { + try { + const args = ['search', '--output', 'json']; + + if (query.name) args.push('--name', query.name); + if (query.version) args.push('--version', query.version); + if (query.skill) args.push('--skill', query.skill); + if (query.domain) args.push('--domain', query.domain); + if (query.locator) args.push('--locator', query.locator); + if (query.type) args.push('--type', query.type); + + const output = await this.execDirctl(args); + + if (!output || output.trim() === '') { + return { records: [], total: 0 }; + } + + const results = JSON.parse(output); + + const records = (results.records || results || []) + .map((item: any) => { + try { + const oasf = typeof item.data === 'string' ? JSON.parse(item.data) : item; + return this.fromOASFRecord(oasf); + } catch (e) { + console.error('Error parsing record:', e); + return null; + } + }) + .filter(Boolean); + + return { + records, + total: records.length, + }; + } catch (error) { + console.error('Error searching records:', error); + throw new Error(`Failed to search records: ${(error as Error).message}`); + } + } + + /** + * Get specific record by CID + */ + async getRecord(customer: CustomerEntity, cid: string): Promise { + try { + const output = await this.execDirctl(['pull', cid, '--output', 'json']); + const data = JSON.parse(output); + const oasf = typeof data.data === 'string' ? JSON.parse(data.data) : data; + + return this.fromOASFRecord(oasf); + } catch (error) { + const errorMsg = (error as Error).message.toLowerCase(); + if (errorMsg.includes('not_found') || errorMsg.includes('not found') || errorMsg.includes('no such')) { + return null; + } + console.error('Error getting record:', error); + throw new Error(`Failed to get record: ${(error as Error).message}`); + } + } + + /** + * Verify record signature + */ + async verifyRecord(customer: CustomerEntity, cid: string): Promise { + try { + const output = await this.execDirctl(['verify', cid, '--output', 'json']); + const result = JSON.parse(output); + + return { + verified: result.verified || false, + signature: result.signature, + signer: result.signer, + timestamp: result.timestamp, + }; + } catch (error) { + console.error('Error verifying record:', error); + return { + verified: false, + error: (error as Error).message, + }; + } + } + + /** + * Sign a record using Sigstore + */ + async signRecord(customer: CustomerEntity, cid: string): Promise<{ signed: boolean; signature?: string }> { + try { + const output = await this.execDirctl(['sign', cid, '--output', 'json']); + const result = JSON.parse(output); + + return { + signed: result.signed || true, + signature: result.signature, + }; + } catch (error) { + console.error('Error signing record:', error); + throw new Error(`Failed to sign record: ${(error as Error).message}`); + } + } + + /** + * Search across distributed network + */ + async searchNetwork(customer: CustomerEntity, query: SearchRecordQuery): Promise<{ records: any[]; total: number }> { + try { + const args = ['routing', 'search', '--output', 'json']; + + if (query.skill) args.push('--skill', query.skill); + if (query.domain) args.push('--domain', query.domain); + + const output = await this.execDirctl(args); + + if (!output || output.trim() === '') { + return { records: [], total: 0 }; + } + + const results = JSON.parse(output); + + return { + records: results.results || [], + total: results.results?.length || 0, + }; + } catch (error) { + console.error('Error searching network:', error); + throw new Error(`Failed to search network: ${(error as Error).message}`); + } + } + + /** + * Sync records from remote directory + */ + async syncFromRemote(customer: CustomerEntity, remoteUrl: string, cids?: string[]): Promise<{ synced: number; errors: string[] }> { + try { + const args = ['sync', 'create', remoteUrl, '--output', 'json']; + + if (cids && cids.length > 0) { + args.push('--cids', cids.join(',')); + } + + const output = await this.execDirctl(args); + const result = JSON.parse(output); + + return { + synced: result.synced || 0, + errors: result.errors || [], + }; + } catch (error) { + console.error('Error syncing records:', error); + throw new Error(`Failed to sync records: ${(error as Error).message}`); + } + } + + /** + * Get OASF skills taxonomy + */ + async getSkillsTaxonomy(): Promise { + try { + const response = await fetch(`${this.oasfSchemaUrl}/api/skills`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + signal: AbortSignal.timeout(10000), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch skills: ${response.statusText}`); + } + + const data = await response.json(); + return data.skills || data || []; + } catch (error) { + console.error('Error fetching skills taxonomy:', error); + return []; + } + } + + /** + * Get OASF domains taxonomy + */ + async getDomainsTaxonomy(): Promise { + try { + const response = await fetch(`${this.oasfSchemaUrl}/api/domains`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + signal: AbortSignal.timeout(10000), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch domains: ${response.statusText}`); + } + + const data = await response.json(); + return data.domains || data || []; + } catch (error) { + console.error('Error fetching domains taxonomy:', error); + return []; + } + } + + /** + * Get OASF modules + */ + async getModules(): Promise { + try { + const response = await fetch(`${this.oasfSchemaUrl}/api/modules`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + signal: AbortSignal.timeout(10000), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch modules: ${response.statusText}`); + } + + const data = await response.json(); + return data.modules || data || []; + } catch (error) { + console.error('Error fetching modules:', error); + return []; + } + } + + /** + * Get OASF schema versions + */ + async getSchemaVersions(): Promise { + try { + const response = await fetch(`${this.oasfSchemaUrl}/api/versions`, { + method: 'GET', + headers: { 'Content-Type': 'application/json' }, + signal: AbortSignal.timeout(10000), + }); + + if (!response.ok) { + throw new Error(`Failed to fetch versions: ${response.statusText}`); + } + + const data = await response.json(); + return data.versions || data || []; + } catch (error) { + console.error('Error fetching schema versions:', error); + return ['0.7.0']; + } + } + + /** + * Delete a record by CID (unpublishes from DHT) + */ + async deleteRecord(customer: CustomerEntity, cid: string): Promise<{ deleted: boolean }> { + try { + await this.execDirctl(['routing', 'unpublish', cid]); + return { deleted: true }; + } catch (error) { + console.error('Error deleting record:', error); + throw new Error(`Failed to delete record: ${(error as Error).message}`); + } + } + + /** + * Update a record (creates new version with new CID) + */ + async updateRecord(customer: CustomerEntity, oldCid: string, record: PublishRecordRequestBody): Promise<{ cid: string }> { + try { + const oasfRecord = this.toOASFRecord(record); + + // Add reference to previous version + if (!oasfRecord.metadata.labels) { + oasfRecord.metadata.labels = []; + } + oasfRecord.metadata.labels.push(`prev_version:${oldCid}`); + + // Unpublish old version + await this.deleteRecord(customer, oldCid); + + // Publish new version + return await this.publish(customer, record); + } catch (error) { + console.error('Error updating record:', error); + throw new Error(`Failed to update record: ${(error as Error).message}`); + } + } + + /** + * Get info about a record (metadata without pulling full content) + */ + async getRecordInfo(customer: CustomerEntity, cid: string): Promise { + try { + const output = await this.execDirctl(['info', cid, '--output', 'json']); + return JSON.parse(output); + } catch (error) { + console.error('Error getting record info:', error); + throw new Error(`Failed to get record info: ${(error as Error).message}`); + } + } +} \ No newline at end of file diff --git a/src/static/swagger-api.json b/src/static/swagger-api.json index 4732eeff..13511523 100644 --- a/src/static/swagger-api.json +++ b/src/static/swagger-api.json @@ -3247,6 +3247,144 @@ } } } + }, + "PublishRecordRequest": { + "type": "object", + "required": [ + "data" + ], + "properties": { + "data": { + "type": "object", + "required": [ + "name", + "version", + "schema_version" + ], + "properties": { + "name": { + "type": "string", + "description": "Name of the record" + }, + "version": { + "type": "string", + "description": "Version of the record" + }, + "schema_version": { + "type": "string", + "description": "Schema version" + }, + "description": { + "type": "string", + "description": "Optional description" + }, + "authors": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Optional list of authors" + }, + "created_at": { + "type": "string", + "format": "date-time", + "description": "Optional creation timestamp" + }, + "type": { + "type": "string", + "enum": [ + "agent", + "organization", + "service", + "mcp-server" + ], + "description": "Optional record type" + }, + "skills": { + "type": "array", + "items": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "number" + }, + "locators": { + "type": "array" + }, + "items": { + "type": "object", + "properties": { + "type": { + "type": "string" + }, + "url": { + "type": "string" + }, + "domains": { + "type": "array", + "items": { + "type": "object" + }, + "properties": { + "name": { + "type": "string" + }, + "id": { + "type": "number" + }, + "modules": { + "type": "array", + "items": { + "type": "object" + } + } + } + } + } + } + } + } + } + } + } + } + }, + "PublishRecordResult": { + "type": "object", + "properties": { + "success": { + "type": "boolean", + "example": true + }, + "cid": { + "type": "string", + "description": "Content identifier of the published record" + }, + "message": { + "type": "string" + }, + "data": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "version": { + "type": "string" + }, + "cid": { + "type": "string" + }, + "published_at": { + "type": "string", + "format": "date-time" + } + } + } + } } } }, @@ -3789,6 +3927,293 @@ } } }, + "/record/publish": { + "post": { + "tags": [ + "Records" + ], + "summary": "Publish an OASF record to the directory.", + "description": "This endpoint publishes an OASF-compliant record to the Agent Directory. The record can represent an agent, organization, service, or MCP server.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/PublishRecordRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishRecordRequest" + } + } + } + }, + "responses": { + "201": { + "description": "The record was successfully published.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PublishRecordResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/record/search": { + "get": { + "tags": [ + "Records" + ], + "summary": "Search for records in the directory.", + "description": "This endpoint searches for OASF records in the Agent Directory based on various criteria such as name, version, skills, domains, and more.", + "parameters": [ + { + "name": "name", + "description": "Name of the record to search for.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "version", + "description": "Version of the record to search for.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "skill", + "description": "Skill name to filter by.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "skill_id", + "description": "Skill ID to filter by.", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "domain", + "description": "Domain to filter by.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "locator", + "description": "Locator type to filter by.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "module", + "description": "Module name to filter by.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "description": "Type of record to filter by.", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "agent", + "organization", + "service", + "mcp-server" + ] + } + }, + { + "name": "page", + "description": "Page number for pagination (default 1).", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "limit", + "description": "Number of results per page (default 20, max 100).", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/SearchRecordResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/record/{cid}": { + "get": { + "tags": [ + "Records" + ], + "summary": "Fetch a record by CID.", + "description": "This endpoint fetches a specific OASF record from the Agent Directory using its Content Identifier (CID). Optionally, the record's signature can be verified.", + "parameters": [ + { + "name": "cid", + "description": "Content Identifier (CID) of the record to fetch.", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "verify", + "description": "Whether to verify the record's signature.", + "in": "query", + "schema": { + "type": "boolean" + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetRecordResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "404": { + "description": "The record was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Record not found" + } + } + } + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, "/credential-status/create/unencrypted": { "post": { "tags": [ diff --git a/src/types/record.ts b/src/types/record.ts new file mode 100644 index 00000000..f484d106 --- /dev/null +++ b/src/types/record.ts @@ -0,0 +1,335 @@ +// Request/Response types for OASF records + +export interface UnsuccessfulPublishRecordResponseBody { + error: string; +} + +export interface SearchRecordResponseBody { + success: true; + data: PublishRecordRequestBody[]; + pagination: { + page: number; + limit: number; + total: number; + total_pages: number; + }; + filters_applied: Record; +} + +export interface UnsuccessfulSearchRecordResponseBody { + error: string; +} + +export interface GetRecordParams { + cid: string; +} + +export interface GetRecordQuery { + verify?: boolean | string; +} + +export interface GetRecordResponseBody { + success: true; + data: PublishRecordRequestBody; + metadata: { + cid: string; + retrieved_at: string; + verified: boolean | null; + signature: VerificationResult | null; + }; +} + +export interface UnsuccessfulGetRecordResponseBody { + error: string; +} + +/** + * OASF Record type definition + */ +export interface OASFRecord { + metadata: { + version: string; + uid: string; // ← Identity field! + product: { + name: string; + vendor_name: string; + version: string; + lang?: string; + url?: string; + }; + labels?: string[]; + }; + record: { + type_uid: string; + type_name: string; + category_uid: string; + category_name: string; + class_uid: string; + class_name: string; + severity_id: number; + time: string; + }; + skills?: Array<{ + skill_id: string; + skill_name: string; + confidence?: number; + }>; + domains?: Array<{ + domain_id: string; + domain_name: string; + }>; + locators?: Array<{ + type: string; + url: string; + description?: string; + }>; + modules?: any[]; +} + + +/** + * Updated interfaces for AGNTCY Directory with identity support + */ + +export interface PublishRecordRequestBody { + data: { + // Core fields + name: string; + version: string; + schema_version: string; + + // Identity field - NEW! ✨ + uid?: string; // DID, OAuth2 client_id, URL, or any unique identifier + + // Optional metadata + description?: string; + authors?: string[]; + created_at?: string; + type?: 'agent' | 'organization' | 'service' | 'mcp-server'; + + // Capabilities + skills?: Array<{ + name: string; + id: number; + }>; + + // Deployment info + locators?: Array<{ + type: string; + url: string; + description?: string; // Optional description + }>; + + // Industry/domain + domains?: Array<{ + name: string; + id: number; + }>; + + // Extensions + modules?: any[]; + }; +} + +export interface PublishRecordResponseBody { + success: true; + cid: string; + message: string; + data: { + name: string; + version: string; + cid: string; + published_at: string; + uid?: string; // Include identity in response + }; +} + +/** + * Search query interface + */ +export interface SearchRecordQuery { + // Text search + name?: string; + version?: string; + description?: string; + + // Identity search - NEW! ✨ + uid?: string; // Search by identity + + // Capability search + skill?: string; + skill_id?: number; + domain?: string; + + // Deployment search + locator?: string; + module?: string; + type?: 'agent' | 'organization' | 'service' | 'mcp-server'; + + // Pagination + page?: number; + limit?: number; +} + +/** + * Verification result interface + */ +export interface VerificationResult { + verified: boolean; + signature?: string; + signer?: string; + timestamp?: string; + error?: string; +} + +/** + * Full OASF record structure (used internally) + */ +export interface OASFRecord { + metadata: { + version: string; + uid: string; // ← IDENTITY FIELD! Can be DID, URL, OAuth2 ID, etc. + product: { + name: string; + vendor_name: string; + version: string; + lang?: string; + url?: string; + }; + labels?: string[]; + }; + record: { + type_uid: string; + type_name: string; + category_uid: string; + category_name: string; + class_uid: string; + class_name: string; + severity_id: number; + time: string; + }; + skills?: Array<{ + skill_id: string; + skill_name: string; + confidence?: number; + }>; + domains?: Array<{ + domain_id: string; + domain_name: string; + }>; + locators?: Array<{ + type: string; + url: string; + description?: string; + }>; + modules?: any[]; + authentication?: { + methods?: string[]; + did?: string; + endpoints?: any[]; + }; +} + +/** + * Example usage with identity: + */ +export const exampleWithIdentity: PublishRecordRequestBody = { + data: { + // Core fields + name: "Customer Support Agent", + version: "1.0.0", + schema_version: "1.0.0", + + // Identity - DID from cheqd, Okta, etc. + uid: "did:cheqd:testnet:7bf81a20-1bfe-4584-9a66-2c4a6b1d5e3f", + + // Metadata + description: "AI agent for customer support queries", + authors: ["ACME Corp"], + type: "agent", + + // Capabilities + skills: [ + { name: "customer_support.query_handling", id: 1 }, + { name: "e_commerce.order_tracking", id: 2 } + ], + + // Deployment + locators: [ + { + type: "api_endpoint", + url: "https://api.acme.com/agents/support" + }, + { + type: "did", + url: "did:cheqd:testnet:7bf81a20-1bfe-4584-9a66-2c4a6b1d5e3f", + description: "Agent DID" + }, + { + type: "badge", + url: "https://identity.acme.com/v1alpha1/vc/abc123/.well-known/vcs.json", + description: "Verifiable credential badge" + } + ], + + // Industry + domains: [ + { name: "e_commerce", id: 1 }, + { name: "customer_service", id: 2 } + ] + } +}; + +/** + * Example without identity (fallback to name): + */ +export const exampleWithoutIdentity: PublishRecordRequestBody = { + data: { + name: "Simple Agent", + version: "1.0.0", + schema_version: "1.0.0", + // uid is optional - will use name as identifier if not provided + skills: [ + { name: "text_generation", id: 1 } + ] + } +}; + +/** + * Type guard to check if record has identity + */ +export function hasIdentity(record: PublishRecordRequestBody): boolean { + return !!record.data.uid && record.data.uid !== record.data.name; +} + +/** + * Extract identity from record + */ +export function getIdentity(record: PublishRecordRequestBody): string { + return record.data.uid || record.data.name; +} + +/** + * Check if identity is a DID + */ +export function isDID(uid: string): boolean { + return uid.startsWith('did:'); +} + +/** + * Check if identity is a URL + */ +export function isURL(uid: string): boolean { + return uid.startsWith('http://') || uid.startsWith('https://'); +} + +/** + * Parse identity type + */ +export function getIdentityType(uid: string): 'did' | 'url' | 'oauth2' | 'uuid' | 'name' { + if (uid.startsWith('did:')) return 'did'; + if (uid.startsWith('http://') || uid.startsWith('https://')) return 'url'; + if (uid.startsWith('urn:uuid:')) return 'uuid'; + if (uid.includes('okta') || uid.includes('auth0') || uid.includes('client')) return 'oauth2'; + return 'name'; +} \ No newline at end of file diff --git a/src/types/swagger-api-types.ts b/src/types/swagger-api-types.ts index 0ce3d4aa..3eb53953 100644 --- a/src/types/swagger-api-types.ts +++ b/src/types/swagger-api-types.ts @@ -2191,4 +2191,99 @@ * type: string * format: date-time * description: Timestamp when status was last updated + * PublishRecordRequest: + * type: object + * required: + * - data + * properties: + * data: + * type: object + * required: + * - name + * - version + * - schema_version + * properties: + * name: + * type: string + * description: Name of the record + * version: + * type: string + * description: Version of the record + * schema_version: + * type: string + * description: Schema version + * description: + * type: string + * description: Optional description + * authors: + * type: array + * items: + * type: string + * description: Optional list of authors + * created_at: + * type: string + * format: date-time + * description: Optional creation timestamp + * type: + * type: string + * enum: + * - agent + * - organization + * - service + * - mcp-server + * description: Optional record type + * skills: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * id: + * type: number + * locators: + * type: array + * items: + * type: object + * properties: + * type: + * type: string + * url: + * type: string + * domains: + * type: array + * items: + * type: object + * properties: + * name: + * type: string + * id: + * type: number + * modules: + * type: array + * items: + * type: object + * PublishRecordResult: + * type: object + * properties: + * success: + * type: boolean + * example: true + * cid: + * type: string + * description: Content identifier of the published record + * message: + * type: string + * data: + * type: object + * properties: + * name: + * type: string + * version: + * type: string + * cid: + * type: string + * published_at: + * type: string + * format: date-time */ From 4bef6bd497d12b8a7607c22df11015d7cf37ae46 Mon Sep 17 00:00:00 2001 From: Daev Mithran Date: Tue, 6 Jan 2026 18:25:53 +0530 Subject: [PATCH 2/4] fix params in agncty client --- src/app.ts | 2 +- src/controllers/api/agntcy.ts | 40 +- src/middleware/auth/routes/api/record-auth.ts | 12 +- src/services/api/agntcy.ts | 432 ++++++++++-------- src/types/record.ts | 91 ++-- 5 files changed, 310 insertions(+), 267 deletions(-) diff --git a/src/app.ts b/src/app.ts index 23c66fd5..042aa59e 100644 --- a/src/app.ts +++ b/src/app.ts @@ -409,7 +409,7 @@ class App { app.post('/record/publish', AgntcyController.recordPublishValidator, new AgntcyController().publishRecord); app.get('/record/search', AgntcyController.recordSearchValidator, new AgntcyController().searchRecord); app.get('/record/:cid', AgntcyController.recordGetValidator, new AgntcyController().getRecord); - + // 404 for all other requests app.all('*', (_req, res) => res.status(StatusCodes.BAD_REQUEST).send('Bad request')); } diff --git a/src/controllers/api/agntcy.ts b/src/controllers/api/agntcy.ts index c218f92b..07116168 100644 --- a/src/controllers/api/agntcy.ts +++ b/src/controllers/api/agntcy.ts @@ -20,7 +20,12 @@ export class AgntcyController { // Validators public static recordPublishValidator = [ check('data').exists().withMessage('data field is required').bail(), - check('data.name').exists().withMessage('name is required').isString().withMessage('name must be a string').bail(), + check('data.name') + .exists() + .withMessage('name is required') + .isString() + .withMessage('name must be a string') + .bail(), check('data.version') .exists() .withMessage('version is required') @@ -62,11 +67,7 @@ export class AgntcyController { .withMessage('Invalid record type') .bail(), query('page').optional().isInt({ min: 1 }).withMessage('page must be a positive integer').bail(), - query('limit') - .optional() - .isInt({ min: 1, max: 100 }) - .withMessage('limit must be between 1 and 100') - .bail(), + query('limit').optional().isInt({ min: 1, max: 100 }).withMessage('limit must be between 1 and 100').bail(), ]; public static recordGetValidator = [ @@ -263,12 +264,10 @@ export class AgntcyController { try { // Set defaults for pagination - const { page = 1, limit = 20 } = request.query as SearchRecordQuery; + const { page = 1, limit = 20 } = request.query as SearchRecordQuery; // Search directory - const results = await agntcyService.search( - response.locals.customer, - { + const results = await agntcyService.search(response.locals.customer, { ...query, page, limit, @@ -284,7 +283,6 @@ export class AgntcyController { total: results.total, total_pages: Math.ceil(results.total / limit), }, - filters_applied: this.getAppliedFilters(query), } satisfies SearchRecordResponseBody); } catch (error) { return response.status(StatusCodes.INTERNAL_SERVER_ERROR).json({ @@ -388,22 +386,4 @@ export class AgntcyController { } satisfies UnsuccessfulGetRecordResponseBody); } } - - /** - * Helper method to extract applied filters from query - */ - private getAppliedFilters(query: SearchRecordQuery): Record { - const filters: Record = {}; - - if (query.name) filters.name = query.name; - if (query.version) filters.version = query.version; - if (query.skill) filters.skill = query.skill; - if (query.skill_id) filters.skill_id = query.skill_id; - if (query.domain) filters.domain = query.domain; - if (query.locator) filters.locator = query.locator; - if (query.module) filters.module = query.module; - if (query.type) filters.type = query.type; - - return filters; - } -} \ No newline at end of file +} diff --git a/src/middleware/auth/routes/api/record-auth.ts b/src/middleware/auth/routes/api/record-auth.ts index 2f5f28f7..490030d6 100644 --- a/src/middleware/auth/routes/api/record-auth.ts +++ b/src/middleware/auth/routes/api/record-auth.ts @@ -1,10 +1,10 @@ import { AuthRuleProvider } from '../auth-rule-provider.js'; export class RecordAuthProvider extends AuthRuleProvider { - constructor() { - super(); - this.registerRule('/record/search', 'GET', 'read:account', { skipNamespace: true }); - this.registerRule('/record/publish', 'POST', 'create:account', { skipNamespace: true }); - this.registerRule('/record/:cid', 'GET', 'read:account', { skipNamespace: true }); - } + constructor() { + super(); + this.registerRule('/record/search', 'GET', 'read:account', { skipNamespace: true }); + this.registerRule('/record/publish', 'POST', 'create:account', { skipNamespace: true }); + this.registerRule('/record/(.*)', 'GET', 'read:account', { skipNamespace: true }); + } } diff --git a/src/services/api/agntcy.ts b/src/services/api/agntcy.ts index 8f71f9c4..6bf0b808 100644 --- a/src/services/api/agntcy.ts +++ b/src/services/api/agntcy.ts @@ -1,121 +1,88 @@ -import { execFile } from 'child_process'; -import { promisify } from 'util'; -import { writeFile, unlink } from 'fs/promises'; +import { Client, Config, models } from 'agntcy-dir'; +import { create } from '@bufbuild/protobuf'; import { CustomerEntity } from '../../database/entities/customer.entity.js'; import type { PublishRecordRequestBody, SearchRecordQuery, VerificationResult } from '../../types/record.js'; -const execFileAsync = promisify(execFile); - /** - * AgntcyService - Wraps dirctl CLI for self-hosted AGNTCY Directory + * AgntcyService - Uses official agntcy-dir npm package + * Based on official examples from agntcy/dir repository + * + * Installation: + * npm install agntcy-dir @bufbuild/protobuf */ export class AgntcyService { - private directoryUrl: string; + private client: Client; private oasfSchemaUrl: string; - private dirctlCommand: string; constructor() { - this.directoryUrl = process.env.DIRECTORY_SERVER_URL || 'localhost:8888'; - this.oasfSchemaUrl = process.env.OASF_SCHEMA_SERVER_URL || 'https://schema.oasf.outshift.com'; - this.dirctlCommand = process.env.DIRCTL_COMMAND || 'dirctl'; - } - - /** - * Execute dirctl command with proper flags - */ - private async execDirctl(args: string[]): Promise { - try { - const fullArgs = ['--server', this.directoryUrl, ...args]; - - const { stdout, stderr } = await execFileAsync(this.dirctlCommand, fullArgs, { - maxBuffer: 10 * 1024 * 1024, - }); + const serverAddress = process.env.DIRECTORY_SERVER_URL || 'localhost:8888'; + const dirctlPath = process.env.DIRCTL_PATH || '/usr/local/bin/dirctl'; - if (stderr && !stderr.includes('[Info]') && !stderr.includes('INFO')) { - console.warn('dirctl stderr:', stderr); - } + this.oasfSchemaUrl = process.env.OASF_SCHEMA_SERVER_URL || 'https://schema.oasf.outshift.com'; - return stdout; - } catch (error: any) { - console.error('dirctl error:', error); - const errorMsg = error.stderr || error.message || 'Unknown error'; - throw new Error(`dirctl command failed: ${errorMsg}`); - } + const config = new Config(serverAddress, dirctlPath); + this.client = new Client(config); } /** * Convert request body to OASF record format - * Uses data from request instead of hardcoding + * Must match the exact structure from the official example */ private toOASFRecord(body: PublishRecordRequestBody): any { const { data } = body; + // Return the exact structure from the example - just the data wrapper return { - metadata: { - version: data.schema_version || '1.0.0', - uid: data.uid || data.name, // Use uid if provided, fallback to name - product: { - name: data.name, - vendor_name: data.authors?.[0] || 'Unknown', - version: data.version, - lang: 'en', - url: data.locators?.find((l) => l.type === 'api_endpoint')?.url, - }, - labels: data.type ? [data.type] : [], - }, - record: { - type_uid: '100001', - type_name: data.type === 'mcp-server' ? 'MCP Server' : 'AI Agent', - category_uid: '1', - category_name: 'Agent', - class_uid: '1001', - class_name: data.type || 'Agent', - severity_id: 1, - time: data.created_at || new Date().toISOString(), + data: { + name: data.name, + version: data.version, + schema_version: data.schema_version || '0.7.0', + description: data.description || '', + authors: data.authors || [], + created_at: data.created_at || new Date().toISOString(), + uid: data.uid, // Include identity if provided + type: data.type, + skills: data.skills || [], + locators: data.locators || [], + domains: data.domains || [], + modules: data.modules || [], }, - // Use data directly from request - skills: data.skills?.map((skill) => ({ - skill_id: skill.name, - skill_name: skill.name, - confidence: 1.0, - })) || [], - domains: data.domains?.map((domain) => ({ - domain_id: domain.name, - domain_name: domain.name, - })) || [], - locators: data.locators || [], - modules: data.modules || [], }; } /** * Convert OASF record back to response format */ - private fromOASFRecord(oasf: any): PublishRecordRequestBody { + private fromOASFRecord(record: any): PublishRecordRequestBody { + const data = record.data || record; + return { data: { - name: oasf.metadata.product.name, - version: oasf.metadata.product.version, - schema_version: oasf.metadata.version, - uid: oasf.metadata.uid, // Preserve identity - description: oasf.metadata.product.url, - authors: [oasf.metadata.product.vendor_name], - created_at: oasf.record.time, - type: oasf.metadata.labels?.[0] as any, - skills: oasf.skills?.map((s: any) => ({ - name: s.skill_name || s.skill_id, - id: 0, - })) || [], - domains: oasf.domains?.map((d: any) => ({ - name: d.domain_name || d.domain_id, - id: 0, - })) || [], - locators: oasf.locators || [], - modules: oasf.modules || [], + name: data.name, + version: data.version, + schema_version: data.schema_version, + uid: data.uid, + description: data.description, + authors: data.authors, + created_at: data.created_at, + type: data.type, + skills: data.skills || [], + locators: data.locators || [], + domains: data.domains || [], + modules: data.modules || [], }, }; } + /** + * Create a RecordRef protobuf message + */ + private createRecordRef(cid: string) { + return create(models.core_v1.RecordRefSchema, { + cid: cid, + }); + } + /** * Validate OASF record against schema server */ @@ -129,7 +96,7 @@ export class AgntcyService { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ - schema_version: record.metadata.version, + schema_version: record.data?.schema_version || '0.7.0', record: record, }), signal: AbortSignal.timeout(10000), @@ -158,39 +125,54 @@ export class AgntcyService { */ async publish(customer: CustomerEntity, record: PublishRecordRequestBody): Promise<{ cid: string }> { try { - // Convert request body to OASF format + // Convert to OASF format (match example structure exactly) const oasfRecord = this.toOASFRecord(record); - // Validate - const isValid = await this.validateOASFSchema(oasfRecord); - if (!isValid) { - throw new Error('OASF validation failed'); + // Remove undefined/null fields to avoid protobuf errors + const cleanRecord = JSON.parse( + JSON.stringify(oasfRecord, (key, value) => { + // Remove undefined and null values + if (value === undefined || value === null) { + return undefined; + } + // Remove empty strings for optional fields + if (value === '' && key !== 'description') { + return undefined; + } + return value; + }) + ); + + console.log('Publishing record:', JSON.stringify(cleanRecord, null, 2)); + + // Push records + const pushed_refs = await this.client.push([cleanRecord]); + + if (!pushed_refs || pushed_refs.length === 0) { + throw new Error('No CID returned from directory server'); } - // Write to temp file - const tempFile = `/tmp/agntcy-record-${Date.now()}.json`; - await writeFile(tempFile, JSON.stringify(oasfRecord, null, 2)); + const cid = pushed_refs[0].cid; + // Auto-publish to DHT for discovery try { - // Push to directory - const output = await this.execDirctl(['push', tempFile, '--output', 'raw']); - const cid = output.trim(); - - if (!cid) { - throw new Error('No CID returned from directory server'); - } - - // Auto-publish to DHT - try { - await this.execDirctl(['routing', 'publish', cid]); - } catch (err) { - console.warn('Failed to publish to DHT:', err); - } - - return { cid }; - } finally { - await unlink(tempFile).catch(() => {}); + const recordRefs = create(models.routing_v1.RecordRefsSchema, { + refs: pushed_refs, + }); + + const publishRequest = create(models.routing_v1.PublishRequestSchema, { + request: { + case: 'recordRefs', + value: recordRefs, + }, + }); + + await this.client.publish(publishRequest); + } catch (err) { + console.warn('Failed to publish to DHT:', err); } + + return { cid }; } catch (error) { console.error('Error publishing record:', error); throw new Error(`Failed to publish record: ${(error as Error).message}`); @@ -200,36 +182,76 @@ export class AgntcyService { /** * Search for records in directory */ - async search(customer: CustomerEntity, query: SearchRecordQuery): Promise<{ records: PublishRecordRequestBody[]; total: number }> { + async search( + customer: CustomerEntity, + query: SearchRecordQuery + ): Promise<{ records: PublishRecordRequestBody[]; total: number }> { try { - const args = ['search', '--output', 'json']; + // Build search queries array + const queries: any[] = []; + + if (query.skill) { + queries.push({ + type: models.search_v1.RecordQueryType.SKILL_NAME, + value: query.skill, + }); + } - if (query.name) args.push('--name', query.name); - if (query.version) args.push('--version', query.version); - if (query.skill) args.push('--skill', query.skill); - if (query.domain) args.push('--domain', query.domain); - if (query.locator) args.push('--locator', query.locator); - if (query.type) args.push('--type', query.type); + if (query.skill_id) { + queries.push({ + type: models.search_v1.RecordQueryType.SKILL_ID, + value: query.skill_id.toString(), + }); + } - const output = await this.execDirctl(args); + if (query.name) { + queries.push({ + type: models.search_v1.RecordQueryType.NAME, + value: query.name, + }); + } - if (!output || output.trim() === '') { - return { records: [], total: 0 }; + if (query.domain) { + queries.push({ + type: models.search_v1.RecordQueryType.DOMAIN_NAME, + value: query.domain, + }); } - const results = JSON.parse(output); + // Create search request using protobuf + const searchRequest = create(models.search_v1.SearchRecordsRequestSchema, { + queries: queries.length > 0 ? queries : [], + limit: query.limit || 20, + offset: query.page ? (query.page - 1) * (query.limit || 20) : 0, + }); + + console.log('Search request:', JSON.stringify(searchRequest, null, 2)); + + // Search using SDK + const results = await this.client.searchRecords(searchRequest); + + console.log('Search results:', results); - const records = (results.records || results || []) - .map((item: any) => { + if (!results || results.length === 0) { + return { records: [], total: 0 }; + } + + // Convert records to response format + const records = results + .map((response: any) => { try { - const oasf = typeof item.data === 'string' ? JSON.parse(item.data) : item; - return this.fromOASFRecord(oasf); + // SearchRecordsResponse contains an optional 'record' field + if (!response || !response.record) { + console.warn('Response missing record field:', response); + return null; + } + return this.fromOASFRecord(response.record); } catch (e) { console.error('Error parsing record:', e); return null; } }) - .filter(Boolean); + .filter(Boolean) as PublishRecordRequestBody[]; return { records, @@ -246,14 +268,20 @@ export class AgntcyService { */ async getRecord(customer: CustomerEntity, cid: string): Promise { try { - const output = await this.execDirctl(['pull', cid, '--output', 'json']); - const data = JSON.parse(output); - const oasf = typeof data.data === 'string' ? JSON.parse(data.data) : data; + // Create RecordRef using protobuf constructor + const recordRef = this.createRecordRef(cid); - return this.fromOASFRecord(oasf); + // Pull record using SDK + const results = await this.client.pull([recordRef]); + + if (!results || results.length === 0) { + return null; + } + + return this.fromOASFRecord(results[0]); } catch (error) { const errorMsg = (error as Error).message.toLowerCase(); - if (errorMsg.includes('not_found') || errorMsg.includes('not found') || errorMsg.includes('no such')) { + if (errorMsg.includes('not found') || errorMsg.includes('not_found')) { return null; } console.error('Error getting record:', error); @@ -266,14 +294,13 @@ export class AgntcyService { */ async verifyRecord(customer: CustomerEntity, cid: string): Promise { try { - const output = await this.execDirctl(['verify', cid, '--output', 'json']); - const result = JSON.parse(output); + // Note: The SDK might have a different verify API + // For now, return unverified since we don't have signature in the example + console.warn('Verify not implemented in example - returning unverified'); return { - verified: result.verified || false, - signature: result.signature, - signer: result.signer, - timestamp: result.timestamp, + verified: false, + error: 'Verification not implemented', }; } catch (error) { console.error('Error verifying record:', error); @@ -286,16 +313,15 @@ export class AgntcyService { /** * Sign a record using Sigstore + * Note: Requires dirctl binary for OIDC signing */ async signRecord(customer: CustomerEntity, cid: string): Promise<{ signed: boolean; signature?: string }> { try { - const output = await this.execDirctl(['sign', cid, '--output', 'json']); - const result = JSON.parse(output); + // Note: Sign not shown in example + // This would require dirctl and OIDC flow + console.warn('Sign requires dirctl and OIDC - not implemented'); - return { - signed: result.signed || true, - signature: result.signature, - }; + throw new Error('Signing requires dirctl binary and OIDC authentication'); } catch (error) { console.error('Error signing record:', error); throw new Error(`Failed to sign record: ${(error as Error).message}`); @@ -305,24 +331,40 @@ export class AgntcyService { /** * Search across distributed network */ - async searchNetwork(customer: CustomerEntity, query: SearchRecordQuery): Promise<{ records: any[]; total: number }> { + async searchNetwork( + customer: CustomerEntity, + query: SearchRecordQuery + ): Promise<{ records: any[]; total: number }> { try { - const args = ['routing', 'search', '--output', 'json']; - - if (query.skill) args.push('--skill', query.skill); - if (query.domain) args.push('--domain', query.domain); - - const output = await this.execDirctl(args); + // Build routing queries + const queries: any[] = []; + + if (query.skill) { + queries.push({ + type: models.routing_v1.RecordQueryType.SKILL, + value: query.skill, + }); + } - if (!output || output.trim() === '') { - return { records: [], total: 0 }; + if (query.domain) { + queries.push({ + type: models.routing_v1.RecordQueryType.DOMAIN, + value: query.domain, + }); } - const results = JSON.parse(output); + // Create ListRequest using protobuf constructor + const listRequest = create(models.routing_v1.ListRequestSchema, { + queries: queries.length > 0 ? queries : [], + limit: query.limit || 20, + }); + + // List published records + const results = await this.client.list(listRequest); return { - records: results.results || [], - total: results.results?.length || 0, + records: results || [], + total: results?.length || 0, }; } catch (error) { console.error('Error searching network:', error); @@ -333,20 +375,18 @@ export class AgntcyService { /** * Sync records from remote directory */ - async syncFromRemote(customer: CustomerEntity, remoteUrl: string, cids?: string[]): Promise<{ synced: number; errors: string[] }> { + async syncFromRemote( + customer: CustomerEntity, + remoteUrl: string, + cids?: string[] + ): Promise<{ synced: number; errors: string[] }> { try { - const args = ['sync', 'create', remoteUrl, '--output', 'json']; - - if (cids && cids.length > 0) { - args.push('--cids', cids.join(',')); - } - - const output = await this.execDirctl(args); - const result = JSON.parse(output); + // Note: Sync not shown in example + console.warn('Sync not implemented in example'); return { - synced: result.synced || 0, - errors: result.errors || [], + synced: 0, + errors: ['Sync not implemented'], }; } catch (error) { console.error('Error syncing records:', error); @@ -451,7 +491,28 @@ export class AgntcyService { */ async deleteRecord(customer: CustomerEntity, cid: string): Promise<{ deleted: boolean }> { try { - await this.execDirctl(['routing', 'unpublish', cid]); + // Create RecordRef using protobuf constructor + const recordRef = this.createRecordRef(cid); + + // Create RecordRefs using protobuf constructor + const recordRefs = create(models.routing_v1.RecordRefsSchema, { + refs: [recordRef], + }); + + // Create UnpublishRequest using protobuf constructor + const unpublishRequest = create(models.routing_v1.UnpublishRequestSchema, { + request: { + case: 'recordRefs', + value: recordRefs, + }, + }); + + // Unpublish from DHT + await this.client.unpublish(unpublishRequest); + + // Delete from storage + await this.client.delete([recordRef]); + return { deleted: true }; } catch (error) { console.error('Error deleting record:', error); @@ -462,17 +523,13 @@ export class AgntcyService { /** * Update a record (creates new version with new CID) */ - async updateRecord(customer: CustomerEntity, oldCid: string, record: PublishRecordRequestBody): Promise<{ cid: string }> { + async updateRecord( + customer: CustomerEntity, + oldCid: string, + record: PublishRecordRequestBody + ): Promise<{ cid: string }> { try { - const oasfRecord = this.toOASFRecord(record); - - // Add reference to previous version - if (!oasfRecord.metadata.labels) { - oasfRecord.metadata.labels = []; - } - oasfRecord.metadata.labels.push(`prev_version:${oldCid}`); - - // Unpublish old version + // Delete old version await this.deleteRecord(customer, oldCid); // Publish new version @@ -488,11 +545,20 @@ export class AgntcyService { */ async getRecordInfo(customer: CustomerEntity, cid: string): Promise { try { - const output = await this.execDirctl(['info', cid, '--output', 'json']); - return JSON.parse(output); + // Create RecordRef using protobuf constructor + const recordRef = this.createRecordRef(cid); + + // Lookup metadata + const results = await this.client.lookup([recordRef]); + + if (!results || results.length === 0) { + throw new Error('Record not found'); + } + + return results[0]; } catch (error) { console.error('Error getting record info:', error); throw new Error(`Failed to get record info: ${(error as Error).message}`); } } -} \ No newline at end of file +} diff --git a/src/types/record.ts b/src/types/record.ts index f484d106..a03ab775 100644 --- a/src/types/record.ts +++ b/src/types/record.ts @@ -13,7 +13,7 @@ export interface SearchRecordResponseBody { total: number; total_pages: number; }; - filters_applied: Record; + filters_applied?: Record; } export interface UnsuccessfulSearchRecordResponseBody { @@ -86,7 +86,6 @@ export interface OASFRecord { modules?: any[]; } - /** * Updated interfaces for AGNTCY Directory with identity support */ @@ -97,35 +96,35 @@ export interface PublishRecordRequestBody { name: string; version: string; schema_version: string; - + // Identity field - NEW! ✨ uid?: string; // DID, OAuth2 client_id, URL, or any unique identifier - + // Optional metadata description?: string; authors?: string[]; created_at?: string; type?: 'agent' | 'organization' | 'service' | 'mcp-server'; - + // Capabilities skills?: Array<{ name: string; id: number; }>; - + // Deployment info locators?: Array<{ type: string; url: string; description?: string; // Optional description }>; - + // Industry/domain domains?: Array<{ name: string; id: number; }>; - + // Extensions modules?: any[]; }; @@ -152,20 +151,20 @@ export interface SearchRecordQuery { name?: string; version?: string; description?: string; - + // Identity search - NEW! ✨ uid?: string; // Search by identity - + // Capability search skill?: string; skill_id?: number; domain?: string; - + // Deployment search locator?: string; module?: string; type?: 'agent' | 'organization' | 'service' | 'mcp-server'; - + // Pagination page?: number; limit?: number; @@ -236,48 +235,48 @@ export interface OASFRecord { export const exampleWithIdentity: PublishRecordRequestBody = { data: { // Core fields - name: "Customer Support Agent", - version: "1.0.0", - schema_version: "1.0.0", - + name: 'Customer Support Agent', + version: '1.0.0', + schema_version: '1.0.0', + // Identity - DID from cheqd, Okta, etc. - uid: "did:cheqd:testnet:7bf81a20-1bfe-4584-9a66-2c4a6b1d5e3f", - + uid: 'did:cheqd:testnet:7bf81a20-1bfe-4584-9a66-2c4a6b1d5e3f', + // Metadata - description: "AI agent for customer support queries", - authors: ["ACME Corp"], - type: "agent", - + description: 'AI agent for customer support queries', + authors: ['ACME Corp'], + type: 'agent', + // Capabilities skills: [ - { name: "customer_support.query_handling", id: 1 }, - { name: "e_commerce.order_tracking", id: 2 } + { name: 'customer_support.query_handling', id: 1 }, + { name: 'e_commerce.order_tracking', id: 2 }, ], - + // Deployment locators: [ { - type: "api_endpoint", - url: "https://api.acme.com/agents/support" + type: 'api_endpoint', + url: 'https://api.acme.com/agents/support', }, { - type: "did", - url: "did:cheqd:testnet:7bf81a20-1bfe-4584-9a66-2c4a6b1d5e3f", - description: "Agent DID" + type: 'did', + url: 'did:cheqd:testnet:7bf81a20-1bfe-4584-9a66-2c4a6b1d5e3f', + description: 'Agent DID', }, { - type: "badge", - url: "https://identity.acme.com/v1alpha1/vc/abc123/.well-known/vcs.json", - description: "Verifiable credential badge" - } + type: 'badge', + url: 'https://identity.acme.com/v1alpha1/vc/abc123/.well-known/vcs.json', + description: 'Verifiable credential badge', + }, ], - + // Industry domains: [ - { name: "e_commerce", id: 1 }, - { name: "customer_service", id: 2 } - ] - } + { name: 'e_commerce', id: 1 }, + { name: 'customer_service', id: 2 }, + ], + }, }; /** @@ -285,14 +284,12 @@ export const exampleWithIdentity: PublishRecordRequestBody = { */ export const exampleWithoutIdentity: PublishRecordRequestBody = { data: { - name: "Simple Agent", - version: "1.0.0", - schema_version: "1.0.0", + name: 'Simple Agent', + version: '1.0.0', + schema_version: '1.0.0', // uid is optional - will use name as identifier if not provided - skills: [ - { name: "text_generation", id: 1 } - ] - } + skills: [{ name: 'text_generation', id: 1 }], + }, }; /** @@ -332,4 +329,4 @@ export function getIdentityType(uid: string): 'did' | 'url' | 'oauth2' | 'uuid' if (uid.startsWith('urn:uuid:')) return 'uuid'; if (uid.includes('okta') || uid.includes('auth0') || uid.includes('client')) return 'oauth2'; return 'name'; -} \ No newline at end of file +} From 3a3369c4b1215693d97dba4f64761f7d4db4b2e4 Mon Sep 17 00:00:00 2001 From: Daev Mithran Date: Wed, 7 Jan 2026 13:51:33 +0530 Subject: [PATCH 3/4] Rename to oasf --- src/app.ts | 8 +- src/controllers/api/{agntcy.ts => oasf.ts} | 28 +- src/middleware/auth/routes/api/oasf-auth.ts | 10 + src/middleware/auth/routes/api/record-auth.ts | 10 - src/middleware/authentication.ts | 4 +- src/services/api/{agntcy.ts => oasf.ts} | 215 ++--- src/static/swagger-api.json | 770 +++++++++--------- src/types/{record.ts => oasf.ts} | 112 +-- 8 files changed, 501 insertions(+), 656 deletions(-) rename src/controllers/api/{agntcy.ts => oasf.ts} (94%) create mode 100644 src/middleware/auth/routes/api/oasf-auth.ts delete mode 100644 src/middleware/auth/routes/api/record-auth.ts rename src/services/api/{agntcy.ts => oasf.ts} (84%) rename src/types/{record.ts => oasf.ts} (62%) diff --git a/src/app.ts b/src/app.ts index 042aa59e..c3e8b8b1 100644 --- a/src/app.ts +++ b/src/app.ts @@ -33,7 +33,7 @@ import { OrganisationController } from './controllers/admin/organisation.js'; import { AccreditationController } from './controllers/api/accreditation.js'; import { OperationController } from './controllers/api/operation.js'; import { ProvidersController } from './controllers/api/providers.controller.js'; -import { AgntcyController } from './controllers/api/agntcy.js'; +import { OasfController } from './controllers/api/oasf.js'; dotenv.config(); @@ -406,9 +406,9 @@ class App { app.get('/admin/organisation/get', new OrganisationController().get); } - app.post('/record/publish', AgntcyController.recordPublishValidator, new AgntcyController().publishRecord); - app.get('/record/search', AgntcyController.recordSearchValidator, new AgntcyController().searchRecord); - app.get('/record/:cid', AgntcyController.recordGetValidator, new AgntcyController().getRecord); + app.post('/oasf/publish', OasfController.recordPublishValidator, new OasfController().publishRecord); + app.get('/oasf/search', OasfController.recordSearchValidator, new OasfController().searchRecord); + app.get('/oasf/:cid', OasfController.recordGetValidator, new OasfController().getRecord); // 404 for all other requests app.all('*', (_req, res) => res.status(StatusCodes.BAD_REQUEST).send('Bad request')); diff --git a/src/controllers/api/agntcy.ts b/src/controllers/api/oasf.ts similarity index 94% rename from src/controllers/api/agntcy.ts rename to src/controllers/api/oasf.ts index 07116168..3d6803fc 100644 --- a/src/controllers/api/agntcy.ts +++ b/src/controllers/api/oasf.ts @@ -13,10 +13,10 @@ import type { GetRecordQuery, GetRecordResponseBody, UnsuccessfulGetRecordResponseBody, -} from '../../types/record.js'; -import { AgntcyService } from '../../services/api/agntcy.js'; +} from '../../types/oasf.js'; +import { OasfService } from '../../services/api/oasf.js'; -export class AgntcyController { +export class OasfController { // Validators public static recordPublishValidator = [ check('data').exists().withMessage('data field is required').bail(), @@ -85,7 +85,7 @@ export class AgntcyController { /** * @openapi * - * /record/publish: + * /oasf/publish: * post: * tags: [ Records ] * summary: Publish an OASF record to the directory. @@ -129,11 +129,11 @@ export class AgntcyController { const record = request.body as PublishRecordRequestBody; // Get directory service - const agntcyService = new AgntcyService(); + const oasfService = new OasfService(); try { // Validate against OASF schema (optional) - const isValid = await agntcyService.validateOASFSchema(record); + const isValid = await oasfService.validateOASFSchema(record); if (!isValid) { return response.status(StatusCodes.BAD_REQUEST).json({ error: 'Record does not conform to OASF schema', @@ -141,7 +141,7 @@ export class AgntcyController { } // Publish to directory - const result = await agntcyService.publish(response.locals.customer, record); + const result = await oasfService.publish(response.locals.customer, record); // Return the response return response.status(StatusCodes.CREATED).json({ @@ -165,7 +165,7 @@ export class AgntcyController { /** * @openapi * - * /record/search: + * /oasf/search: * get: * tags: [ Records ] * summary: Search for records in the directory. @@ -260,14 +260,14 @@ export class AgntcyController { const query = request.query as SearchRecordQuery; // Get directory service - const agntcyService = new AgntcyService(); + const oasfService = new OasfService(); try { // Set defaults for pagination const { page = 1, limit = 20 } = request.query as SearchRecordQuery; // Search directory - const results = await agntcyService.search(response.locals.customer, { + const results = await oasfService.search({ ...query, page, limit, @@ -294,7 +294,7 @@ export class AgntcyController { /** * @openapi * - * /record/{cid}: + * /oasf/{cid}: * get: * tags: [ Records ] * summary: Fetch a record by CID. @@ -351,11 +351,11 @@ export class AgntcyController { const { verify } = request.query as GetRecordQuery; // Get directory service - const agntcyService = new AgntcyService(); + const oasfService = new OasfService(); try { // Fetch record from directory - const record = await agntcyService.getRecord(response.locals.customer, cid); + const record = await oasfService.getRecord(cid); if (!record) { return response.status(StatusCodes.NOT_FOUND).json({ @@ -366,7 +366,7 @@ export class AgntcyController { // Optional: Verify signature let verificationResult = null; if (verify === 'true' || verify === true) { - verificationResult = await agntcyService.verifyRecord(response.locals.customer, cid); + verificationResult = await oasfService.verifyRecord(cid); } // Return record with metadata diff --git a/src/middleware/auth/routes/api/oasf-auth.ts b/src/middleware/auth/routes/api/oasf-auth.ts new file mode 100644 index 00000000..aacc8068 --- /dev/null +++ b/src/middleware/auth/routes/api/oasf-auth.ts @@ -0,0 +1,10 @@ +import { AuthRuleProvider } from '../auth-rule-provider.js'; + +export class AgntcyAuthProvider extends AuthRuleProvider { + constructor() { + super(); + this.registerRule('/oasf/search', 'GET', 'read:oasf', { skipNamespace: true, allowUnauthorized: true }); + this.registerRule('/oasf/(.*)', 'GET', 'read:oasf', { skipNamespace: true, allowUnauthorized: true }); + this.registerRule('/oasf/publish', 'POST', 'create:oasf', { skipNamespace: true }); + } +} diff --git a/src/middleware/auth/routes/api/record-auth.ts b/src/middleware/auth/routes/api/record-auth.ts deleted file mode 100644 index 490030d6..00000000 --- a/src/middleware/auth/routes/api/record-auth.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { AuthRuleProvider } from '../auth-rule-provider.js'; - -export class RecordAuthProvider extends AuthRuleProvider { - constructor() { - super(); - this.registerRule('/record/search', 'GET', 'read:account', { skipNamespace: true }); - this.registerRule('/record/publish', 'POST', 'create:account', { skipNamespace: true }); - this.registerRule('/record/(.*)', 'GET', 'read:account', { skipNamespace: true }); - } -} diff --git a/src/middleware/authentication.ts b/src/middleware/authentication.ts index b6ac0bdd..41e228af 100644 --- a/src/middleware/authentication.ts +++ b/src/middleware/authentication.ts @@ -22,7 +22,7 @@ import type { UnsuccessfulResponseBody } from '../types/shared.js'; import { AccreditationAuthRuleProvider } from './auth/routes/api/accreditation-auth.js'; import { EventAuthRuleProvider } from './auth/routes/api/event-auth.js'; import { ProvidersAuthRuleProvider } from './auth/routes/api/provider-auth.js'; -import { RecordAuthProvider } from './auth/routes/api/record-auth.js'; +import { AgntcyAuthProvider } from './auth/routes/api/oasf-auth.js'; dotenv.config(); @@ -53,7 +53,7 @@ export class Authentication { authRuleRepository.push(new AdminAuthRuleProvider()); authRuleRepository.push(new ProvidersAuthRuleProvider()); - authRuleRepository.push(new RecordAuthProvider()); + authRuleRepository.push(new AgntcyAuthProvider()); this.apiGuardian = new APIGuard(authRuleRepository, this.oauthProvider); // Initial auth handler diff --git a/src/services/api/agntcy.ts b/src/services/api/oasf.ts similarity index 84% rename from src/services/api/agntcy.ts rename to src/services/api/oasf.ts index 6bf0b808..05098d34 100644 --- a/src/services/api/agntcy.ts +++ b/src/services/api/oasf.ts @@ -1,16 +1,14 @@ import { Client, Config, models } from 'agntcy-dir'; import { create } from '@bufbuild/protobuf'; import { CustomerEntity } from '../../database/entities/customer.entity.js'; -import type { PublishRecordRequestBody, SearchRecordQuery, VerificationResult } from '../../types/record.js'; - -/** - * AgntcyService - Uses official agntcy-dir npm package - * Based on official examples from agntcy/dir repository - * - * Installation: - * npm install agntcy-dir @bufbuild/protobuf - */ -export class AgntcyService { +import { + queryTypeMap, + type PublishRecordRequestBody, + type SearchRecordQuery, + type VerificationResult, +} from '../../types/oasf.js'; + +export class OasfService { private client: Client; private oasfSchemaUrl: string; @@ -24,65 +22,6 @@ export class AgntcyService { this.client = new Client(config); } - /** - * Convert request body to OASF record format - * Must match the exact structure from the official example - */ - private toOASFRecord(body: PublishRecordRequestBody): any { - const { data } = body; - - // Return the exact structure from the example - just the data wrapper - return { - data: { - name: data.name, - version: data.version, - schema_version: data.schema_version || '0.7.0', - description: data.description || '', - authors: data.authors || [], - created_at: data.created_at || new Date().toISOString(), - uid: data.uid, // Include identity if provided - type: data.type, - skills: data.skills || [], - locators: data.locators || [], - domains: data.domains || [], - modules: data.modules || [], - }, - }; - } - - /** - * Convert OASF record back to response format - */ - private fromOASFRecord(record: any): PublishRecordRequestBody { - const data = record.data || record; - - return { - data: { - name: data.name, - version: data.version, - schema_version: data.schema_version, - uid: data.uid, - description: data.description, - authors: data.authors, - created_at: data.created_at, - type: data.type, - skills: data.skills || [], - locators: data.locators || [], - domains: data.domains || [], - modules: data.modules || [], - }, - }; - } - - /** - * Create a RecordRef protobuf message - */ - private createRecordRef(cid: string) { - return create(models.core_v1.RecordRefSchema, { - cid: cid, - }); - } - /** * Validate OASF record against schema server */ @@ -182,40 +121,16 @@ export class AgntcyService { /** * Search for records in directory */ - async search( - customer: CustomerEntity, - query: SearchRecordQuery - ): Promise<{ records: PublishRecordRequestBody[]; total: number }> { + async search(query: SearchRecordQuery): Promise<{ records: PublishRecordRequestBody[]; total: number }> { try { // Build search queries array const queries: any[] = []; - if (query.skill) { - queries.push({ - type: models.search_v1.RecordQueryType.SKILL_NAME, - value: query.skill, - }); - } - - if (query.skill_id) { - queries.push({ - type: models.search_v1.RecordQueryType.SKILL_ID, - value: query.skill_id.toString(), - }); - } - - if (query.name) { - queries.push({ - type: models.search_v1.RecordQueryType.NAME, - value: query.name, - }); - } - - if (query.domain) { - queries.push({ - type: models.search_v1.RecordQueryType.DOMAIN_NAME, - value: query.domain, - }); + for (const [field, queryType] of Object.entries(queryTypeMap)) { + const value = query[field as keyof SearchRecordQuery]; + if (value !== undefined) { + queries.push({ type: queryType, value: String(value) }); + } } // Create search request using protobuf @@ -266,7 +181,7 @@ export class AgntcyService { /** * Get specific record by CID */ - async getRecord(customer: CustomerEntity, cid: string): Promise { + async getRecord(cid: string): Promise { try { // Create RecordRef using protobuf constructor const recordRef = this.createRecordRef(cid); @@ -292,7 +207,7 @@ export class AgntcyService { /** * Verify record signature */ - async verifyRecord(customer: CustomerEntity, cid: string): Promise { + async verifyRecord(cid: string): Promise { try { // Note: The SDK might have a different verify API // For now, return unverified since we don't have signature in the example @@ -311,23 +226,6 @@ export class AgntcyService { } } - /** - * Sign a record using Sigstore - * Note: Requires dirctl binary for OIDC signing - */ - async signRecord(customer: CustomerEntity, cid: string): Promise<{ signed: boolean; signature?: string }> { - try { - // Note: Sign not shown in example - // This would require dirctl and OIDC flow - console.warn('Sign requires dirctl and OIDC - not implemented'); - - throw new Error('Signing requires dirctl binary and OIDC authentication'); - } catch (error) { - console.error('Error signing record:', error); - throw new Error(`Failed to sign record: ${(error as Error).message}`); - } - } - /** * Search across distributed network */ @@ -372,28 +270,6 @@ export class AgntcyService { } } - /** - * Sync records from remote directory - */ - async syncFromRemote( - customer: CustomerEntity, - remoteUrl: string, - cids?: string[] - ): Promise<{ synced: number; errors: string[] }> { - try { - // Note: Sync not shown in example - console.warn('Sync not implemented in example'); - - return { - synced: 0, - errors: ['Sync not implemented'], - }; - } catch (error) { - console.error('Error syncing records:', error); - throw new Error(`Failed to sync records: ${(error as Error).message}`); - } - } - /** * Get OASF skills taxonomy */ @@ -561,4 +437,63 @@ export class AgntcyService { throw new Error(`Failed to get record info: ${(error as Error).message}`); } } + + /** + * Convert request body to OASF record format + * Must match the exact structure from the official example + */ + private toOASFRecord(body: PublishRecordRequestBody): any { + const { data } = body; + + // Return the exact structure from the example - just the data wrapper + return { + data: { + name: data.name, + version: data.version, + schema_version: data.schema_version || '0.7.0', + description: data.description || '', + authors: data.authors || [], + created_at: data.created_at || new Date().toISOString(), + uid: data.uid, // Include identity if provided + type: data.type, + skills: data.skills || [], + locators: data.locators || [], + domains: data.domains || [], + modules: data.modules || [], + }, + }; + } + + /** + * Convert OASF record back to response format + */ + private fromOASFRecord(record: any): PublishRecordRequestBody { + const data = record.data || record; + + return { + data: { + name: data.name, + version: data.version, + schema_version: data.schema_version, + uid: data.uid, + description: data.description, + authors: data.authors, + created_at: data.created_at, + type: data.type, + skills: data.skills || [], + locators: data.locators || [], + domains: data.domains || [], + modules: data.modules || [], + }, + }; + } + + /** + * Create a RecordRef protobuf message + */ + private createRecordRef(cid: string) { + return create(models.core_v1.RecordRefSchema, { + cid: cid, + }); + } } diff --git a/src/static/swagger-api.json b/src/static/swagger-api.json index 13511523..b7273461 100644 --- a/src/static/swagger-api.json +++ b/src/static/swagger-api.json @@ -3927,293 +3927,6 @@ } } }, - "/record/publish": { - "post": { - "tags": [ - "Records" - ], - "summary": "Publish an OASF record to the directory.", - "description": "This endpoint publishes an OASF-compliant record to the Agent Directory. The record can represent an agent, organization, service, or MCP server.", - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "$ref": "#/components/schemas/PublishRecordRequest" - } - }, - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublishRecordRequest" - } - } - } - }, - "responses": { - "201": { - "description": "The record was successfully published.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/PublishRecordResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/record/search": { - "get": { - "tags": [ - "Records" - ], - "summary": "Search for records in the directory.", - "description": "This endpoint searches for OASF records in the Agent Directory based on various criteria such as name, version, skills, domains, and more.", - "parameters": [ - { - "name": "name", - "description": "Name of the record to search for.", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "version", - "description": "Version of the record to search for.", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "skill", - "description": "Skill name to filter by.", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "skill_id", - "description": "Skill ID to filter by.", - "in": "query", - "schema": { - "type": "integer" - } - }, - { - "name": "domain", - "description": "Domain to filter by.", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "locator", - "description": "Locator type to filter by.", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "module", - "description": "Module name to filter by.", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "type", - "description": "Type of record to filter by.", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "agent", - "organization", - "service", - "mcp-server" - ] - } - }, - { - "name": "page", - "description": "Page number for pagination (default 1).", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1 - } - }, - { - "name": "limit", - "description": "Number of results per page (default 20, max 100).", - "in": "query", - "schema": { - "type": "integer", - "minimum": 1, - "maximum": 100 - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/SearchRecordResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, - "/record/{cid}": { - "get": { - "tags": [ - "Records" - ], - "summary": "Fetch a record by CID.", - "description": "This endpoint fetches a specific OASF record from the Agent Directory using its Content Identifier (CID). Optionally, the record's signature can be verified.", - "parameters": [ - { - "name": "cid", - "description": "Content Identifier (CID) of the record to fetch.", - "in": "path", - "schema": { - "type": "string" - }, - "required": true - }, - { - "name": "verify", - "description": "Whether to verify the record's signature.", - "in": "query", - "schema": { - "type": "boolean" - } - } - ], - "responses": { - "200": { - "description": "The request was successful.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/GetRecordResult" - } - } - } - }, - "400": { - "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "InvalidRequest" - } - } - } - }, - "401": { - "$ref": "#/components/schemas/UnauthorizedError" - }, - "404": { - "description": "The record was not found.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Record not found" - } - } - } - }, - "500": { - "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", - "content": { - "application/json": { - "schema": { - "$ref": "#/components/schemas/InvalidRequest" - }, - "example": { - "error": "Internal Error" - } - } - } - } - } - } - }, "/credential-status/create/unencrypted": { "post": { "tags": [ @@ -6012,55 +5725,243 @@ "parameters": [ { "in": "path", - "name": "did", - "description": "DID identifier to resolve.", + "name": "did", + "description": "DID identifier to resolve.", + "schema": { + "type": "string" + }, + "required": true + } + ], + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "required": false + }, + "providerId": { + "type": "string", + "required": false + } + } + } + }, + "application/json": { + "schema": { + "type": "object", + "properties": { + "password": { + "type": "string", + "required": false + }, + "providerId": { + "type": "string", + "required": false + } + } + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ExportDidResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/create": { + "post": { + "tags": [ + "Keys" + ], + "summary": "Create an identity key pair.", + "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", + "parameters": [ + { + "name": "type", + "description": "Key type of the identity key pair to create.", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "Ed25519" + ] + } + } + ], + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/import": { + "post": { + "tags": [ + "Keys" + ], + "summary": "Import an identity key pair.", + "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", + "requestBody": { + "content": { + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyImportRequest" + } + } + } + }, + "responses": { + "200": { + "description": "The request was successful.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/KeyResult" + } + } + } + }, + "400": { + "description": "A problem with the input fields has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "InvalidRequest" + } + } + } + }, + "401": { + "$ref": "#/components/schemas/UnauthorizedError" + }, + "500": { + "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Internal Error" + } + } + } + } + } + } + }, + "/key/read/{kid}": { + "get": { + "tags": [ + "Keys" + ], + "summary": "Fetch an identity key pair.", + "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", + "parameters": [ + { + "name": "kid", + "description": "Key ID of the identity key pair to fetch.", + "in": "path", "schema": { "type": "string" }, "required": true } ], - "requestBody": { - "content": { - "application/x-www-form-urlencoded": { - "schema": { - "type": "object", - "properties": { - "password": { - "type": "string", - "required": false - }, - "providerId": { - "type": "string", - "required": false - } - } - } - }, - "application/json": { - "schema": { - "type": "object", - "properties": { - "password": { - "type": "string", - "required": false - }, - "providerId": { - "type": "string", - "required": false - } - } - } - } - } - }, "responses": { "200": { "description": "The request was successful.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/ExportDidResult" + "$ref": "#/components/schemas/KeyResult" } } } @@ -6097,23 +5998,51 @@ } } }, - "/key/create": { - "post": { + "/key/{kid}/verification-method": { + "get": { "tags": [ "Keys" ], - "summary": "Create an identity key pair.", - "description": "This endpoint creates an identity key pair associated with the user's account for custodian-mode clients.", + "summary": "Convert a key to a W3C Verification Method.", + "description": "This endpoint converts a stored key (by key ID) into a [W3C Verification Method](https://www.w3.org/TR/did-core/#verification-methods) format.", "parameters": [ { - "name": "type", - "description": "Key type of the identity key pair to create.", + "name": "kid", + "description": "Key ID of the identity key pair to convert.", + "in": "path", + "schema": { + "type": "string" + }, + "required": true + }, + { + "name": "verificationMethodType", + "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", "in": "query", "schema": { "type": "string", "enum": [ - "Ed25519" + "Ed25519VerificationKey2018", + "Ed25519VerificationKey2020", + "JsonWebKey2020" ] + }, + "required": true + }, + { + "name": "controller", + "description": "Controller DID of the verification method", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "id", + "description": "Verification Method Id Fragment", + "in": "query", + "schema": { + "type": "string" } } ], @@ -6123,7 +6052,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/KeyResult" + "$ref": "#/components/schemas/VerificationMethod" } } } @@ -6144,6 +6073,19 @@ "401": { "$ref": "#/components/schemas/UnauthorizedError" }, + "404": { + "description": "The key was not found.", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/InvalidRequest" + }, + "example": { + "error": "Key not found" + } + } + } + }, "500": { "description": "An internal error has occurred. Additional state information plus metadata may be available in the response body.", "content": { @@ -6160,34 +6102,34 @@ } } }, - "/key/import": { + "/oasf/publish": { "post": { "tags": [ - "Keys" + "Records" ], - "summary": "Import an identity key pair.", - "description": "This endpoint imports an identity key pair associated with the user's account for custodian-mode clients.", + "summary": "Publish an OASF record to the directory.", + "description": "This endpoint publishes an OASF-compliant record to the Agent Directory. The record can represent an agent, organization, service, or MCP server.", "requestBody": { "content": { "application/x-www-form-urlencoded": { "schema": { - "$ref": "#/components/schemas/KeyImportRequest" + "$ref": "#/components/schemas/PublishRecordRequest" } }, "application/json": { "schema": { - "$ref": "#/components/schemas/KeyImportRequest" + "$ref": "#/components/schemas/PublishRecordRequest" } } } }, "responses": { - "200": { - "description": "The request was successful.", + "201": { + "description": "The record was successfully published.", "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/KeyResult" + "$ref": "#/components/schemas/PublishRecordResult" } } } @@ -6224,22 +6166,102 @@ } } }, - "/key/read/{kid}": { + "/oasf/search": { "get": { "tags": [ - "Keys" + "Records" ], - "summary": "Fetch an identity key pair.", - "description": "This endpoint fetches an identity key pair's details for a given key ID. Only the user account associated with the custodian-mode client can fetch the key pair.", + "summary": "Search for records in the directory.", + "description": "This endpoint searches for OASF records in the Agent Directory based on various criteria such as name, version, skills, domains, and more.", "parameters": [ { - "name": "kid", - "description": "Key ID of the identity key pair to fetch.", - "in": "path", + "name": "name", + "description": "Name of the record to search for.", + "in": "query", "schema": { "type": "string" - }, - "required": true + } + }, + { + "name": "version", + "description": "Version of the record to search for.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "skill", + "description": "Skill name to filter by.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "skill_id", + "description": "Skill ID to filter by.", + "in": "query", + "schema": { + "type": "integer" + } + }, + { + "name": "domain", + "description": "Domain to filter by.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "locator", + "description": "Locator type to filter by.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "module", + "description": "Module name to filter by.", + "in": "query", + "schema": { + "type": "string" + } + }, + { + "name": "type", + "description": "Type of record to filter by.", + "in": "query", + "schema": { + "type": "string", + "enum": [ + "agent", + "organization", + "service", + "mcp-server" + ] + } + }, + { + "name": "page", + "description": "Page number for pagination (default 1).", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1 + } + }, + { + "name": "limit", + "description": "Number of results per page (default 20, max 100).", + "in": "query", + "schema": { + "type": "integer", + "minimum": 1, + "maximum": 100 + } } ], "responses": { @@ -6248,7 +6270,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/KeyResult" + "$ref": "#/components/schemas/SearchRecordResult" } } } @@ -6285,17 +6307,17 @@ } } }, - "/key/{kid}/verification-method": { + "/oasf/{cid}": { "get": { "tags": [ - "Keys" + "Records" ], - "summary": "Convert a key to a W3C Verification Method.", - "description": "This endpoint converts a stored key (by key ID) into a [W3C Verification Method](https://www.w3.org/TR/did-core/#verification-methods) format.", + "summary": "Fetch a record by CID.", + "description": "This endpoint fetches a specific OASF record from the Agent Directory using its Content Identifier (CID). Optionally, the record's signature can be verified.", "parameters": [ { - "name": "kid", - "description": "Key ID of the identity key pair to convert.", + "name": "cid", + "description": "Content Identifier (CID) of the record to fetch.", "in": "path", "schema": { "type": "string" @@ -6303,33 +6325,11 @@ "required": true }, { - "name": "verificationMethodType", - "description": "Type of verification method to use for the DID. See DID Core specification for more details. Only the types listed below are supported.", - "in": "query", - "schema": { - "type": "string", - "enum": [ - "Ed25519VerificationKey2018", - "Ed25519VerificationKey2020", - "JsonWebKey2020" - ] - }, - "required": true - }, - { - "name": "controller", - "description": "Controller DID of the verification method", - "in": "query", - "schema": { - "type": "string" - } - }, - { - "name": "id", - "description": "Verification Method Id Fragment", + "name": "verify", + "description": "Whether to verify the record's signature.", "in": "query", "schema": { - "type": "string" + "type": "boolean" } } ], @@ -6339,7 +6339,7 @@ "content": { "application/json": { "schema": { - "$ref": "#/components/schemas/VerificationMethod" + "$ref": "#/components/schemas/GetRecordResult" } } } @@ -6361,14 +6361,14 @@ "$ref": "#/components/schemas/UnauthorizedError" }, "404": { - "description": "The key was not found.", + "description": "The record was not found.", "content": { "application/json": { "schema": { "$ref": "#/components/schemas/InvalidRequest" }, "example": { - "error": "Key not found" + "error": "Record not found" } } } diff --git a/src/types/record.ts b/src/types/oasf.ts similarity index 62% rename from src/types/record.ts rename to src/types/oasf.ts index a03ab775..cef7b4a5 100644 --- a/src/types/record.ts +++ b/src/types/oasf.ts @@ -1,5 +1,7 @@ // Request/Response types for OASF records +import { models } from 'agntcy-dir'; + export interface UnsuccessfulPublishRecordResponseBody { error: string; } @@ -150,9 +152,10 @@ export interface SearchRecordQuery { // Text search name?: string; version?: string; + schema_version?: string; description?: string; - // Identity search - NEW! ✨ + // Identity search uid?: string; // Search by identity // Capability search @@ -229,104 +232,11 @@ export interface OASFRecord { }; } -/** - * Example usage with identity: - */ -export const exampleWithIdentity: PublishRecordRequestBody = { - data: { - // Core fields - name: 'Customer Support Agent', - version: '1.0.0', - schema_version: '1.0.0', - - // Identity - DID from cheqd, Okta, etc. - uid: 'did:cheqd:testnet:7bf81a20-1bfe-4584-9a66-2c4a6b1d5e3f', - - // Metadata - description: 'AI agent for customer support queries', - authors: ['ACME Corp'], - type: 'agent', - - // Capabilities - skills: [ - { name: 'customer_support.query_handling', id: 1 }, - { name: 'e_commerce.order_tracking', id: 2 }, - ], - - // Deployment - locators: [ - { - type: 'api_endpoint', - url: 'https://api.acme.com/agents/support', - }, - { - type: 'did', - url: 'did:cheqd:testnet:7bf81a20-1bfe-4584-9a66-2c4a6b1d5e3f', - description: 'Agent DID', - }, - { - type: 'badge', - url: 'https://identity.acme.com/v1alpha1/vc/abc123/.well-known/vcs.json', - description: 'Verifiable credential badge', - }, - ], - - // Industry - domains: [ - { name: 'e_commerce', id: 1 }, - { name: 'customer_service', id: 2 }, - ], - }, +export const queryTypeMap: Partial> = { + name: models.search_v1.RecordQueryType.NAME, + version: models.search_v1.RecordQueryType.VERSION, + skill: models.search_v1.RecordQueryType.SKILL_NAME, + domain: models.search_v1.RecordQueryType.DOMAIN_NAME, + locator: models.search_v1.RecordQueryType.LOCATOR, + schema_version: models.search_v1.RecordQueryType.SCHEMA_VERSION, }; - -/** - * Example without identity (fallback to name): - */ -export const exampleWithoutIdentity: PublishRecordRequestBody = { - data: { - name: 'Simple Agent', - version: '1.0.0', - schema_version: '1.0.0', - // uid is optional - will use name as identifier if not provided - skills: [{ name: 'text_generation', id: 1 }], - }, -}; - -/** - * Type guard to check if record has identity - */ -export function hasIdentity(record: PublishRecordRequestBody): boolean { - return !!record.data.uid && record.data.uid !== record.data.name; -} - -/** - * Extract identity from record - */ -export function getIdentity(record: PublishRecordRequestBody): string { - return record.data.uid || record.data.name; -} - -/** - * Check if identity is a DID - */ -export function isDID(uid: string): boolean { - return uid.startsWith('did:'); -} - -/** - * Check if identity is a URL - */ -export function isURL(uid: string): boolean { - return uid.startsWith('http://') || uid.startsWith('https://'); -} - -/** - * Parse identity type - */ -export function getIdentityType(uid: string): 'did' | 'url' | 'oauth2' | 'uuid' | 'name' { - if (uid.startsWith('did:')) return 'did'; - if (uid.startsWith('http://') || uid.startsWith('https://')) return 'url'; - if (uid.startsWith('urn:uuid:')) return 'uuid'; - if (uid.includes('okta') || uid.includes('auth0') || uid.includes('client')) return 'oauth2'; - return 'name'; -} From 8cf51e349077ea08a348b2cebdaff58a315cb9f7 Mon Sep 17 00:00:00 2001 From: Daev Mithran Date: Mon, 12 Jan 2026 11:35:32 +0530 Subject: [PATCH 4/4] Cleanup --- src/services/api/oasf.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/api/oasf.ts b/src/services/api/oasf.ts index 05098d34..8292313b 100644 --- a/src/services/api/oasf.ts +++ b/src/services/api/oasf.ts @@ -14,12 +14,10 @@ export class OasfService { constructor() { const serverAddress = process.env.DIRECTORY_SERVER_URL || 'localhost:8888'; - const dirctlPath = process.env.DIRCTL_PATH || '/usr/local/bin/dirctl'; this.oasfSchemaUrl = process.env.OASF_SCHEMA_SERVER_URL || 'https://schema.oasf.outshift.com'; - const config = new Config(serverAddress, dirctlPath); - this.client = new Client(config); + this.client = new Client(new Config(serverAddress)); } /**