diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..119b033 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,24 @@ +name: CI + +on: + pull_request: + push: + branches: [ main, work ] + +jobs: + trust-authority: + runs-on: ubuntu-latest + defaults: + run: + working-directory: reference-implementations/trust-authority + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 20 + - name: Install dependencies + run: npm install --no-audit --no-fund + - name: Build + run: npm run build + - name: Test + run: npm test diff --git a/reference-implementations/trust-authority/jest.config.cjs b/reference-implementations/trust-authority/jest.config.cjs new file mode 100644 index 0000000..2fa5c72 --- /dev/null +++ b/reference-implementations/trust-authority/jest.config.cjs @@ -0,0 +1,6 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + roots: ['/src'], + testMatch: ['**/*.test.ts'] +} diff --git a/reference-implementations/trust-authority/package.json b/reference-implementations/trust-authority/package.json index 19f3198..30a3eb3 100644 --- a/reference-implementations/trust-authority/package.json +++ b/reference-implementations/trust-authority/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@types/express": "^4.17.21", + "@types/jest": "^29.5.14", "@types/uuid": "^9.0.7", "@types/node": "^20.10.0", "typescript": "^5.3.0", diff --git a/reference-implementations/trust-authority/src/aggregation.test.ts b/reference-implementations/trust-authority/src/aggregation.test.ts new file mode 100644 index 0000000..ff86f60 --- /dev/null +++ b/reference-implementations/trust-authority/src/aggregation.test.ts @@ -0,0 +1,56 @@ +import { aggregateTrustScore } from './aggregation' +import { StoredReceipt, TTP_VERSION } from './types' +import { describe, it, expect } from '@jest/globals' + +function r(overrides: Partial): StoredReceipt { + return { + ttp_version: TTP_VERSION, + receipt_id: 'r-1', + agent_id: 'agent-1', + issuer_id: 'issuer-1', + event_type: 'api_call', + domain: 'retention', + timestamp: 1_700_000_000_000, + score: 0.9, + signature: 'sig', + accepted_at: 1_700_000_000_000, + ...overrides + } +} + +describe('aggregateTrustScore', () => { + it('throws when no receipts in window', () => { + const now = 1_700_000_500_000 + const receipts = [r({ timestamp: now - 1_000_000 })] + expect(() => aggregateTrustScore(receipts, now, { receiptWindowS: 300 })).toThrow('INSUFFICIENT_TRUST_DATA') + }) + + it('returns score in [0,1] with receipt metadata', () => { + const now = 1_700_000_200_000 + const receipts = [ + r({ receipt_id: 'a', issuer_id: 'issuer-a', score: 0.95, timestamp: now - 10_000 }), + r({ receipt_id: 'b', issuer_id: 'issuer-b', score: 0.85, timestamp: now - 20_000 }) + ] + + const out = aggregateTrustScore(receipts, now) + expect(out.score).toBeGreaterThanOrEqual(0) + expect(out.score).toBeLessThanOrEqual(1) + expect(out.contributingReceipts).toBe(2) + expect(out.contributingIssuers).toBe(2) + }) + + it('applies issuer weight cap to prevent domination', () => { + const now = 1_700_000_300_000 + const receipts: StoredReceipt[] = [] + + for (let i = 0; i < 30; i++) { + receipts.push(r({ receipt_id: `dom-${i}`, issuer_id: 'issuer-dominant', score: 0.95, timestamp: now - 1_000 - i })) + } + receipts.push(r({ receipt_id: 'independent-1', issuer_id: 'issuer-independent', score: 0.10, timestamp: now - 2_000 })) + + const capped = aggregateTrustScore(receipts, now, { maxIssuerWeight: 0.40 }) + const uncapped = aggregateTrustScore(receipts, now, { maxIssuerWeight: 1.0 }) + + expect(capped.score).toBeLessThan(uncapped.score) + }) +}) diff --git a/reference-implementations/trust-authority/src/crypto.ts b/reference-implementations/trust-authority/src/crypto.ts index f81125e..01acead 100644 --- a/reference-implementations/trust-authority/src/crypto.ts +++ b/reference-implementations/trust-authority/src/crypto.ts @@ -10,7 +10,7 @@ import { sha512 } from "@noble/hashes/sha512" import { BehavioralReceipt } from "./types" // Configure @noble/ed25519 to use SHA-512 (required for Ed25519) -ed.etc.sha512Sync = (...m) => sha512(...m) +ed.etc.sha512Sync = (message) => sha512(message) /** * Compute the canonical signing payload for a receipt. diff --git a/reference-implementations/trust-authority/src/index.ts b/reference-implementations/trust-authority/src/index.ts index e52ef6a..6bc78e9 100644 --- a/reference-implementations/trust-authority/src/index.ts +++ b/reference-implementations/trust-authority/src/index.ts @@ -25,7 +25,7 @@ import { createRouter } from "./routes" import { base64urlDecode, base64urlEncode } from "./crypto" // Configure @noble/ed25519 to use SHA-512 -ed.etc.sha512Sync = (...m) => sha512(...m) +ed.etc.sha512Sync = (message) => sha512(message) async function main() { const PORT = parseInt(process.env.PORT ?? "3000") diff --git a/reference-implementations/trust-authority/src/scripts/generate-keys.ts b/reference-implementations/trust-authority/src/scripts/generate-keys.ts index 60f1613..78ab64b 100644 --- a/reference-implementations/trust-authority/src/scripts/generate-keys.ts +++ b/reference-implementations/trust-authority/src/scripts/generate-keys.ts @@ -8,7 +8,7 @@ import { sha512 } from "@noble/hashes/sha512" import * as fs from "fs" import * as path from "path" -ed.etc.sha512Sync = (...m) => sha512(...m) +ed.etc.sha512Sync = (message) => sha512(message) function base64urlEncode(bytes: Uint8Array): string { return Buffer.from(bytes).toString("base64").replace(/\+/g, "-").replace(/\//g, "_").replace(/=/g, "") diff --git a/reference-implementations/trust-authority/tsconfig.json b/reference-implementations/trust-authority/tsconfig.json index c6dc354..c646016 100644 --- a/reference-implementations/trust-authority/tsconfig.json +++ b/reference-implementations/trust-authority/tsconfig.json @@ -14,5 +14,5 @@ "skipLibCheck": true }, "include": ["src/**/*"], - "exclude": ["node_modules", "dist"] + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] }