aqta-verify-receipt 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,17 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ Copyright 2026 Aqta Technologies Ltd
6
+
7
+ Licensed under the Apache License, Version 2.0 (the "License");
8
+ you may not use this file except in compliance with the License.
9
+ You may obtain a copy of the License at
10
+
11
+ http://www.apache.org/licenses/LICENSE-2.0
12
+
13
+ Unless required by applicable law or agreed to in writing, software
14
+ distributed under the License is distributed on an "AS IS" BASIS,
15
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ See the License for the specific language governing permissions and
17
+ limitations under the License.
package/README.md ADDED
@@ -0,0 +1,63 @@
1
+ # aqta-verify-receipt
2
+
3
+ Independent verifier for [AqtaCore](https://app.aqta.ai) attestation receipts.
4
+ Checks the Ed25519 signature on an enforcement-attestation receipt using only
5
+ the published public key: **no dependency on Aqta's servers**.
6
+
7
+ ## Why this exists
8
+
9
+ AqtaCore returns a signed receipt with every AI enforcement decision.
10
+ Regulators, auditors, and internal compliance teams need to verify those
11
+ receipts independently, without trusting the issuer. This package is the
12
+ reference implementation of that verifier, maintained by Aqta under the open
13
+ [ATTESTATION-v1](https://github.com/Aqta-ai/attestation-spec/blob/main/spec/ATTESTATION-v1.md)
14
+ format specification.
15
+
16
+ ## Install
17
+
18
+ ```bash
19
+ npm install aqta-verify-receipt
20
+ ```
21
+
22
+ ## Usage
23
+
24
+ ```ts
25
+ import { verifyReceipt, fetchPublishedPublicKey } from 'aqta-verify-receipt';
26
+
27
+ // One-time: fetch and pin the issuer's public key.
28
+ const trustedPublicKey = await fetchPublishedPublicKey();
29
+
30
+ // Per receipt:
31
+ const result = verifyReceipt(receipt, { trustedPublicKey });
32
+
33
+ if (!result.valid) {
34
+ throw new Error(`Receipt invalid: ${result.reason}`);
35
+ }
36
+ console.log('Receipt verified without contacting Aqta.');
37
+ ```
38
+
39
+ ## API
40
+
41
+ ### `verifyReceipt(receipt, options?) → { valid, reason? }`
42
+
43
+ Verifies an attestation receipt against the declared (or pinned) public key.
44
+
45
+ Options:
46
+ - `trustedPublicKey`: base64url public key. If set, the receipt's `public_key`
47
+ field must match byte for byte. Strongly recommended for production.
48
+ - `strictFields`: if `true` (default), any unknown top-level field causes
49
+ rejection, per ATTESTATION-v1 §4.
50
+
51
+ ### `fetchPublishedPublicKey(url?) → Promise<string>`
52
+
53
+ Fetches the AqtaCore public key from
54
+ `https://app.aqta.ai/security/pubkey.txt`. Pass a custom URL for self-hosted
55
+ issuers.
56
+
57
+ ## Receipt format
58
+
59
+ See [ATTESTATION-v1](https://github.com/Aqta-ai/attestation-spec/blob/main/spec/ATTESTATION-v1.md).
60
+
61
+ ## Licence
62
+
63
+ Apache-2.0. See `LICENSE`.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * aqta-verify-receipt
3
+ *
4
+ * Independent verifier for AqtaCore attestation receipts (ATTESTATION-v1).
5
+ * Verifies the Ed25519 signature on a receipt using only the published
6
+ * public key: no dependency on any third-party server.
7
+ *
8
+ * Spec: https://github.com/Aqta-ai/attestation-spec/blob/main/spec/ATTESTATION-v1.md
9
+ */
10
+ /** Required top-level fields of an ATTESTATION-v1 receipt. */
11
+ export interface AttestationReceipt {
12
+ v: number;
13
+ attestation_id: string;
14
+ trace_id: string;
15
+ org_id: string;
16
+ request_hash: string;
17
+ model: string;
18
+ outcome: 'ALLOWED' | 'BLOCKED' | 'SUPPRESSED' | 'PASSED';
19
+ policy_applied: string[];
20
+ cost_prevented_eur: number;
21
+ timestamp: string;
22
+ public_key: string;
23
+ signature: string;
24
+ }
25
+ export interface VerifyOptions {
26
+ /**
27
+ * The trusted public key (base64url, no padding). If supplied, the receipt
28
+ * is rejected unless its `public_key` field matches byte-for-byte. This
29
+ * prevents an attacker from substituting a different key.
30
+ *
31
+ * If omitted, the receipt is verified against whatever public key it
32
+ * declares. This is lower trust but useful for offline / one-shot checks.
33
+ */
34
+ trustedPublicKey?: string;
35
+ /**
36
+ * If true, unknown top-level fields in the receipt cause rejection. Default
37
+ * true per spec §4: "Verifiers MUST reject receipts containing unknown
38
+ * top-level fields."
39
+ */
40
+ strictFields?: boolean;
41
+ }
42
+ export interface VerifyResult {
43
+ valid: boolean;
44
+ reason?: string;
45
+ }
46
+ /**
47
+ * Verify an AqtaCore attestation receipt.
48
+ *
49
+ * @param receipt The full receipt object (including `signature`).
50
+ * @param options Optional verification constraints.
51
+ * @returns `{ valid: true }` if the signature is valid under the
52
+ * declared (and optionally pinned) public key; otherwise
53
+ * `{ valid: false, reason: string }`.
54
+ *
55
+ * @example
56
+ * const result = verifyReceipt(receipt, {
57
+ * trustedPublicKey: 'gUoUhIvptKAoLTnry3VrDtOQEWggGQveLrHFVrfNqmE',
58
+ * });
59
+ * if (!result.valid) throw new Error(`Receipt invalid: ${result.reason}`);
60
+ */
61
+ export declare function verifyReceipt(receipt: unknown, options?: VerifyOptions): VerifyResult;
62
+ /**
63
+ * Fetch the AqtaCore public key from the published URL and return it as a
64
+ * base64url string ready to pass to `verifyReceipt` as `trustedPublicKey`.
65
+ *
66
+ * Callers SHOULD pin this value once retrieved, rather than re-fetching on
67
+ * every verification.
68
+ */
69
+ export declare function fetchPublishedPublicKey(url?: string): Promise<string>;
70
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAKH,8DAA8D;AAC9D,MAAM,WAAW,kBAAkB;IACjC,CAAC,EAAE,MAAM,CAAC;IACV,cAAc,EAAE,MAAM,CAAC;IACvB,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,SAAS,GAAG,SAAS,GAAG,YAAY,GAAG,QAAQ,CAAC;IACzD,cAAc,EAAE,MAAM,EAAE,CAAC;IACzB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,aAAa;IAC5B;;;;;;;OAOG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAE1B;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,EAAE,OAAO,CAAC;IACf,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAoED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,aAAa,CAC3B,OAAO,EAAE,OAAO,EAChB,OAAO,GAAE,aAAkB,GAC1B,YAAY,CAuEd;AAED;;;;;;GAMG;AACH,wBAAsB,uBAAuB,CAC3C,GAAG,SAA4C,GAC9C,OAAO,CAAC,MAAM,CAAC,CAMjB"}
package/dist/index.js ADDED
@@ -0,0 +1,175 @@
1
+ "use strict";
2
+ /**
3
+ * aqta-verify-receipt
4
+ *
5
+ * Independent verifier for AqtaCore attestation receipts (ATTESTATION-v1).
6
+ * Verifies the Ed25519 signature on a receipt using only the published
7
+ * public key: no dependency on any third-party server.
8
+ *
9
+ * Spec: https://github.com/Aqta-ai/attestation-spec/blob/main/spec/ATTESTATION-v1.md
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.verifyReceipt = verifyReceipt;
16
+ exports.fetchPublishedPublicKey = fetchPublishedPublicKey;
17
+ const tweetnacl_1 = __importDefault(require("tweetnacl"));
18
+ const tweetnacl_util_1 = __importDefault(require("tweetnacl-util"));
19
+ const ALLOWED_OUTCOMES = new Set([
20
+ 'ALLOWED',
21
+ 'BLOCKED',
22
+ 'SUPPRESSED',
23
+ 'PASSED',
24
+ ]);
25
+ const REQUIRED_FIELDS = new Set([
26
+ 'v',
27
+ 'attestation_id',
28
+ 'trace_id',
29
+ 'org_id',
30
+ 'request_hash',
31
+ 'model',
32
+ 'outcome',
33
+ 'policy_applied',
34
+ 'cost_prevented_eur',
35
+ 'timestamp',
36
+ 'public_key',
37
+ 'signature',
38
+ ]);
39
+ /** Decode a base64url string (no padding) into a Uint8Array. */
40
+ function base64urlDecode(s) {
41
+ const padded = s + '='.repeat((4 - (s.length % 4)) % 4);
42
+ const b64 = padded.replace(/-/g, '+').replace(/_/g, '/');
43
+ return tweetnacl_util_1.default.decodeBase64(b64);
44
+ }
45
+ /**
46
+ * Canonical JSON serialisation per ATTESTATION-v1 §6.
47
+ *
48
+ * Equivalent to Python's:
49
+ * json.dumps(payload, sort_keys=True, separators=(",", ":")).encode("utf-8")
50
+ *
51
+ * This function handles only the receipt shape (flat object with scalar +
52
+ * array values). It is NOT a general-purpose canonical-JSON library.
53
+ */
54
+ function canonicalise(payload) {
55
+ const keys = Object.keys(payload).sort();
56
+ const parts = [];
57
+ for (const k of keys) {
58
+ parts.push(`${JSON.stringify(k)}:${canonicalValue(payload[k])}`);
59
+ }
60
+ return new TextEncoder().encode('{' + parts.join(',') + '}');
61
+ }
62
+ function canonicalValue(v) {
63
+ if (v === null || v === undefined) {
64
+ return 'null';
65
+ }
66
+ if (Array.isArray(v)) {
67
+ return '[' + v.map(canonicalValue).join(',') + ']';
68
+ }
69
+ if (typeof v === 'number') {
70
+ // Preserving the serialisation of floats such as 0.0 as "0.0" is NOT
71
+ // spec. JSON.stringify(0) yields "0" and that is what Python json.dumps
72
+ // yields for float 0.0 when sort_keys=True on a rounded value. For the
73
+ // one float field on the receipt (cost_prevented_eur) the issuer rounds
74
+ // to 6 dp and integers serialise as integers.
75
+ return JSON.stringify(v);
76
+ }
77
+ // Strings, booleans
78
+ return JSON.stringify(v);
79
+ }
80
+ /**
81
+ * Verify an AqtaCore attestation receipt.
82
+ *
83
+ * @param receipt The full receipt object (including `signature`).
84
+ * @param options Optional verification constraints.
85
+ * @returns `{ valid: true }` if the signature is valid under the
86
+ * declared (and optionally pinned) public key; otherwise
87
+ * `{ valid: false, reason: string }`.
88
+ *
89
+ * @example
90
+ * const result = verifyReceipt(receipt, {
91
+ * trustedPublicKey: 'gUoUhIvptKAoLTnry3VrDtOQEWggGQveLrHFVrfNqmE',
92
+ * });
93
+ * if (!result.valid) throw new Error(`Receipt invalid: ${result.reason}`);
94
+ */
95
+ function verifyReceipt(receipt, options = {}) {
96
+ if (typeof receipt !== 'object' || receipt === null) {
97
+ return { valid: false, reason: 'receipt is not an object' };
98
+ }
99
+ const r = receipt;
100
+ // Structural checks
101
+ for (const field of REQUIRED_FIELDS) {
102
+ if (!(field in r)) {
103
+ return { valid: false, reason: `missing required field: ${field}` };
104
+ }
105
+ }
106
+ if (options.strictFields !== false) {
107
+ for (const k of Object.keys(r)) {
108
+ if (!REQUIRED_FIELDS.has(k)) {
109
+ return { valid: false, reason: `unknown top-level field: ${k}` };
110
+ }
111
+ }
112
+ }
113
+ // Semantic sanity
114
+ if (r.v !== 1) {
115
+ return { valid: false, reason: `unsupported version: ${r.v}` };
116
+ }
117
+ if (typeof r.outcome !== 'string' || !ALLOWED_OUTCOMES.has(r.outcome)) {
118
+ return { valid: false, reason: `invalid outcome: ${String(r.outcome)}` };
119
+ }
120
+ if (!Array.isArray(r.policy_applied)) {
121
+ return { valid: false, reason: 'policy_applied must be an array' };
122
+ }
123
+ if (typeof r.request_hash !== 'string' || !/^[0-9a-f]{64}$/.test(r.request_hash)) {
124
+ return { valid: false, reason: 'request_hash must be 64 lowercase hex chars' };
125
+ }
126
+ // Public-key pinning
127
+ if (options.trustedPublicKey !== undefined &&
128
+ options.trustedPublicKey !== r.public_key) {
129
+ return {
130
+ valid: false,
131
+ reason: 'public_key does not match trusted key',
132
+ };
133
+ }
134
+ // Canonical payload (all fields except signature)
135
+ const payload = { ...r };
136
+ delete payload.signature;
137
+ let canonical;
138
+ try {
139
+ canonical = canonicalise(payload);
140
+ }
141
+ catch (e) {
142
+ return { valid: false, reason: `failed to canonicalise: ${String(e)}` };
143
+ }
144
+ // Ed25519 verification
145
+ try {
146
+ const sig = base64urlDecode(r.signature);
147
+ const pub = base64urlDecode(r.public_key);
148
+ if (sig.length !== 64) {
149
+ return { valid: false, reason: 'signature length != 64 bytes' };
150
+ }
151
+ if (pub.length !== 32) {
152
+ return { valid: false, reason: 'public key length != 32 bytes' };
153
+ }
154
+ const ok = tweetnacl_1.default.sign.detached.verify(canonical, sig, pub);
155
+ return ok ? { valid: true } : { valid: false, reason: 'signature check failed' };
156
+ }
157
+ catch (e) {
158
+ return { valid: false, reason: `signature decode error: ${String(e)}` };
159
+ }
160
+ }
161
+ /**
162
+ * Fetch the AqtaCore public key from the published URL and return it as a
163
+ * base64url string ready to pass to `verifyReceipt` as `trustedPublicKey`.
164
+ *
165
+ * Callers SHOULD pin this value once retrieved, rather than re-fetching on
166
+ * every verification.
167
+ */
168
+ async function fetchPublishedPublicKey(url = 'https://app.aqta.ai/security/pubkey.txt') {
169
+ const res = await fetch(url);
170
+ if (!res.ok) {
171
+ throw new Error(`failed to fetch public key: HTTP ${res.status}`);
172
+ }
173
+ return (await res.text()).trim();
174
+ }
175
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,6 @@
1
+ /**
2
+ * Interop tests: verify a receipt produced by the Python reference implementation
3
+ * against this TypeScript verifier. Run: `npm run build && node --test dist/index.test.js`.
4
+ */
5
+ export {};
6
+ //# sourceMappingURL=index.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.d.ts","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"AAAA;;;GAGG"}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ /**
3
+ * Interop tests: verify a receipt produced by the Python reference implementation
4
+ * against this TypeScript verifier. Run: `npm run build && node --test dist/index.test.js`.
5
+ */
6
+ var __importDefault = (this && this.__importDefault) || function (mod) {
7
+ return (mod && mod.__esModule) ? mod : { "default": mod };
8
+ };
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ const node_test_1 = require("node:test");
11
+ const strict_1 = __importDefault(require("node:assert/strict"));
12
+ const index_js_1 = require("./index.js");
13
+ /**
14
+ * Fixture receipt shape, used here to exercise the structural checks.
15
+ * Full signature-level interop is covered by the cross-implementation
16
+ * runner at `scripts/make-interop-fixture.mjs`, which generates a receipt
17
+ * with the reference issuer and verifies it with this library.
18
+ */
19
+ const FIXTURE_RECEIPT = {
20
+ v: 1,
21
+ attestation_id: 'a3f2b1c4-9d87-4e6f-b012-34567890abcd',
22
+ trace_id: 'trace-fixture-v1',
23
+ org_id: 'org-test',
24
+ request_hash: '8f3a7e2b9c4d5f6a1b0c9d8e7f6a5b4c3d2e1f0a9b8c7d6e5f4a3b2c1d0e9f8a',
25
+ model: 'gpt-4o',
26
+ outcome: 'ALLOWED',
27
+ policy_applied: ['budget_guard', 'loop_guard'],
28
+ cost_prevented_eur: 0.0,
29
+ timestamp: '2026-04-23T10:15:30.123456+00:00',
30
+ public_key: '', // populated by make-test-fixture.py
31
+ signature: '', // populated by make-test-fixture.py
32
+ };
33
+ (0, node_test_1.test)('rejects empty object', () => {
34
+ const res = (0, index_js_1.verifyReceipt)({});
35
+ strict_1.default.equal(res.valid, false);
36
+ });
37
+ (0, node_test_1.test)('rejects missing required field', () => {
38
+ const bad = { ...FIXTURE_RECEIPT };
39
+ delete bad.outcome;
40
+ const res = (0, index_js_1.verifyReceipt)(bad);
41
+ strict_1.default.equal(res.valid, false);
42
+ strict_1.default.match(res.reason ?? '', /missing required field/);
43
+ });
44
+ (0, node_test_1.test)('rejects unknown top-level field (strict mode)', () => {
45
+ const bad = { ...FIXTURE_RECEIPT, extra: 'foo' };
46
+ const res = (0, index_js_1.verifyReceipt)(bad);
47
+ strict_1.default.equal(res.valid, false);
48
+ strict_1.default.match(res.reason ?? '', /unknown top-level field/);
49
+ });
50
+ (0, node_test_1.test)('rejects unsupported version', () => {
51
+ const bad = { ...FIXTURE_RECEIPT, v: 2 };
52
+ const res = (0, index_js_1.verifyReceipt)(bad);
53
+ strict_1.default.equal(res.valid, false);
54
+ strict_1.default.match(res.reason ?? '', /unsupported version/);
55
+ });
56
+ (0, node_test_1.test)('rejects invalid outcome', () => {
57
+ const bad = { ...FIXTURE_RECEIPT, outcome: 'YOLO' };
58
+ const res = (0, index_js_1.verifyReceipt)(bad);
59
+ strict_1.default.equal(res.valid, false);
60
+ strict_1.default.match(res.reason ?? '', /invalid outcome/);
61
+ });
62
+ (0, node_test_1.test)('rejects malformed request_hash', () => {
63
+ const bad = { ...FIXTURE_RECEIPT, request_hash: 'not-a-hash' };
64
+ const res = (0, index_js_1.verifyReceipt)(bad);
65
+ strict_1.default.equal(res.valid, false);
66
+ strict_1.default.match(res.reason ?? '', /request_hash/);
67
+ });
68
+ (0, node_test_1.test)('rejects pinned-key mismatch', () => {
69
+ const res = (0, index_js_1.verifyReceipt)(FIXTURE_RECEIPT, {
70
+ trustedPublicKey: 'different_key_000000000000000000000000000000',
71
+ });
72
+ strict_1.default.equal(res.valid, false);
73
+ strict_1.default.match(res.reason ?? '', /does not match trusted key/);
74
+ });
75
+ // Note: full signature-verification interop test requires the fixture to be
76
+ // populated by `scripts/make-test-fixture.py` before running this suite.
77
+ // When FIXTURE_RECEIPT.signature is empty, the signature check is skipped.
78
+ (0, node_test_1.test)('signature decoder rejects empty signature', () => {
79
+ const res = (0, index_js_1.verifyReceipt)(FIXTURE_RECEIPT);
80
+ strict_1.default.equal(res.valid, false);
81
+ // Either the empty signature fails decode or fails length check
82
+ strict_1.default.ok(/signature/.test(res.reason ?? '') ||
83
+ /signature length/.test(res.reason ?? ''));
84
+ });
85
+ //# sourceMappingURL=index.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.test.js","sourceRoot":"","sources":["../src/index.test.ts"],"names":[],"mappings":"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"}
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "aqta-verify-receipt",
3
+ "version": "1.0.1",
4
+ "description": "Independent verifier for AqtaCore attestation receipts. Verifies the Ed25519 signature on an attestation receipt without trusting any third-party server. Reference implementation of ATTESTATION-v1.",
5
+ "main": "dist/index.js",
6
+ "types": "dist/index.d.ts",
7
+ "files": [
8
+ "dist/",
9
+ "README.md",
10
+ "LICENSE"
11
+ ],
12
+ "scripts": {
13
+ "build": "tsc",
14
+ "test": "node --test dist/index.test.js",
15
+ "prepublishOnly": "npm run build"
16
+ },
17
+ "keywords": [
18
+ "aqta",
19
+ "ai-governance",
20
+ "attestation",
21
+ "ed25519",
22
+ "zero-trust",
23
+ "eu-ai-act",
24
+ "dora",
25
+ "compliance",
26
+ "receipt-verification"
27
+ ],
28
+ "author": "Aqta Technologies Ltd",
29
+ "license": "Apache-2.0",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "https://github.com/Aqta-ai/attestation-spec.git",
33
+ "directory": "packages/verify-receipt"
34
+ },
35
+ "homepage": "https://github.com/Aqta-ai/attestation-spec",
36
+ "bugs": {
37
+ "url": "https://github.com/Aqta-ai/attestation-spec/issues"
38
+ },
39
+ "dependencies": {
40
+ "tweetnacl": "^1.0.3",
41
+ "tweetnacl-util": "^0.15.1"
42
+ },
43
+ "devDependencies": {
44
+ "@types/node": "^20.0.0",
45
+ "typescript": "^5.3.0"
46
+ },
47
+ "engines": {
48
+ "node": ">=18"
49
+ },
50
+ "publishConfig": {
51
+ "access": "public"
52
+ }
53
+ }