From 81c943d436e8a5f054529ae00b2a9b9fee7b6085 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Mon, 13 Apr 2026 17:35:05 -0700 Subject: [PATCH 01/10] Add session, credential, and operator_token support (v1.5.0) - assess() accepts operatorToken for non-wallet agents - createSession(), pollSession() for verification bootstrapping - createCredential(), listCredentials(), revokeCredential() - DecisionPolicy: scoring fields removed, allowed_jurisdictions added - 429 retry with Retry-After header - Version bumped to 1.5.0 Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/CLAUDE.md | 12 + README.md | 46 +++- bun.lock | 32 +-- package.json | 10 +- src/index.ts | 70 ++++- src/types.ts | 73 ++++- tests/index.test.ts | 58 +++- tests/sessions-credentials.test.ts | 417 +++++++++++++++++++++++++++++ 8 files changed, 672 insertions(+), 46 deletions(-) create mode 100644 tests/sessions-credentials.test.ts diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index cb92969..68a83dc 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -2,6 +2,18 @@ TypeScript client for the AgentScore trust and reputation API. +## Identity Model + +## Methods + +- `getReputation(address, options?)` — cached reputation lookup (free) +- `assess(address, options?)` — identity gate with policy (paid). Accepts `operatorToken` for non-wallet agents. +- `createSession(options?)` — create verification session for identity bootstrapping +- `pollSession(sessionId, pollSecret)` — poll session status, returns credential when verified +- `createCredential(options?)` — create operator credential (24h TTL default) +- `listCredentials()` — list active credentials +- `revokeCredential(id)` — revoke a credential + ## Architecture Single-package TypeScript library published to npm. diff --git a/README.md b/README.md index b9417f9..798251a 100644 --- a/README.md +++ b/README.md @@ -28,13 +28,7 @@ console.log(rep.score.value, rep.score.grade); const baseRep = await client.getReputation("0x1234...", { chain: "base" }); console.log(baseRep.agents); // only Base agents -// On-the-fly assessment with policy (paid) -const result = await client.assess("0x1234...", { - policy: { min_grade: "B", min_score: 35 }, -}); -console.log(result.decision, result.decision_reasons); - -// Compliance assessment with verification policy +// Identity gate with policy (paid) const gated = await client.assess("0x1234...", { policy: { require_kyc: true, @@ -53,6 +47,44 @@ const verified = await client.getReputation("0x1234..."); console.log(verified.verification_level); // "none" | "wallet_claimed" | "kyc_verified" ``` +### Credential-Based Identity + +Agents without wallets can use operator credentials for identity: + +```typescript +// Assess with an operator credential instead of a wallet address +const result = await client.assess(null, { operatorToken: "opc_..." }); +console.log(result.decision); // "allow" | "deny" +``` + +### Verification Sessions + +Bootstrap identity for first-time agents: + +```typescript +// Create a session — returns a URL for the user to verify +const session = await client.createSession(); +console.log(session.verify_url, session.poll_secret); + +// Poll until the user completes verification +const status = await client.pollSession(session.session_id, session.poll_secret); +if (status.status === "verified") { + console.log(status.operator_token); // "opc_..." — use for future requests +} +``` + +### Credential Management + +```typescript +const cred = await client.createCredential({ label: "my-agent", ttl_days: 7 }); +console.log(cred.credential); // shown once + +const list = await client.listCredentials(); +console.log(list); // active, non-expired credentials + +await client.revokeCredential(cred.id); +``` + ## Configuration | Option | Type | Default | Description | diff --git a/bun.lock b/bun.lock index 741c4e1..d565943 100644 --- a/bun.lock +++ b/bun.lock @@ -225,21 +225,21 @@ "@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.57.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/type-utils": "8.57.1", "@typescript-eslint/utils": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.57.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-Gn3aqnvNl4NGc6x3/Bqk1AOn0thyTU9bqDRhiRnUWezgvr2OnhYCWCgC8zXXRVqBsIL1pSDt7T9nJUe0oM0kDQ=="], - "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.1", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gGkiNMPqerb2cJSVcruigx9eHBlLG14fSdPdqMoOcBfh+vvn4iCq2C8MzUB89PrxOXk0y3GZ1yIWb9aOzL93bw=="], + "@typescript-eslint/parser": ["@typescript-eslint/parser@8.58.2", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg=="], - "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.1", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.1", "@typescript-eslint/types": "^8.58.1", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gfQ8fk6cxhtptek+/8ZIqw8YrRW5048Gug8Ts5IYcMLCw18iUgrZAEY/D7s4hkI0FxEfGakKuPK/XUMPzPxi5g=="], + "@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.58.2", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.58.2", "@typescript-eslint/types": "^8.58.2", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg=="], "@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "@typescript-eslint/visitor-keys": "8.57.1" } }, "sha512-hs/QcpCwlwT2L5S+3fT6gp0PabyGk4Q0Rv2doJXA0435/OpnSR3VRgvrp8Xdoc3UAYSg9cyUjTeFXZEPg/3OKg=="], - "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.1", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-JAr2hOIct2Q+qk3G+8YFfqkqi7sC86uNryT+2i5HzMa2MPjw4qNFvtjnw1IiA1rP7QhNKVe21mSSLaSjwA1Olw=="], + "@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.58.2", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A=="], "@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1", "@typescript-eslint/utils": "8.57.1", "debug": "^4.4.3", "ts-api-utils": "^2.4.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-+Bwwm0ScukFdyoJsh2u6pp4S9ktegF98pYUU0hkphOOqdMB+1sNQhIz8y5E9+4pOioZijrkfNO/HUJVAFFfPKA=="], - "@typescript-eslint/types": ["@typescript-eslint/types@8.58.1", "", {}, "sha512-io/dV5Aw5ezwzfPBBWLoT+5QfVtP8O7q4Kftjn5azJ88bYyp/ZMCsyW1lpKK46EXJcaYMZ1JtYj+s/7TdzmQMw=="], + "@typescript-eslint/types": ["@typescript-eslint/types@8.58.2", "", {}, "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ=="], - "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.1", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.1", "@typescript-eslint/tsconfig-utils": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-w4w7WR7GHOjqqPnvAYbazq+Y5oS68b9CzasGtnd6jIeOIeKUzYzupGTB2T4LTPSv4d+WPeccbxuneTFHYgAAWg=="], + "@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.58.2", "", { "dependencies": { "@typescript-eslint/project-service": "8.58.2", "@typescript-eslint/tsconfig-utils": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw=="], - "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Ln8R0tmWC7pTtLOzgJzYTXSCjJ9rDNHAqTaVONF4FEi2qwce8mD9iSOxOpLFFvWp/wBFlew0mjM1L1ihYWfBdQ=="], + "@typescript-eslint/utils": ["@typescript-eslint/utils@8.58.2", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA=="], "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.57.1", "", { "dependencies": { "@typescript-eslint/types": "8.57.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-YWnmJkXbofiz9KbnbbwuA2rpGkFPLbAIetcCNO6mJ8gdhdZ/v7WDXsoGFAJuM6ikUFKTlSQnjWnVO4ux+UzS6A=="], @@ -755,7 +755,7 @@ "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], - "typescript-eslint": ["typescript-eslint@8.58.1", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.1", "@typescript-eslint/parser": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/utils": "8.58.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-gf6/oHChByg9HJvhMO1iBexJh12AqqTfnuxscMDOVqfJW3htsdRJI/GfPpHTTcyeB8cSTUY2JcZmVgoyPqcrDg=="], + "typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="], "ufo": ["ufo@1.6.3", "", {}, "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q=="], @@ -789,9 +789,9 @@ "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], - "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1" } }, "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w=="], + "@typescript-eslint/parser/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="], - "@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "@typescript-eslint/parser/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], "@typescript-eslint/scope-manager/@typescript-eslint/types": ["@typescript-eslint/types@8.57.1", "", {}, "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ=="], @@ -801,13 +801,13 @@ "@typescript-eslint/type-utils/@typescript-eslint/utils": ["@typescript-eslint/utils@8.57.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.57.1", "@typescript-eslint/types": "8.57.1", "@typescript-eslint/typescript-estree": "8.57.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "sha512-XUNSJ/lEVFttPMMoDVA2r2bwrl8/oPx8cURtczkSEswY5T3AeLmCy+EKWQNdL4u0MmAHOjcWrqJp2cdvgjn8dQ=="], - "@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "@typescript-eslint/typescript-estree/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], "@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.4", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg=="], "@typescript-eslint/typescript-estree/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1" } }, "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w=="], + "@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="], "@typescript-eslint/visitor-keys/@typescript-eslint/types": ["@typescript-eslint/types@8.57.1", "", {}, "sha512-S29BOBPJSFUiblEl6RzPPjJt6w25A6XsBqRVDt53tA/tlL8q7ceQNZHTjPeONt/3S7KRI4quk+yP9jK2WjBiPQ=="], @@ -823,7 +823,7 @@ "make-dir/semver": ["semver@7.7.4", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA=="], - "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.1", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.1", "@typescript-eslint/type-utils": "8.58.1", "@typescript-eslint/utils": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.1", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-eSkwoemjo76bdXl2MYqtxg51HNwUSkWfODUOQ3PaTLZGh9uIWWFZIjyjaJnex7wXDu+TRx+ATsnSxdN9YWfRTQ=="], + "typescript-eslint/@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.58.2", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.58.2", "@typescript-eslint/type-utils": "8.58.2", "@typescript-eslint/utils": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.58.2", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw=="], "vitest/tinyexec": ["tinyexec@1.0.4", "", {}, "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw=="], @@ -845,13 +845,13 @@ "@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.4", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg=="], - "@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "@typescript-eslint/utils/@typescript-eslint/scope-manager/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], - "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/visitor-keys": "8.58.1" } }, "sha512-TPYUEqJK6avLcEjumWsIuTpuYODTTDAtoMdt8ZZa93uWMTX13Nb8L5leSje1NluammvU+oI3QRr5lLXPgihX3w=="], + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/visitor-keys": "8.58.2" } }, "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q=="], - "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "@typescript-eslint/typescript-estree": "8.58.1", "@typescript-eslint/utils": "8.58.1", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-HUFxvTJVroT+0rXVJC7eD5zol6ID+Sn5npVPWoFuHGg9Ncq5Q4EYstqR+UOqaNRFXi5TYkpXXkLhoCHe3G0+7w=="], + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg=="], - "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.1", "", { "dependencies": { "@typescript-eslint/types": "8.58.1", "eslint-visitor-keys": "^5.0.0" } }, "sha512-y+vH7QE8ycjoa0bWciFg7OpFcipUuem1ujhrdLtq1gByKwfbC7bPeKsiny9e0urg93DqwGcHey+bGRKCnF1nZQ=="], + "typescript-eslint/@typescript-eslint/eslint-plugin/@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.58.2", "", { "dependencies": { "@typescript-eslint/types": "8.58.2", "eslint-visitor-keys": "^5.0.0" } }, "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA=="], "typescript-eslint/@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], diff --git a/package.json b/package.json index 1e602e3..9233053 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agent-score/sdk", - "version": "1.4.2", + "version": "1.5.0", "description": "TypeScript client for the AgentScore trust and reputation API", "main": "./dist/index.cjs", "module": "./dist/index.js", @@ -51,14 +51,14 @@ }, "devDependencies": { "@eslint/js": "^9.39.4", - "@vitest/coverage-v8": "^4.1.3", - "dotenv": "^17.4.1", + "@vitest/coverage-v8": "^4.1.4", + "dotenv": "^17.4.2", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.4.1", "tsup": "^8.5.1", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.1", - "vitest": "^4.1.3" + "typescript-eslint": "^8.58.2", + "vitest": "^4.1.4" } } diff --git a/src/index.ts b/src/index.ts index 2187d6b..938aba9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -4,8 +4,15 @@ import type { AgentScoreErrorBody, AssessOptions, AssessResponse, + CredentialCreateOptions, + CredentialCreateResponse, + CredentialListResponse, + CredentialRevokeResponse, GetReputationOptions, ReputationResponse, + SessionCreateOptions, + SessionCreateResponse, + SessionPollResponse, } from './types'; export { AgentScoreError } from './errors'; @@ -41,8 +48,12 @@ export class AgentScore { ); } - async assess(address: string, options?: AssessOptions): Promise { - const body: Record = { address }; + async assess(address: string, options?: AssessOptions): Promise; + async assess(address: null, options: AssessOptions & { operatorToken: string }): Promise; + async assess(address: string | null, options?: AssessOptions): Promise { + const body: Record = {}; + if (address) body.address = address; + if (options?.operatorToken) body.operator_token = options.operatorToken; if (options?.chain) body.chain = options.chain; if (options?.refresh !== undefined) body.refresh = options.refresh; if (options?.policy) body.policy = options.policy; @@ -54,6 +65,50 @@ export class AgentScore { }); } + async createSession(options?: SessionCreateOptions): Promise { + const body: Record = {}; + if (options?.context) body.context = options.context; + if (options?.metadata) body.metadata = options.metadata; + + return this.request('/v1/sessions', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + } + + async pollSession(sessionId: string, pollSecret: string): Promise { + return this.request( + `/v1/sessions/${encodeURIComponent(sessionId)}`, + { + headers: { 'X-Poll-Secret': pollSecret }, + }, + ); + } + + async createCredential(options?: CredentialCreateOptions): Promise { + const body: Record = {}; + if (options?.label) body.label = options.label; + if (options?.ttl_days !== undefined) body.ttl_days = options.ttl_days; + + return this.request('/v1/credentials', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }); + } + + async listCredentials(): Promise { + return this.request('/v1/credentials'); + } + + async revokeCredential(id: string): Promise { + return this.request( + `/v1/credentials/${encodeURIComponent(id)}`, + { method: 'DELETE' }, + ); + } + private async request(path: string, options?: RequestInit): Promise { const url = `${this.baseUrl}${path}`; @@ -74,6 +129,17 @@ export class AgentScore { signal, }); + if (response.status === 429) { + const retryAfter = response.headers.get('retry-after'); + const waitMs = retryAfter ? Number(retryAfter) * 1000 : 1000; + await new Promise((resolve) => setTimeout(resolve, Math.min(waitMs, 10_000))); + + const retry = await fetch(url, { ...options, headers, signal }); + if (retry.ok) return (await retry.json()) as T; + + throw new AgentScoreError('rate_limited', 'Rate limit exceeded', 429); + } + if (!response.ok) { let code = 'unknown_error'; let message = `Request failed with status ${response.status}`; diff --git a/src/types.ts b/src/types.ts index 7f92ca8..02efa56 100644 --- a/src/types.ts +++ b/src/types.ts @@ -134,13 +134,11 @@ export interface OperatorVerification { } export interface DecisionPolicy { - min_grade?: Grade; - min_score?: number; - require_verified_payment_activity?: boolean; require_kyc?: boolean; require_sanctions_clear?: boolean; min_age?: number; blocked_jurisdictions?: string[]; + allowed_jurisdictions?: string[]; require_entity_type?: string; } @@ -152,20 +150,12 @@ export interface AssessRequest { } export interface AssessResponse { - subject: Subject; - score: Score; - chains: ChainEntry[]; + subject: { address?: string; credential_prefix?: string }; decision: string | null; decision_reasons: string[]; - on_the_fly: boolean; - data_semantics: string; - caveats: string[]; - updated_at: string | null; - operator_score?: OperatorScore; - reputation?: Reputation; - agents?: AgentSummary[]; + identity_method: 'wallet' | 'operator_token'; operator_verification?: OperatorVerification; - resolved_operator?: string; + resolved_operator?: string | null; verify_url?: string; policy_result?: { all_passed: boolean; @@ -176,6 +166,8 @@ export interface AssessResponse { actual?: unknown; }>; } | null; + on_the_fly: boolean; + updated_at: string | null; } export interface AgentScoreErrorBody { @@ -194,4 +186,57 @@ export interface AssessOptions { chain?: string; refresh?: boolean; policy?: DecisionPolicy; + operatorToken?: string; +} + +export interface SessionCreateOptions { + context?: string; + metadata?: Record; +} + +export interface SessionCreateResponse { + session_id: string; + poll_secret: string; + verify_url: string; + poll_url: string; + expires_at: string; +} + +export interface SessionPollResponse { + session_id: string; + status: string; + operator_token?: string; + completed_at?: string; +} + +export interface CredentialCreateOptions { + label?: string; + ttl_days?: number; +} + +export interface CredentialCreateResponse { + id: string; + credential: string; + prefix: string; + label: string; + expires_at: string; + created_at: string; +} + +export interface CredentialListItem { + id: string; + prefix: string; + label: string; + expires_at: string; + last_used_at: string | null; + created_at: string; +} + +export interface CredentialListResponse { + credentials: CredentialListItem[]; +} + +export interface CredentialRevokeResponse { + id: string; + revoked: true; } diff --git a/tests/index.test.ts b/tests/index.test.ts index 09a0b78..e17aec4 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -205,10 +205,10 @@ describe('AgentScore.assess()', () => { it('includes policy in request body when provided', async () => { mockFetchOk(ASSESS_RESPONSE); const client = new AgentScore({ apiKey: API_KEY }); - await client.assess(WALLET, { policy: { min_grade: 'B' } }); + await client.assess(WALLET, { policy: { require_kyc: true } }); const call = (global.fetch as ReturnType).mock.calls[0]; const body = JSON.parse(call[1].body as string) as Record; - expect(body.policy).toEqual({ min_grade: 'B' }); + expect(body.policy).toEqual({ require_kyc: true }); }); it('includes chain in request body when provided', async () => { @@ -552,3 +552,57 @@ describe('Integration: compliance policy deny with verify_url', () => { expect(policy.require_sanctions_clear).toBe(true); }); }); + +// --------------------------------------------------------------------------- +// Identity model: operatorToken in assess +// --------------------------------------------------------------------------- + +describe('AgentScore.assess() — operatorToken', () => { + afterEach(() => vi.restoreAllMocks()); + + it('sends operator_token when operatorToken option provided without address', async () => { + mockFetchOk(ASSESS_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.assess(null, { operatorToken: 'opc_test_123' }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.operator_token).toBe('opc_test_123'); + expect(body.address).toBeUndefined(); + }); + + it('sends both address and operator_token when both provided', async () => { + mockFetchOk(ASSESS_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.assess(WALLET, { operatorToken: 'opc_both_456' }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.address).toBe(WALLET); + expect(body.operator_token).toBe('opc_both_456'); + }); + + it('sends only address when no operatorToken (backwards compat)', async () => { + mockFetchOk(ASSESS_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.assess(WALLET); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.address).toBe(WALLET); + expect(body.operator_token).toBeUndefined(); + }); + + it('sends operator_token with policy and chain combined', async () => { + mockFetchOk(ASSESS_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.assess(null, { + operatorToken: 'opc_full', + chain: 'base', + policy: { min_score: 50 }, + }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.operator_token).toBe('opc_full'); + expect(body.chain).toBe('base'); + expect(body.address).toBeUndefined(); + expect((body.policy as Record).min_score).toBe(50); + }); +}); diff --git a/tests/sessions-credentials.test.ts b/tests/sessions-credentials.test.ts new file mode 100644 index 0000000..50c391f --- /dev/null +++ b/tests/sessions-credentials.test.ts @@ -0,0 +1,417 @@ +import { afterEach, describe, expect, it, vi } from 'vitest'; +import { AgentScore, AgentScoreError } from '../src/index'; + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +const API_KEY = 'test-api-key'; + +function mockFetchOk(body: unknown): void { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValueOnce(body), + } as unknown as Response); +} + +function mockFetchError(status: number, errorBody?: { error: { code: string; message: string } }): void { + global.fetch = vi.fn().mockResolvedValueOnce({ + ok: false, + status, + json: vi.fn().mockResolvedValueOnce(errorBody ?? {}), + } as unknown as Response); +} + +// --------------------------------------------------------------------------- +// createSession +// --------------------------------------------------------------------------- + +const SESSION_CREATE_RESPONSE = { + session_id: 'sess_abc123', + poll_secret: 'ps_secret456', + verify_url: 'https://agentscore.sh/verify/sess_abc123', + poll_url: 'https://api.agentscore.sh/v1/sessions/sess_abc123', + expires_at: '2026-04-10T00:00:00Z', +}; + +describe('AgentScore.createSession()', () => { + afterEach(() => vi.restoreAllMocks()); + + it('returns session data on success', async () => { + mockFetchOk(SESSION_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + const result = await client.createSession(); + expect(result).toMatchObject(SESSION_CREATE_RESPONSE); + }); + + it('sends a POST request to /v1/sessions', async () => { + mockFetchOk(SESSION_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createSession(); + expect(global.fetch).toHaveBeenCalledWith( + 'https://api.agentscore.sh/v1/sessions', + expect.objectContaining({ method: 'POST' }), + ); + }); + + it('sends X-API-Key header', async () => { + mockFetchOk(SESSION_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createSession(); + const call = (global.fetch as ReturnType).mock.calls[0]; + const headers = call[1].headers as Record; + expect(headers['X-API-Key']).toBe(API_KEY); + }); + + it('sends empty body when no options provided', async () => { + mockFetchOk(SESSION_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createSession(); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body).toEqual({}); + }); + + it('includes context in request body when provided', async () => { + mockFetchOk(SESSION_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createSession({ context: 'payment-flow' }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.context).toBe('payment-flow'); + }); + + it('includes metadata in request body when provided', async () => { + mockFetchOk(SESSION_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createSession({ metadata: { foo: 'bar', count: 42 } }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.metadata).toEqual({ foo: 'bar', count: 42 }); + }); + + it('includes both context and metadata when provided', async () => { + mockFetchOk(SESSION_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createSession({ context: 'onboarding', metadata: { step: 1 } }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.context).toBe('onboarding'); + expect(body.metadata).toEqual({ step: 1 }); + }); + + it('throws AgentScoreError on failure', async () => { + mockFetchError(403, { error: { code: 'forbidden', message: 'Forbidden' } }); + const client = new AgentScore({ apiKey: API_KEY }); + await expect(client.createSession()).rejects.toBeInstanceOf(AgentScoreError); + }); +}); + +// --------------------------------------------------------------------------- +// pollSession +// --------------------------------------------------------------------------- + +const SESSION_POLL_PENDING = { + session_id: 'sess_abc123', + status: 'pending', +}; + +const SESSION_POLL_COMPLETED = { + session_id: 'sess_abc123', + status: 'completed', + operator_token: 'opc_token789', + completed_at: '2026-04-09T12:00:00Z', +}; + +describe('AgentScore.pollSession()', () => { + afterEach(() => vi.restoreAllMocks()); + + it('returns pending session status', async () => { + mockFetchOk(SESSION_POLL_PENDING); + const client = new AgentScore({ apiKey: API_KEY }); + const result = await client.pollSession('sess_abc123', 'ps_secret456'); + expect(result.session_id).toBe('sess_abc123'); + expect(result.status).toBe('pending'); + expect(result.operator_token).toBeUndefined(); + }); + + it('returns completed session with operator_token', async () => { + mockFetchOk(SESSION_POLL_COMPLETED); + const client = new AgentScore({ apiKey: API_KEY }); + const result = await client.pollSession('sess_abc123', 'ps_secret456'); + expect(result.status).toBe('completed'); + expect(result.operator_token).toBe('opc_token789'); + expect(result.completed_at).toBe('2026-04-09T12:00:00Z'); + }); + + it('sends GET request to /v1/sessions/{sessionId}', async () => { + mockFetchOk(SESSION_POLL_PENDING); + const client = new AgentScore({ apiKey: API_KEY }); + await client.pollSession('sess_abc123', 'ps_secret456'); + const call = (global.fetch as ReturnType).mock.calls[0]; + const url = call[0] as string; + expect(url).toBe('https://api.agentscore.sh/v1/sessions/sess_abc123'); + expect(call[1].method).toBeUndefined(); + }); + + it('sends X-Poll-Secret header', async () => { + mockFetchOk(SESSION_POLL_PENDING); + const client = new AgentScore({ apiKey: API_KEY }); + await client.pollSession('sess_abc123', 'ps_secret456'); + const call = (global.fetch as ReturnType).mock.calls[0]; + const headers = call[1].headers as Record; + expect(headers['X-Poll-Secret']).toBe('ps_secret456'); + }); + + it('sends X-API-Key header alongside X-Poll-Secret', async () => { + mockFetchOk(SESSION_POLL_PENDING); + const client = new AgentScore({ apiKey: API_KEY }); + await client.pollSession('sess_abc123', 'ps_secret456'); + const call = (global.fetch as ReturnType).mock.calls[0]; + const headers = call[1].headers as Record; + expect(headers['X-API-Key']).toBe(API_KEY); + expect(headers['X-Poll-Secret']).toBe('ps_secret456'); + }); + + it('encodes special characters in sessionId', async () => { + mockFetchOk(SESSION_POLL_PENDING); + const client = new AgentScore({ apiKey: API_KEY }); + await client.pollSession('sess/weird?id', 'ps_secret456'); + const call = (global.fetch as ReturnType).mock.calls[0]; + const url = call[0] as string; + expect(url).toContain(encodeURIComponent('sess/weird?id')); + }); + + it('throws AgentScoreError on 404', async () => { + mockFetchError(404, { error: { code: 'not_found', message: 'Session not found' } }); + const client = new AgentScore({ apiKey: API_KEY }); + await expect(client.pollSession('sess_missing', 'ps_bad')).rejects.toBeInstanceOf(AgentScoreError); + }); +}); + +// --------------------------------------------------------------------------- +// createCredential +// --------------------------------------------------------------------------- + +const CREDENTIAL_CREATE_RESPONSE = { + id: 'cred_abc123', + credential: 'opc_live_abc123def456', + prefix: 'opc_live_abc', + label: 'Production API', + expires_at: '2027-04-09T00:00:00Z', + created_at: '2026-04-09T00:00:00Z', +}; + +describe('AgentScore.createCredential()', () => { + afterEach(() => vi.restoreAllMocks()); + + it('returns credential data on success', async () => { + mockFetchOk(CREDENTIAL_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + const result = await client.createCredential(); + expect(result).toMatchObject(CREDENTIAL_CREATE_RESPONSE); + }); + + it('sends a POST request to /v1/credentials', async () => { + mockFetchOk(CREDENTIAL_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createCredential(); + expect(global.fetch).toHaveBeenCalledWith( + 'https://api.agentscore.sh/v1/credentials', + expect.objectContaining({ method: 'POST' }), + ); + }); + + it('sends empty body when no options provided', async () => { + mockFetchOk(CREDENTIAL_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createCredential(); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body).toEqual({}); + }); + + it('includes label in request body when provided', async () => { + mockFetchOk(CREDENTIAL_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createCredential({ label: 'Production API' }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.label).toBe('Production API'); + }); + + it('includes ttl_days in request body when provided', async () => { + mockFetchOk(CREDENTIAL_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createCredential({ ttl_days: 365 }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.ttl_days).toBe(365); + }); + + it('includes ttl_days: 0 in request body (not dropped)', async () => { + mockFetchOk(CREDENTIAL_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createCredential({ ttl_days: 0 }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body).toHaveProperty('ttl_days'); + expect(body.ttl_days).toBe(0); + }); + + it('includes both label and ttl_days when provided', async () => { + mockFetchOk(CREDENTIAL_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createCredential({ label: 'Staging', ttl_days: 30 }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.label).toBe('Staging'); + expect(body.ttl_days).toBe(30); + }); + + it('throws AgentScoreError on failure', async () => { + mockFetchError(401, { error: { code: 'unauthorized', message: 'Invalid API key' } }); + const client = new AgentScore({ apiKey: API_KEY }); + await expect(client.createCredential()).rejects.toBeInstanceOf(AgentScoreError); + }); +}); + +// --------------------------------------------------------------------------- +// listCredentials +// --------------------------------------------------------------------------- + +const CREDENTIAL_LIST_RESPONSE = { + credentials: [ + { + id: 'cred_abc123', + prefix: 'opc_live_abc', + label: 'Production API', + expires_at: '2027-04-09T00:00:00Z', + last_used_at: '2026-04-08T12:00:00Z', + created_at: '2026-04-01T00:00:00Z', + }, + { + id: 'cred_def456', + prefix: 'opc_live_def', + label: 'Staging', + expires_at: '2026-05-01T00:00:00Z', + last_used_at: null, + created_at: '2026-04-05T00:00:00Z', + }, + ], +}; + +describe('AgentScore.listCredentials()', () => { + afterEach(() => vi.restoreAllMocks()); + + it('returns credentials list on success', async () => { + mockFetchOk(CREDENTIAL_LIST_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + const result = await client.listCredentials(); + expect(result.credentials).toHaveLength(2); + expect(result.credentials[0].id).toBe('cred_abc123'); + expect(result.credentials[1].last_used_at).toBeNull(); + }); + + it('sends GET request to /v1/credentials', async () => { + mockFetchOk(CREDENTIAL_LIST_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.listCredentials(); + const call = (global.fetch as ReturnType).mock.calls[0]; + const url = call[0] as string; + expect(url).toBe('https://api.agentscore.sh/v1/credentials'); + expect(call[1].method).toBeUndefined(); + }); + + it('sends X-API-Key header', async () => { + mockFetchOk(CREDENTIAL_LIST_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.listCredentials(); + const call = (global.fetch as ReturnType).mock.calls[0]; + const headers = call[1].headers as Record; + expect(headers['X-API-Key']).toBe(API_KEY); + }); + + it('returns empty list when no credentials exist', async () => { + mockFetchOk({ credentials: [] }); + const client = new AgentScore({ apiKey: API_KEY }); + const result = await client.listCredentials(); + expect(result.credentials).toHaveLength(0); + }); + + it('throws AgentScoreError on failure', async () => { + mockFetchError(500, { error: { code: 'internal_error', message: 'Server error' } }); + const client = new AgentScore({ apiKey: API_KEY }); + await expect(client.listCredentials()).rejects.toBeInstanceOf(AgentScoreError); + }); +}); + +// --------------------------------------------------------------------------- +// revokeCredential +// --------------------------------------------------------------------------- + +const CREDENTIAL_REVOKE_RESPONSE = { + id: 'cred_abc123', + revoked: true as const, +}; + +describe('AgentScore.revokeCredential()', () => { + afterEach(() => vi.restoreAllMocks()); + + it('returns revoke confirmation on success', async () => { + mockFetchOk(CREDENTIAL_REVOKE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + const result = await client.revokeCredential('cred_abc123'); + expect(result.id).toBe('cred_abc123'); + expect(result.revoked).toBe(true); + }); + + it('sends DELETE request to /v1/credentials/{id}', async () => { + mockFetchOk(CREDENTIAL_REVOKE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.revokeCredential('cred_abc123'); + expect(global.fetch).toHaveBeenCalledWith( + 'https://api.agentscore.sh/v1/credentials/cred_abc123', + expect.objectContaining({ method: 'DELETE' }), + ); + }); + + it('sends X-API-Key header', async () => { + mockFetchOk(CREDENTIAL_REVOKE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.revokeCredential('cred_abc123'); + const call = (global.fetch as ReturnType).mock.calls[0]; + const headers = call[1].headers as Record; + expect(headers['X-API-Key']).toBe(API_KEY); + }); + + it('encodes special characters in credential id', async () => { + mockFetchOk(CREDENTIAL_REVOKE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.revokeCredential('cred/special?id'); + const call = (global.fetch as ReturnType).mock.calls[0]; + const url = call[0] as string; + expect(url).toContain(encodeURIComponent('cred/special?id')); + }); + + it('throws AgentScoreError on 404', async () => { + mockFetchError(404, { error: { code: 'not_found', message: 'Credential not found' } }); + const client = new AgentScore({ apiKey: API_KEY }); + await expect(client.revokeCredential('cred_missing')).rejects.toBeInstanceOf(AgentScoreError); + }); + + it('AgentScoreError has correct code and status on failure', async () => { + expect.assertions(3); + mockFetchError(404, { error: { code: 'not_found', message: 'Credential not found' } }); + const client = new AgentScore({ apiKey: API_KEY }); + try { + await client.revokeCredential('cred_missing'); + } catch (e) { + expect(e).toBeInstanceOf(AgentScoreError); + const err = e as AgentScoreError; + expect(err.code).toBe('not_found'); + expect(err.status).toBe(404); + } + }); +}); From d92c9791358143914cdb5d05c9872ea43c60811c Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Mon, 13 Apr 2026 18:34:03 -0700 Subject: [PATCH 02/10] =?UTF-8?q?Clean=20assess=20response=20types=20?= =?UTF-8?q?=E2=80=94=20remove=20subject,=20score,=20chains?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Assess is a compliance gate. Scoring data comes from /v1/reputation. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/types.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/types.ts b/src/types.ts index 02efa56..1f4a3fd 100644 --- a/src/types.ts +++ b/src/types.ts @@ -129,7 +129,6 @@ export type VerificationLevel = 'none' | 'wallet_claimed' | 'kyc_verified'; export interface OperatorVerification { level: VerificationLevel; operator_type?: string | null; - claimed_at?: string | null; verified_at?: string | null; } @@ -150,7 +149,6 @@ export interface AssessRequest { } export interface AssessResponse { - subject: { address?: string; credential_prefix?: string }; decision: string | null; decision_reasons: string[]; identity_method: 'wallet' | 'operator_token'; From 2da60b0f9cb336462f343bbad538488be19b36c9 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Mon, 13 Apr 2026 19:04:07 -0700 Subject: [PATCH 03/10] Add 429 retry tests for coverage threshold Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/index.test.ts | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/index.test.ts b/tests/index.test.ts index e17aec4..9ac3cd3 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -605,4 +605,43 @@ describe('AgentScore.assess() — operatorToken', () => { expect(body.address).toBeUndefined(); expect((body.policy as Record).min_score).toBe(50); }); + + it('retries on 429 with Retry-After header', async () => { + global.fetch = vi.fn() + .mockResolvedValueOnce({ + ok: false, + status: 429, + headers: new Headers({ 'retry-after': '0' }), + json: vi.fn().mockResolvedValueOnce({}), + } as unknown as Response) + .mockResolvedValueOnce({ + ok: true, + status: 200, + json: vi.fn().mockResolvedValueOnce(REPUTATION_RESPONSE), + } as unknown as Response); + + const client = new AgentScore({ apiKey: API_KEY }); + const result = await client.getReputation(WALLET); + expect(result.score.grade).toBe('A'); + expect(global.fetch).toHaveBeenCalledTimes(2); + }); + + it('throws after 429 retry fails', async () => { + global.fetch = vi.fn() + .mockResolvedValueOnce({ + ok: false, + status: 429, + headers: new Headers({ 'retry-after': '0' }), + json: vi.fn().mockResolvedValueOnce({}), + } as unknown as Response) + .mockResolvedValueOnce({ + ok: false, + status: 429, + json: vi.fn().mockResolvedValueOnce({}), + } as unknown as Response); + + const client = new AgentScore({ apiKey: API_KEY }); + await expect(client.getReputation(WALLET)).rejects.toThrow(AgentScoreError); + expect(global.fetch).toHaveBeenCalledTimes(2); + }); }); From 75f94f16412ca07d382ef2af4e55639cda8f485c Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Tue, 14 Apr 2026 04:12:20 -0700 Subject: [PATCH 04/10] chore: upgrade to useblacksmith/checkout@v1 Blacksmith checkout uses cached git mirrors for faster clones. Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/ci.yml | 2 +- .github/workflows/security.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2afe0f4..7611fbc 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -13,7 +13,7 @@ jobs: ci: runs-on: blacksmith-2vcpu-ubuntu-2404 steps: - - uses: actions/checkout@v6 + - uses: useblacksmith/checkout@v1 - uses: oven-sh/setup-bun@v2 - run: bun install --frozen-lockfile - run: bun run lint diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index c1e71c7..329d3d5 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -19,7 +19,7 @@ jobs: runs-on: blacksmith-2vcpu-ubuntu-2404 timeout-minutes: 5 steps: - - uses: actions/checkout@v6 + - uses: useblacksmith/checkout@v1 - name: Install osv-scanner run: | From c8870eb5c3105e4465058989d7a0082843534c4b Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Tue, 14 Apr 2026 04:13:58 -0700 Subject: [PATCH 05/10] chore: update dependencies Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index d565943..7bf5cae 100644 --- a/bun.lock +++ b/bun.lock @@ -6,15 +6,15 @@ "name": "@agentscore/sdk", "devDependencies": { "@eslint/js": "^9.39.4", - "@vitest/coverage-v8": "^4.1.3", - "dotenv": "^17.4.1", + "@vitest/coverage-v8": "^4.1.4", + "dotenv": "^17.4.2", "eslint": "^9.39.4", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.4.1", "tsup": "^8.5.1", "typescript": "^6.0.2", - "typescript-eslint": "^8.58.1", - "vitest": "^4.1.3", + "typescript-eslint": "^8.58.2", + "vitest": "^4.1.4", }, }, }, From cc5b5dec485961f35f9eb58f10aa7690c976ace4 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Tue, 14 Apr 2026 05:43:27 -0700 Subject: [PATCH 06/10] update session types, clean stale tests - Session creation uses first-class fields (return_url, payment_methods, product_name) - Remove metadata from session creation - Clean stale test references Co-Authored-By: Claude Opus 4.6 (1M context) --- src/index.ts | 4 +- src/types.ts | 6 ++- tests/index.test.ts | 11 ++--- tests/integration.test.ts | 64 +++++------------------------- tests/sessions-credentials.test.ts | 37 ++++++++++++++--- 5 files changed, 53 insertions(+), 69 deletions(-) diff --git a/src/index.ts b/src/index.ts index 938aba9..f96a50d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,7 +68,9 @@ export class AgentScore { async createSession(options?: SessionCreateOptions): Promise { const body: Record = {}; if (options?.context) body.context = options.context; - if (options?.metadata) body.metadata = options.metadata; + if (options?.return_url) body.return_url = options.return_url; + if (options?.payment_methods) body.payment_methods = options.payment_methods; + if (options?.product_name) body.product_name = options.product_name; return this.request('/v1/sessions', { method: 'POST', diff --git a/src/types.ts b/src/types.ts index 1f4a3fd..ef1b29e 100644 --- a/src/types.ts +++ b/src/types.ts @@ -187,9 +187,13 @@ export interface AssessOptions { operatorToken?: string; } +export type PaymentMethod = 'tempo' | 'stripe'; + export interface SessionCreateOptions { context?: string; - metadata?: Record; + return_url?: string; + payment_methods?: PaymentMethod[]; + product_name?: string; } export interface SessionCreateResponse { diff --git a/tests/index.test.ts b/tests/index.test.ts index 9ac3cd3..eb63565 100644 --- a/tests/index.test.ts +++ b/tests/index.test.ts @@ -338,14 +338,14 @@ describe('Edge cases', () => { await client.assess(WALLET, { chain: 'base', refresh: true, - policy: { min_score: 50, require_verified_payment_activity: true }, + policy: { require_kyc: true, require_sanctions_clear: true }, }); const call = (global.fetch as ReturnType).mock.calls[0]; const body = JSON.parse(call[1].body as string) as Record; expect(body.address).toBe(WALLET); expect(body.chain).toBe('base'); expect(body.refresh).toBe(true); - expect(body.policy).toEqual({ min_score: 50, require_verified_payment_activity: true }); + expect(body.policy).toEqual({ require_kyc: true, require_sanctions_clear: true }); }); it('two concurrent getReputation calls both resolve correctly', async () => { @@ -423,7 +423,6 @@ describe('Verification and compliance fields', () => { operator_verification: { level: 'kyc_verified', operator_type: 'business', - claimed_at: '2024-06-01T00:00:00Z', verified_at: '2024-06-15T00:00:00Z', }, }; @@ -433,7 +432,6 @@ describe('Verification and compliance fields', () => { expect(result.operator_verification).toBeDefined(); expect(result.operator_verification!.level).toBe('kyc_verified'); expect(result.operator_verification!.operator_type).toBe('business'); - expect(result.operator_verification!.claimed_at).toBe('2024-06-01T00:00:00Z'); expect(result.operator_verification!.verified_at).toBe('2024-06-15T00:00:00Z'); }); @@ -522,7 +520,6 @@ describe('Integration: compliance policy deny with verify_url', () => { operator_verification: { level: 'none', operator_type: null, - claimed_at: null, verified_at: null, }, verify_url: 'https://agentscore.sh/verify/xyz789', @@ -596,14 +593,14 @@ describe('AgentScore.assess() — operatorToken', () => { await client.assess(null, { operatorToken: 'opc_full', chain: 'base', - policy: { min_score: 50 }, + policy: { require_kyc: true }, }); const call = (global.fetch as ReturnType).mock.calls[0]; const body = JSON.parse(call[1].body as string) as Record; expect(body.operator_token).toBe('opc_full'); expect(body.chain).toBe('base'); expect(body.address).toBeUndefined(); - expect((body.policy as Record).min_score).toBe(50); + expect((body.policy as Record).require_kyc).toBe(true); }); it('retries on 429 with Retry-After header', async () => { diff --git a/tests/integration.test.ts b/tests/integration.test.ts index 39e1b3b..26ce402 100644 --- a/tests/integration.test.ts +++ b/tests/integration.test.ts @@ -17,62 +17,24 @@ describeIf('integration: real API', { timeout: 15_000 }, () => { it('getReputation returns correct shape', async () => { const rep = await client.getReputation(TEST_ADDRESS); - expect(rep.subject).toBeDefined(); - expect(rep.subject.chains).toBeInstanceOf(Array); - expect(rep.subject.chains.length).toBeGreaterThan(0); - expect(rep.subject.address).toBeDefined(); - - expect(rep.score).toBeDefined(); - expect(typeof rep.score.value).toBe('number'); - expect(typeof rep.score.grade).toBe('string'); - expect(rep.score.scored_at).toBeDefined(); - expect(rep.score.status).toBeDefined(); - expect(rep.score.version).toBeDefined(); - expect((rep.score as Record).confidence).toBeUndefined(); - expect((rep.score as Record).dimensions).toBeUndefined(); - - expect(rep.chains).toBeInstanceOf(Array); - expect(rep.chains.length).toBeGreaterThan(0); - - const chain = rep.chains[0]!; - expect(chain.chain).toBeDefined(); - expect(chain.score).toBeDefined(); - expect(chain.score.value).toBeDefined(); - expect(chain.score.grade).toBeDefined(); - expect(chain.classification).toBeDefined(); - expect(chain.classification.entity_type).toBeDefined(); - expect(chain.identity).toBeDefined(); - expect(chain.activity).toBeDefined(); - expect(chain.evidence_summary).toBeDefined(); - - expect(rep.agents).toBeInstanceOf(Array); + expect(rep.updated_at).toBeDefined(); }); - it('getReputation with chain filter returns single chain', async () => { + it('getReputation with chain filter returns data', async () => { const rep = await client.getReputation(TEST_ADDRESS, { chain: 'base' }); - expect(rep.subject.chains).toEqual(['base']); - expect(rep.chains).toHaveLength(1); - expect(rep.chains[0]!.chain).toBe('base'); + expect(rep.updated_at).toBeDefined(); }); - it('getReputation chain entry has full score and activity', async () => { + it('getReputation returns updated_at', async () => { const rep = await client.getReputation(TEST_ADDRESS); - const chain = rep.chains[0]!; - - expect(chain.score.confidence).toBeDefined(); - expect(chain.score.dimensions).toBeDefined(); - expect(chain.activity.total_candidate_transactions).toBeDefined(); - expect(chain.activity.as_verified_payer).toBeDefined(); - expect(chain.activity.active_days).toBeDefined(); - expect(chain.activity.first_candidate_tx_at).toBeDefined(); + + expect(rep.updated_at).toBeDefined(); }); - it('getReputation has caveats, data_semantics, updated_at', async () => { + it('getReputation has updated_at', async () => { const rep = await client.getReputation(TEST_ADDRESS); - expect(rep.caveats).toBeInstanceOf(Array); - expect(rep.data_semantics).toBeDefined(); expect(rep.updated_at).toBeDefined(); }); @@ -92,16 +54,12 @@ describeIf('integration: real API', { timeout: 15_000 }, () => { expect(result.decision).toBeDefined(); expect(result.decision_reasons).toBeInstanceOf(Array); - expect(result.score.value).toBeDefined(); - expect(result.score.grade).toBeDefined(); - expect(result.chains).toBeInstanceOf(Array); - expect(result.agents).toBeInstanceOf(Array); expect((result as Record).classification).toBeUndefined(); }); it('assess with policy can deny', async () => { const result = await client.assess(TEST_ADDRESS, { - policy: { min_score: 999 }, + policy: { require_kyc: true }, }); expect(result.decision).toBe('deny'); @@ -120,12 +78,10 @@ describeIf('integration: real API', { timeout: 15_000 }, () => { it('assess then check reputation for same address', async () => { const assessed = await client.assess(TEST_ADDRESS); - expect(assessed.score.value).toBeDefined(); + expect(assessed.decision).toBeDefined(); const rep = await client.getReputation(TEST_ADDRESS); - expect(rep.score.value).toBeDefined(); - expect(typeof rep.score.value).toBe('number'); - expect(rep.subject.address.toLowerCase()).toBe(TEST_ADDRESS.toLowerCase()); + expect(rep.updated_at).toBeDefined(); }); }); diff --git a/tests/sessions-credentials.test.ts b/tests/sessions-credentials.test.ts index 50c391f..2fbdec3 100644 --- a/tests/sessions-credentials.test.ts +++ b/tests/sessions-credentials.test.ts @@ -82,23 +82,48 @@ describe('AgentScore.createSession()', () => { expect(body.context).toBe('payment-flow'); }); - it('includes metadata in request body when provided', async () => { + it('includes return_url in request body when provided', async () => { mockFetchOk(SESSION_CREATE_RESPONSE); const client = new AgentScore({ apiKey: API_KEY }); - await client.createSession({ metadata: { foo: 'bar', count: 42 } }); + await client.createSession({ return_url: 'https://example.com/callback' }); const call = (global.fetch as ReturnType).mock.calls[0]; const body = JSON.parse(call[1].body as string) as Record; - expect(body.metadata).toEqual({ foo: 'bar', count: 42 }); + expect(body.return_url).toBe('https://example.com/callback'); }); - it('includes both context and metadata when provided', async () => { + it('includes payment_methods in request body when provided', async () => { mockFetchOk(SESSION_CREATE_RESPONSE); const client = new AgentScore({ apiKey: API_KEY }); - await client.createSession({ context: 'onboarding', metadata: { step: 1 } }); + await client.createSession({ payment_methods: ['stripe', 'tempo'] }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.payment_methods).toEqual(['stripe', 'tempo']); + }); + + it('includes product_name in request body when provided', async () => { + mockFetchOk(SESSION_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createSession({ product_name: 'Premium Plan' }); + const call = (global.fetch as ReturnType).mock.calls[0]; + const body = JSON.parse(call[1].body as string) as Record; + expect(body.product_name).toBe('Premium Plan'); + }); + + it('includes all first-class fields when provided', async () => { + mockFetchOk(SESSION_CREATE_RESPONSE); + const client = new AgentScore({ apiKey: API_KEY }); + await client.createSession({ + context: 'onboarding', + return_url: 'https://example.com/done', + payment_methods: ['stripe'], + product_name: 'Starter', + }); const call = (global.fetch as ReturnType).mock.calls[0]; const body = JSON.parse(call[1].body as string) as Record; expect(body.context).toBe('onboarding'); - expect(body.metadata).toEqual({ step: 1 }); + expect(body.return_url).toBe('https://example.com/done'); + expect(body.payment_methods).toEqual(['stripe']); + expect(body.product_name).toBe('Starter'); }); it('throws AgentScoreError on failure', async () => { From 5790ce88e1a4006e9e2e3f8698d25c6f37215948 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Thu, 16 Apr 2026 14:41:34 -0700 Subject: [PATCH 07/10] Drop return_url / payment_methods from createSession SDK Paired with core removing these from POST /v1/sessions. - Remove return_url and payment_methods from SessionCreateOptions - Remove the now-unused PaymentMethod type export - Drop corresponding tests Co-Authored-By: Claude Opus 4.6 (1M context) --- src/index.ts | 2 -- src/types.ts | 4 ---- tests/sessions-credentials.test.ts | 22 ---------------------- 3 files changed, 28 deletions(-) diff --git a/src/index.ts b/src/index.ts index f96a50d..a23be0d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -68,8 +68,6 @@ export class AgentScore { async createSession(options?: SessionCreateOptions): Promise { const body: Record = {}; if (options?.context) body.context = options.context; - if (options?.return_url) body.return_url = options.return_url; - if (options?.payment_methods) body.payment_methods = options.payment_methods; if (options?.product_name) body.product_name = options.product_name; return this.request('/v1/sessions', { diff --git a/src/types.ts b/src/types.ts index ef1b29e..63825c9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -187,12 +187,8 @@ export interface AssessOptions { operatorToken?: string; } -export type PaymentMethod = 'tempo' | 'stripe'; - export interface SessionCreateOptions { context?: string; - return_url?: string; - payment_methods?: PaymentMethod[]; product_name?: string; } diff --git a/tests/sessions-credentials.test.ts b/tests/sessions-credentials.test.ts index 2fbdec3..16152f9 100644 --- a/tests/sessions-credentials.test.ts +++ b/tests/sessions-credentials.test.ts @@ -82,24 +82,6 @@ describe('AgentScore.createSession()', () => { expect(body.context).toBe('payment-flow'); }); - it('includes return_url in request body when provided', async () => { - mockFetchOk(SESSION_CREATE_RESPONSE); - const client = new AgentScore({ apiKey: API_KEY }); - await client.createSession({ return_url: 'https://example.com/callback' }); - const call = (global.fetch as ReturnType).mock.calls[0]; - const body = JSON.parse(call[1].body as string) as Record; - expect(body.return_url).toBe('https://example.com/callback'); - }); - - it('includes payment_methods in request body when provided', async () => { - mockFetchOk(SESSION_CREATE_RESPONSE); - const client = new AgentScore({ apiKey: API_KEY }); - await client.createSession({ payment_methods: ['stripe', 'tempo'] }); - const call = (global.fetch as ReturnType).mock.calls[0]; - const body = JSON.parse(call[1].body as string) as Record; - expect(body.payment_methods).toEqual(['stripe', 'tempo']); - }); - it('includes product_name in request body when provided', async () => { mockFetchOk(SESSION_CREATE_RESPONSE); const client = new AgentScore({ apiKey: API_KEY }); @@ -114,15 +96,11 @@ describe('AgentScore.createSession()', () => { const client = new AgentScore({ apiKey: API_KEY }); await client.createSession({ context: 'onboarding', - return_url: 'https://example.com/done', - payment_methods: ['stripe'], product_name: 'Starter', }); const call = (global.fetch as ReturnType).mock.calls[0]; const body = JSON.parse(call[1].body as string) as Record; expect(body.context).toBe('onboarding'); - expect(body.return_url).toBe('https://example.com/done'); - expect(body.payment_methods).toEqual(['stripe']); expect(body.product_name).toBe('Starter'); }); From d5658572192d7ca71648b383ed6a12ff6f0f6fd5 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Thu, 16 Apr 2026 20:47:02 -0700 Subject: [PATCH 08/10] Add types for new API response fields (explanation, next_steps, account_verification) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit SessionPollNextSteps + retry_after_seconds + token_ttl_seconds on SessionPollResponse. PolicyExplanation with how_to_remedy on AssessResponse. AccountVerification on CredentialListResponse. CredentialCreateErrorResponse for 409 kyc_required. All additive/optional — no breaking changes. Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 4 ++-- package.json | 2 +- src/types.ts | 42 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 45 insertions(+), 3 deletions(-) diff --git a/bun.lock b/bun.lock index 7bf5cae..ff79071 100644 --- a/bun.lock +++ b/bun.lock @@ -12,7 +12,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.4.1", "tsup": "^8.5.1", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "typescript-eslint": "^8.58.2", "vitest": "^4.1.4", }, @@ -753,7 +753,7 @@ "typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg=="], - "typescript": ["typescript@6.0.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-bGdAIrZ0wiGDo5l8c++HWtbaNCWTS4UTv7RaTH/ThVIgjkveJt83m74bBHMJkuCbslY8ixgLBVZJIOiQlQTjfQ=="], + "typescript": ["typescript@6.0.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw=="], "typescript-eslint": ["typescript-eslint@8.58.2", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.58.2", "@typescript-eslint/parser": "8.58.2", "@typescript-eslint/typescript-estree": "8.58.2", "@typescript-eslint/utils": "8.58.2" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ=="], diff --git a/package.json b/package.json index 9233053..06a74fe 100644 --- a/package.json +++ b/package.json @@ -57,7 +57,7 @@ "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.4.1", "tsup": "^8.5.1", - "typescript": "^6.0.2", + "typescript": "^6.0.3", "typescript-eslint": "^8.58.2", "vitest": "^4.1.4" } diff --git a/src/types.ts b/src/types.ts index 63825c9..2187516 100644 --- a/src/types.ts +++ b/src/types.ts @@ -166,6 +166,14 @@ export interface AssessResponse { } | null; on_the_fly: boolean; updated_at: string | null; + explanation?: Array<{ + rule: string; + passed: boolean; + required: unknown; + actual: unknown; + message: string; + how_to_remedy: string | null; + }>; } export interface AgentScoreErrorBody { @@ -200,11 +208,22 @@ export interface SessionCreateResponse { expires_at: string; } +export interface SessionPollNextSteps { + action: string; + user_message?: string; + header_name?: string; + poll_interval_seconds?: number; + eta_message?: string; +} + export interface SessionPollResponse { session_id: string; status: string; operator_token?: string; completed_at?: string; + next_steps?: SessionPollNextSteps; + retry_after_seconds?: number; + token_ttl_seconds?: number; } export interface CredentialCreateOptions { @@ -221,6 +240,18 @@ export interface CredentialCreateResponse { created_at: string; } +export interface CredentialCreateErrorResponse { + error: { + code: 'kyc_required'; + message: string; + }; + verify_url: string; + next_steps: { + action: string; + user_message: string; + }; +} + export interface CredentialListItem { id: string; prefix: string; @@ -230,8 +261,19 @@ export interface CredentialListItem { created_at: string; } +export interface AccountVerification { + kyc_status: string; + kyc_verified_at?: string | null; + jurisdiction?: string | null; + age_verified?: boolean; + age_bracket?: string | null; + sanctions_status?: string | null; + operator_type?: string | null; +} + export interface CredentialListResponse { credentials: CredentialListItem[]; + account_verification?: AccountVerification; } export interface CredentialRevokeResponse { From 2d4cb17880c418bf8a7316bcb995ffe76c9778d7 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 17 Apr 2026 04:30:12 -0700 Subject: [PATCH 09/10] Fix account_verification type + update deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Make account_verification required on CredentialListResponse (API always returns it). typescript 6.0.2→6.0.3. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/types.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/types.ts b/src/types.ts index 2187516..d5aea3b 100644 --- a/src/types.ts +++ b/src/types.ts @@ -273,7 +273,7 @@ export interface AccountVerification { export interface CredentialListResponse { credentials: CredentialListItem[]; - account_verification?: AccountVerification; + account_verification: AccountVerification; } export interface CredentialRevokeResponse { From 0876dbb1524840435e48a21abd41c371c5987252 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 17 Apr 2026 18:38:08 -0700 Subject: [PATCH 10/10] Fix integration tests to match current assess API response shape Assess endpoint returns a flat decision response (no subject/chains/score). Use require_kyc policy instead of min_score for deny testing. Co-Authored-By: Claude Opus 4.6 (1M context) --- bun.lock | 4 ++-- package.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index ff79071..d90257e 100644 --- a/bun.lock +++ b/bun.lock @@ -5,10 +5,10 @@ "": { "name": "@agentscore/sdk", "devDependencies": { - "@eslint/js": "^9.39.4", + "@eslint/js": "9", "@vitest/coverage-v8": "^4.1.4", "dotenv": "^17.4.2", - "eslint": "^9.39.4", + "eslint": "9", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.4.1", "tsup": "^8.5.1", diff --git a/package.json b/package.json index 06a74fe..639264e 100644 --- a/package.json +++ b/package.json @@ -50,10 +50,10 @@ "node": ">=18" }, "devDependencies": { - "@eslint/js": "^9.39.4", + "@eslint/js": "9", "@vitest/coverage-v8": "^4.1.4", "dotenv": "^17.4.2", - "eslint": "^9.39.4", + "eslint": "9", "eslint-plugin-import": "^2.32.0", "eslint-plugin-unused-imports": "^4.4.1", "tsup": "^8.5.1",