@taplid/client
Advanced tools
+4
-1
@@ -1,2 +0,2 @@ | ||
| import type { TaplidAuditRequest, TaplidAuditResponse, TaplidClientOptions, TaplidDecisionRouting, TaplidPublicLaneSummary, TaplidThresholdProximity } from './types.js'; | ||
| import type { TaplidAuditRequest, TaplidAuditResponse, TaplidClientOptions, TaplidDecisionRouting, TaplidPublicLaneSummary, TaplidThresholdProximity, TaplidVerifyAuditReport, TaplidVerifyAuditRequest } from './types.js'; | ||
| /** @internal Retained — routing no longer included in public response (ITD-192). */ | ||
@@ -17,3 +17,6 @@ export declare function isTaplidDecisionRouting(value: unknown): value is TaplidDecisionRouting; | ||
| }): Promise<TaplidAuditResponse>; | ||
| verifyAudit(input: TaplidVerifyAuditRequest, options?: { | ||
| signal?: AbortSignal | undefined; | ||
| }): Promise<TaplidVerifyAuditReport>; | ||
| } | ||
| //# sourceMappingURL=client.d.ts.map |
+178
-0
@@ -527,2 +527,123 @@ import { TaplidApiError } from './errors.js'; | ||
| } | ||
| // CHANGE [2026-06-16]: ITD-1131 — Verify Audit token normalizer. Mirrors | ||
| // normalizeAuditIdForRetrieval but allows dots (JWTs contain them) and a larger | ||
| // cap. CRITICAL: never put the token value itself into any error message. | ||
| const MAX_VERIFY_TOKEN_LENGTH = 8192; | ||
| function normalizeVerifyToken(token) { | ||
| const trimmed = token.trim(); | ||
| if (trimmed.length === 0) { | ||
| throw new TaplidApiError({ | ||
| code: 'invalid_request', | ||
| message: 'token must not be empty.', | ||
| retryable: false, | ||
| }); | ||
| } | ||
| if (trimmed.length > MAX_VERIFY_TOKEN_LENGTH) { | ||
| throw new TaplidApiError({ | ||
| code: 'invalid_request', | ||
| message: `token must not exceed ${MAX_VERIFY_TOKEN_LENGTH} characters.`, | ||
| retryable: false, | ||
| }); | ||
| } | ||
| return trimmed; | ||
| } | ||
| // CHANGE [2026-06-16]: ITD-1131 — enforce EXACTLY ONE of auditId/token. | ||
| function serializeVerifyAuditRequest(input) { | ||
| // Defensive: a JavaScript caller (no compile-time checking) may pass | ||
| // null/undefined/garbage. Re-widen to `unknown` and treat a field as | ||
| // "provided" only when it is a string, so we throw a clean `invalid_request` | ||
| // instead of a raw TypeError from `.trim()` on a non-string. | ||
| const candidate = input; | ||
| if (!isRecord(candidate)) { | ||
| throw new TaplidApiError({ | ||
| code: 'invalid_request', | ||
| message: 'verifyAudit requires an object with exactly one of auditId or token.', | ||
| retryable: false, | ||
| }); | ||
| } | ||
| const hasAuditId = typeof candidate.auditId === 'string'; | ||
| const hasToken = typeof candidate.token === 'string'; | ||
| if (hasAuditId === hasToken) { | ||
| throw new TaplidApiError({ | ||
| code: 'invalid_request', | ||
| message: 'Provide exactly one of auditId or token.', | ||
| retryable: false, | ||
| }); | ||
| } | ||
| if (typeof candidate.auditId === 'string') { | ||
| return { auditId: normalizeAuditIdForRetrieval(candidate.auditId) }; | ||
| } | ||
| if (typeof candidate.token === 'string') { | ||
| return { token: normalizeVerifyToken(candidate.token) }; | ||
| } | ||
| // Unreachable: the exactly-one guard above guarantees one branch fired. | ||
| throw new TaplidApiError({ | ||
| code: 'invalid_request', | ||
| message: 'Provide exactly one of auditId or token.', | ||
| retryable: false, | ||
| }); | ||
| } | ||
| // CHANGE [2026-06-16]: ITD-1131 — Verify Audit report shape guard (separate | ||
| // from isTaplidAuditResponse; the embedded result reuses that guard). | ||
| function isTaplidVerifyAuditReport(value) { | ||
| if (!isRecord(value)) { | ||
| return false; | ||
| } | ||
| const statusOk = value.status === 'valid' || | ||
| value.status === 'warning' || | ||
| value.status === 'invalid'; | ||
| const inputKindOk = value.inputKind === 'audit-id' || value.inputKind === 'token'; | ||
| const boolsOk = typeof value.isSignatureValid === 'boolean' && | ||
| typeof value.isIssuedByTaplid === 'boolean'; | ||
| const triOk = (candidate) => candidate === null || typeof candidate === 'boolean'; | ||
| const stringOrAbsent = (candidate) => candidate === undefined || typeof candidate === 'string'; | ||
| const numberOrAbsent = (candidate) => candidate === undefined || typeof candidate === 'number'; | ||
| // header/payload: null OR a record whose KNOWN fields carry the declared | ||
| // types (string/number). Unknown fields are ignored (forward-compatible). | ||
| const headerOk = value.header === null || | ||
| (isRecord(value.header) && | ||
| stringOrAbsent(value.header.alg) && | ||
| stringOrAbsent(value.header.kid) && | ||
| stringOrAbsent(value.header.typ)); | ||
| const payloadOk = value.payload === null || | ||
| (isRecord(value.payload) && | ||
| stringOrAbsent(value.payload.aud) && | ||
| stringOrAbsent(value.payload.auditId) && | ||
| stringOrAbsent(value.payload.auditMode) && | ||
| stringOrAbsent(value.payload.decision) && | ||
| stringOrAbsent(value.payload.inputHash) && | ||
| stringOrAbsent(value.payload.iss) && | ||
| stringOrAbsent(value.payload.resultHash) && | ||
| numberOrAbsent(value.payload.iat) && | ||
| numberOrAbsent(value.payload.trustScore) && | ||
| numberOrAbsent(value.payload.version)); | ||
| const resultOk = value.result === null || isTaplidAuditResponse(value.result); | ||
| // errorMessage appears ONLY on an 'invalid' report — the server structurally | ||
| // guarantees this. Reject a contradictory success report that carries one. | ||
| const errorMessageOk = value.errorMessage === undefined | ||
| ? true | ||
| : typeof value.errorMessage === 'string' && value.status === 'invalid'; | ||
| return (statusOk && | ||
| inputKindOk && | ||
| boolsOk && | ||
| triOk(value.resultHashMatches) && | ||
| triOk(value.signedFieldsMatch) && | ||
| headerOk && | ||
| payloadOk && | ||
| resultOk && | ||
| errorMessageOk); | ||
| } | ||
| /** | ||
| * Strip ITD-192 removed fields from any embedded audit result, and defensively | ||
| * drop a stray top-level `token`. The verify endpoint never returns a token, but | ||
| * the SDK must never surface a raw token even if a proxy/older server injected one. | ||
| */ | ||
| function sanitizeVerifyAuditReport(report) { | ||
| // eslint-disable-next-line @typescript-eslint/no-unused-vars -- destructure to strip | ||
| const { token: _token, ...rest } = report; | ||
| if (rest.result !== null) { | ||
| return { ...rest, result: sanitizeAuditResponse(rest.result) }; | ||
| } | ||
| return rest; | ||
| } | ||
| function resolveAuditMode(input) { | ||
@@ -757,3 +878,60 @@ const normalizedAuditMode = normalizeAuditMode(input.auditMode); | ||
| } | ||
| // CHANGE [2026-06-16]: ITD-1131 — verifyAudit: anonymous POST /verify-audit, | ||
| // no Authorization header even when #apiKey is set (mirrors getAudit). Sends a | ||
| // JSON body with exactly one of auditId/token; validates the verify report | ||
| // shape. The token value is never placed into any error message. | ||
| async verifyAudit(input, options) { | ||
| const requestBody = serializeVerifyAuditRequest(input); | ||
| const abortController = createAbortController({ | ||
| signal: options?.signal, | ||
| timeoutMs: this.#timeoutMs, | ||
| }); | ||
| try { | ||
| const response = await this.#fetch(new URL('/verify-audit', this.#baseUrl), { | ||
| method: 'POST', | ||
| headers: { 'content-type': 'application/json', accept: 'application/json' }, | ||
| body: JSON.stringify(requestBody), | ||
| signal: abortController.signal, | ||
| }); | ||
| const parsedBody = await parseJsonBody(response); | ||
| if (!response.ok) { | ||
| throw buildApiErrorFromResponse(response, parsedBody); | ||
| } | ||
| if (!isTaplidVerifyAuditReport(parsedBody)) { | ||
| throw new TaplidApiError({ | ||
| code: 'invalid_response', | ||
| message: 'Taplid returned an unexpected verification response shape.', | ||
| requestId: resolveRequestId(response), | ||
| retryable: false, | ||
| statusCode: response.status, | ||
| }); | ||
| } | ||
| return sanitizeVerifyAuditReport(parsedBody); | ||
| } | ||
| catch (error) { | ||
| if (error instanceof TaplidApiError) { | ||
| throw error; | ||
| } | ||
| if (isAbortError(error)) { | ||
| throw new TaplidApiError({ | ||
| cause: error, | ||
| code: abortController.timedOut() ? 'timeout' : 'aborted', | ||
| message: abortController.timedOut() | ||
| ? 'Taplid request timed out.' | ||
| : 'Taplid request was aborted.', | ||
| retryable: abortController.timedOut(), | ||
| }); | ||
| } | ||
| throw new TaplidApiError({ | ||
| cause: error, | ||
| code: 'network_error', | ||
| message: 'Taplid request failed before a response was received.', | ||
| retryable: true, | ||
| }); | ||
| } | ||
| finally { | ||
| abortController.cleanup(); | ||
| } | ||
| } | ||
| } | ||
| //# sourceMappingURL=client.js.map |
+1
-1
| export { Taplid } from './client.js'; | ||
| export { TaplidApiError } from './errors.js'; | ||
| export type { TaplidAuditEvidence, TaplidAuditMode, TaplidAuditRequest, TaplidAuditResponse, TaplidClientOptions, TaplidDecision, TaplidEvidenceSource, TaplidPublicContextSignalCategory, TaplidPublicDiagnosisSummary, TaplidPublicForensicContextDiagnostics, TaplidPublicForensicContextSignal, TaplidPublicForensicDiagnostics, TaplidPublicForensicEvidenceSummary, TaplidPublicForensicPromptDiagnostics, TaplidPublicForensicPromptSignal, TaplidPublicIssue, TaplidPublicLaneStrongestCode, TaplidPublicLaneSummary, TaplidPublicLaneSummaryLane, TaplidPublicRepairAction, TaplidPublicPromptSignalCategory, TaplidRepairPriority, TaplidRepairTarget, } from './types.js'; | ||
| export type { TaplidAuditEvidence, TaplidAuditMode, TaplidAuditRequest, TaplidAuditResponse, TaplidClientOptions, TaplidDecision, TaplidEvidenceSource, TaplidPublicContextSignalCategory, TaplidPublicDiagnosisSummary, TaplidPublicForensicContextDiagnostics, TaplidPublicForensicContextSignal, TaplidPublicForensicDiagnostics, TaplidPublicForensicEvidenceSummary, TaplidPublicForensicPromptDiagnostics, TaplidPublicForensicPromptSignal, TaplidPublicIssue, TaplidPublicLaneStrongestCode, TaplidPublicLaneSummary, TaplidPublicLaneSummaryLane, TaplidPublicRepairAction, TaplidPublicPromptSignalCategory, TaplidRepairPriority, TaplidRepairTarget, TaplidVerifyAuditInputKind, TaplidVerifyAuditJwtHeader, TaplidVerifyAuditPayload, TaplidVerifyAuditReport, TaplidVerifyAuditRequest, TaplidVerifyAuditStatus, } from './types.js'; | ||
| //# sourceMappingURL=index.d.ts.map |
+35
-0
@@ -195,2 +195,37 @@ export declare const TAPLID_DECISIONS: readonly ["ALLOW", "REVIEW", "BLOCK"]; | ||
| } | ||
| export type TaplidVerifyAuditStatus = 'valid' | 'warning' | 'invalid'; | ||
| export type TaplidVerifyAuditInputKind = 'audit-id' | 'token'; | ||
| export interface TaplidVerifyAuditRequest { | ||
| auditId?: string | undefined; | ||
| token?: string | undefined; | ||
| } | ||
| export interface TaplidVerifyAuditJwtHeader { | ||
| alg?: string | undefined; | ||
| kid?: string | undefined; | ||
| typ?: string | undefined; | ||
| } | ||
| export interface TaplidVerifyAuditPayload { | ||
| aud?: string | undefined; | ||
| auditId?: string | undefined; | ||
| auditMode?: string | undefined; | ||
| decision?: string | undefined; | ||
| iat?: number | undefined; | ||
| inputHash?: string | undefined; | ||
| iss?: string | undefined; | ||
| resultHash?: string | undefined; | ||
| trustScore?: number | undefined; | ||
| version?: number | undefined; | ||
| } | ||
| export interface TaplidVerifyAuditReport { | ||
| status: TaplidVerifyAuditStatus; | ||
| inputKind: TaplidVerifyAuditInputKind; | ||
| isSignatureValid: boolean; | ||
| isIssuedByTaplid: boolean; | ||
| resultHashMatches: boolean | null; | ||
| signedFieldsMatch: boolean | null; | ||
| header: TaplidVerifyAuditJwtHeader | null; | ||
| payload: TaplidVerifyAuditPayload | null; | ||
| result: TaplidAuditResponse | null; | ||
| errorMessage?: string | undefined; | ||
| } | ||
| export interface TaplidClientOptions { | ||
@@ -197,0 +232,0 @@ apiKey?: string | undefined; |
+2
-2
| { | ||
| "name": "@taplid/client", | ||
| "version": "0.5.15", | ||
| "version": "0.5.16", | ||
| "description": "Official Node.js SDK for the hosted Taplid audit API.", | ||
@@ -38,3 +38,3 @@ "keywords": [ | ||
| "dependencies": { | ||
| "@taplid/contract": "0.5.15" | ||
| "@taplid/contract": "0.5.16" | ||
| }, | ||
@@ -41,0 +41,0 @@ "engines": { |
+35
-3
@@ -37,2 +37,4 @@ # @taplid/client | ||
| TypeScript autocomplete exposes the three SDK methods: `taplid.audit(...)`, `taplid.getAudit(...)`, and `taplid.verifyAudit(...)`. | ||
| ## Retrieve an audit | ||
@@ -56,2 +58,32 @@ | ||
| ## Verify an audit | ||
| Verify a persisted audit ID or signed attestation token. No API key is required. | ||
| Verify by audit ID: | ||
| ```ts | ||
| import { Taplid } from '@taplid/client'; | ||
| const taplid = new Taplid(); | ||
| const verification = await taplid.verifyAudit({ | ||
| auditId: 'AUD-xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx', | ||
| }); | ||
| console.log(verification.status); | ||
| ``` | ||
| Verify by attestation token: | ||
| ```ts | ||
| const verification = await taplid.verifyAudit({ | ||
| token: 'eyJhbGciOiJFUzI1NiIsImtpZCI6InRhcGxpZC1lczI1Ni0yMDI2LTA2IiwidHlwIjoiSldUIn0...', | ||
| }); | ||
| console.log(verification.status); | ||
| ``` | ||
| Audit ID verification checks the persisted public result, signed fields, issuer, signature, and result hash. Token-only verification checks the signature and issuer, but cannot check the result hash without the persisted audit result. | ||
| ## Signed attestations | ||
@@ -67,3 +99,3 @@ | ||
| The `attestation.token` can be verified against the JWKS public key. The signed payload includes the `auditId`, `auditMode`, `decision`, `trustScore`, `inputHash`, and `resultHash`. | ||
| The `attestation.token` can be verified against the JWKS public key, by calling `taplid.verifyAudit(...)`, or by posting an `auditId` or `token` to `https://api.taplid.com/verify-audit`. The signed payload includes the `auditId`, `auditMode`, `decision`, `trustScore`, `inputHash`, and `resultHash`. | ||
@@ -215,4 +247,4 @@ ## HTTP API Example | ||
| - [Taplid Audit](https://taplid.com/audit) - run audits in the browser | ||
| - [Taplid CLI](https://www.npmjs.com/package/@taplid/cli) - run audits locally or in CI with npx @taplid/cli audit request.json | ||
| - [Taplid MCP](https://www.npmjs.com/package/@taplid/mcp) - expose Taplid as a tool to MCP-capable AI clients | ||
| - [Taplid CLI](https://www.npmjs.com/package/@taplid/cli) - run, retrieve, and verify audits locally or in CI | ||
| - [Taplid MCP](https://www.npmjs.com/package/@taplid/mcp) - expose Taplid audit, retrieval, and verification tools to MCP-capable AI clients | ||
| - [Taplid CLI eval](https://www.npmjs.com/package/@taplid/cli) - CI threshold gate via `npx @taplid/cli eval request.json --api-key tap_live_... --pass-threshold 80` (exits non-zero when below) | ||
@@ -219,0 +251,0 @@ - [Full docs](https://github.com/tonyfa/taplid#readme) |
62665
20.21%1244
21.13%253
14.48%10
11.11%+ Added
- Removed
Updated