@agent-receipts/crypto 0.1.0
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 +21 -0
- package/README.md +57 -0
- package/bin/generate-keys.ts +15 -0
- package/dist/index.d.mts +72 -0
- package/dist/index.d.ts +72 -0
- package/dist/index.js +110 -0
- package/dist/index.js.map +1 -0
- package/dist/index.mjs +78 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +62 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Webaes
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# @agent-receipts/crypto
|
|
2
|
+
|
|
3
|
+
Ed25519 signing and verification for the [Action Receipt Protocol](https://github.com/webaesbyamin/agent-receipts).
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install @agent-receipts/crypto
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
```typescript
|
|
14
|
+
import {
|
|
15
|
+
generateKeyPair,
|
|
16
|
+
signReceipt,
|
|
17
|
+
verifyReceipt,
|
|
18
|
+
getSignablePayload,
|
|
19
|
+
getPublicKeyFromPrivate,
|
|
20
|
+
} from '@agent-receipts/crypto'
|
|
21
|
+
|
|
22
|
+
// Generate a new Ed25519 key pair
|
|
23
|
+
const { privateKey, publicKey } = generateKeyPair()
|
|
24
|
+
|
|
25
|
+
// Sign a receipt
|
|
26
|
+
const signable = getSignablePayload(receipt)
|
|
27
|
+
const signature = signReceipt(signable, privateKey)
|
|
28
|
+
|
|
29
|
+
// Verify a receipt
|
|
30
|
+
const valid = verifyReceipt(signable, signature, publicKey)
|
|
31
|
+
|
|
32
|
+
// Derive public key from private
|
|
33
|
+
const pub = getPublicKeyFromPrivate(privateKey)
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
## API
|
|
37
|
+
|
|
38
|
+
| Function | Description |
|
|
39
|
+
|----------|-------------|
|
|
40
|
+
| `generateKeyPair()` | Generate an Ed25519 key pair (returns `{ privateKey, publicKey }` as hex strings) |
|
|
41
|
+
| `signReceipt(payload, privateKey)` | Sign a canonical payload, returns hex signature |
|
|
42
|
+
| `verifyReceipt(payload, signature, publicKey)` | Verify a signature against a payload and public key |
|
|
43
|
+
| `getSignablePayload(receipt)` | Extract the deterministic signable fields from a receipt |
|
|
44
|
+
| `getPublicKeyFromPrivate(privateKey)` | Derive the public key from a private key |
|
|
45
|
+
| `canonicalize(obj)` | Deterministic JSON serialization for signing |
|
|
46
|
+
|
|
47
|
+
## Key Generation CLI
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
npx @agent-receipts/crypto generate-keys
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
Outputs `RECEIPT_SIGNING_PRIVATE_KEY` and `RECEIPT_SIGNING_PUBLIC_KEY` for your `.env` file.
|
|
54
|
+
|
|
55
|
+
## License
|
|
56
|
+
|
|
57
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
#!/usr/bin/env npx tsx
|
|
2
|
+
import { generateKeyPair } from '../src/keys'
|
|
3
|
+
|
|
4
|
+
function main() {
|
|
5
|
+
const { privateKey, publicKey } = generateKeyPair()
|
|
6
|
+
|
|
7
|
+
console.log('\nAgent Receipts — Ed25519 Key Pair Generated\n')
|
|
8
|
+
console.log('Add these to your .env file:\n')
|
|
9
|
+
console.log(`RECEIPT_SIGNING_PRIVATE_KEY=${privateKey}`)
|
|
10
|
+
console.log(`RECEIPT_SIGNING_PUBLIC_KEY=${publicKey}`)
|
|
11
|
+
console.log('\nKeep the private key SECRET. Never commit it to git.')
|
|
12
|
+
console.log("The public key is safe to share — it's published at /.well-known/receipt-public-key.json\n")
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
main()
|
package/dist/index.d.mts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { SignablePayload } from '@agent-receipts/schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Produce a canonical JSON string from a SignablePayload.
|
|
5
|
+
* Keys are sorted alphabetically. Output is deterministic.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: SignablePayload contains only flat primitives (strings, nulls).
|
|
8
|
+
* No nested objects or arrays. Simple key sorting is sufficient here.
|
|
9
|
+
*
|
|
10
|
+
* For hashing arbitrary user input/output (in the SDK), use deep canonical
|
|
11
|
+
* sorting — that's a different function in the SDK package.
|
|
12
|
+
*/
|
|
13
|
+
declare function canonicalize(payload: SignablePayload): string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract the signable fields from a full receipt object.
|
|
17
|
+
* Maps receipt fields to the SignablePayload structure.
|
|
18
|
+
*
|
|
19
|
+
* NOTE: The API uses `timestamp` while the DB uses `created_at`.
|
|
20
|
+
* This function expects the API format (timestamp).
|
|
21
|
+
*/
|
|
22
|
+
declare function getSignablePayload(receipt: {
|
|
23
|
+
receipt_id: string;
|
|
24
|
+
chain_id: string;
|
|
25
|
+
receipt_type: string;
|
|
26
|
+
agent_id: string;
|
|
27
|
+
org_id: string;
|
|
28
|
+
action: string;
|
|
29
|
+
input_hash: string;
|
|
30
|
+
output_hash: string | null;
|
|
31
|
+
status: string;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
completed_at: string | null;
|
|
34
|
+
environment: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}): SignablePayload;
|
|
37
|
+
/**
|
|
38
|
+
* Sign a receipt payload with Ed25519.
|
|
39
|
+
*
|
|
40
|
+
* @param payload - The signable fields (12 deterministic fields)
|
|
41
|
+
* @param privateKey - Ed25519 private key as hex string (64 chars = 32 bytes)
|
|
42
|
+
* @returns Signature string in format "ed25519:<base64>"
|
|
43
|
+
*/
|
|
44
|
+
declare function signReceipt(payload: SignablePayload, privateKey: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Verify a receipt signature.
|
|
47
|
+
*
|
|
48
|
+
* @param payload - The signable fields (must match what was signed)
|
|
49
|
+
* @param signature - Signature string in format "ed25519:<base64>"
|
|
50
|
+
* @param publicKey - Ed25519 public key as hex string (64 chars = 32 bytes)
|
|
51
|
+
* @returns true if signature is valid
|
|
52
|
+
*/
|
|
53
|
+
declare function verifyReceipt(payload: SignablePayload, signature: string, publicKey: string): boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a new Ed25519 key pair.
|
|
57
|
+
*
|
|
58
|
+
* @returns { privateKey: string, publicKey: string } — both hex-encoded (64 chars each)
|
|
59
|
+
*/
|
|
60
|
+
declare function generateKeyPair(): {
|
|
61
|
+
privateKey: string;
|
|
62
|
+
publicKey: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Derive public key from private key.
|
|
66
|
+
*
|
|
67
|
+
* @param privateKeyHex - Ed25519 private key as hex string (64 chars)
|
|
68
|
+
* @returns Public key as hex string (64 chars)
|
|
69
|
+
*/
|
|
70
|
+
declare function getPublicKeyFromPrivate(privateKeyHex: string): string;
|
|
71
|
+
|
|
72
|
+
export { canonicalize, generateKeyPair, getPublicKeyFromPrivate, getSignablePayload, signReceipt, verifyReceipt };
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { SignablePayload } from '@agent-receipts/schema';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Produce a canonical JSON string from a SignablePayload.
|
|
5
|
+
* Keys are sorted alphabetically. Output is deterministic.
|
|
6
|
+
*
|
|
7
|
+
* IMPORTANT: SignablePayload contains only flat primitives (strings, nulls).
|
|
8
|
+
* No nested objects or arrays. Simple key sorting is sufficient here.
|
|
9
|
+
*
|
|
10
|
+
* For hashing arbitrary user input/output (in the SDK), use deep canonical
|
|
11
|
+
* sorting — that's a different function in the SDK package.
|
|
12
|
+
*/
|
|
13
|
+
declare function canonicalize(payload: SignablePayload): string;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Extract the signable fields from a full receipt object.
|
|
17
|
+
* Maps receipt fields to the SignablePayload structure.
|
|
18
|
+
*
|
|
19
|
+
* NOTE: The API uses `timestamp` while the DB uses `created_at`.
|
|
20
|
+
* This function expects the API format (timestamp).
|
|
21
|
+
*/
|
|
22
|
+
declare function getSignablePayload(receipt: {
|
|
23
|
+
receipt_id: string;
|
|
24
|
+
chain_id: string;
|
|
25
|
+
receipt_type: string;
|
|
26
|
+
agent_id: string;
|
|
27
|
+
org_id: string;
|
|
28
|
+
action: string;
|
|
29
|
+
input_hash: string;
|
|
30
|
+
output_hash: string | null;
|
|
31
|
+
status: string;
|
|
32
|
+
timestamp: string;
|
|
33
|
+
completed_at: string | null;
|
|
34
|
+
environment: string;
|
|
35
|
+
[key: string]: unknown;
|
|
36
|
+
}): SignablePayload;
|
|
37
|
+
/**
|
|
38
|
+
* Sign a receipt payload with Ed25519.
|
|
39
|
+
*
|
|
40
|
+
* @param payload - The signable fields (12 deterministic fields)
|
|
41
|
+
* @param privateKey - Ed25519 private key as hex string (64 chars = 32 bytes)
|
|
42
|
+
* @returns Signature string in format "ed25519:<base64>"
|
|
43
|
+
*/
|
|
44
|
+
declare function signReceipt(payload: SignablePayload, privateKey: string): string;
|
|
45
|
+
/**
|
|
46
|
+
* Verify a receipt signature.
|
|
47
|
+
*
|
|
48
|
+
* @param payload - The signable fields (must match what was signed)
|
|
49
|
+
* @param signature - Signature string in format "ed25519:<base64>"
|
|
50
|
+
* @param publicKey - Ed25519 public key as hex string (64 chars = 32 bytes)
|
|
51
|
+
* @returns true if signature is valid
|
|
52
|
+
*/
|
|
53
|
+
declare function verifyReceipt(payload: SignablePayload, signature: string, publicKey: string): boolean;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Generate a new Ed25519 key pair.
|
|
57
|
+
*
|
|
58
|
+
* @returns { privateKey: string, publicKey: string } — both hex-encoded (64 chars each)
|
|
59
|
+
*/
|
|
60
|
+
declare function generateKeyPair(): {
|
|
61
|
+
privateKey: string;
|
|
62
|
+
publicKey: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Derive public key from private key.
|
|
66
|
+
*
|
|
67
|
+
* @param privateKeyHex - Ed25519 private key as hex string (64 chars)
|
|
68
|
+
* @returns Public key as hex string (64 chars)
|
|
69
|
+
*/
|
|
70
|
+
declare function getPublicKeyFromPrivate(privateKeyHex: string): string;
|
|
71
|
+
|
|
72
|
+
export { canonicalize, generateKeyPair, getPublicKeyFromPrivate, getSignablePayload, signReceipt, verifyReceipt };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __defProp = Object.defineProperty;
|
|
3
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
4
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
5
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
6
|
+
var __export = (target, all) => {
|
|
7
|
+
for (var name in all)
|
|
8
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
9
|
+
};
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (let key of __getOwnPropNames(from))
|
|
13
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
14
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
15
|
+
}
|
|
16
|
+
return to;
|
|
17
|
+
};
|
|
18
|
+
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
19
|
+
|
|
20
|
+
// src/index.ts
|
|
21
|
+
var index_exports = {};
|
|
22
|
+
__export(index_exports, {
|
|
23
|
+
canonicalize: () => canonicalize,
|
|
24
|
+
generateKeyPair: () => generateKeyPair,
|
|
25
|
+
getPublicKeyFromPrivate: () => getPublicKeyFromPrivate,
|
|
26
|
+
getSignablePayload: () => getSignablePayload,
|
|
27
|
+
signReceipt: () => signReceipt,
|
|
28
|
+
verifyReceipt: () => verifyReceipt
|
|
29
|
+
});
|
|
30
|
+
module.exports = __toCommonJS(index_exports);
|
|
31
|
+
|
|
32
|
+
// src/canonical.ts
|
|
33
|
+
function canonicalize(payload) {
|
|
34
|
+
const sorted = Object.keys(payload).sort();
|
|
35
|
+
return JSON.stringify(payload, sorted);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// src/sign.ts
|
|
39
|
+
var import_ed25519 = require("@noble/ed25519");
|
|
40
|
+
var import_sha512 = require("@noble/hashes/sha512");
|
|
41
|
+
import_ed25519.etc.sha512Sync = (...messages) => (0, import_sha512.sha512)(import_ed25519.etc.concatBytes(...messages));
|
|
42
|
+
function getSignablePayload(receipt) {
|
|
43
|
+
return {
|
|
44
|
+
receipt_id: receipt.receipt_id,
|
|
45
|
+
chain_id: receipt.chain_id,
|
|
46
|
+
receipt_type: receipt.receipt_type,
|
|
47
|
+
agent_id: receipt.agent_id,
|
|
48
|
+
org_id: receipt.org_id,
|
|
49
|
+
action: receipt.action,
|
|
50
|
+
input_hash: receipt.input_hash,
|
|
51
|
+
output_hash: receipt.output_hash,
|
|
52
|
+
status: receipt.status,
|
|
53
|
+
timestamp: receipt.timestamp,
|
|
54
|
+
completed_at: receipt.completed_at,
|
|
55
|
+
environment: receipt.environment
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function signReceipt(payload, privateKey) {
|
|
59
|
+
const canonical = canonicalize(payload);
|
|
60
|
+
const message = new TextEncoder().encode(canonical);
|
|
61
|
+
const privKeyBytes = import_ed25519.etc.hexToBytes(privateKey);
|
|
62
|
+
const signature = (0, import_ed25519.sign)(message, privKeyBytes);
|
|
63
|
+
return `ed25519:${bytesToBase64(signature)}`;
|
|
64
|
+
}
|
|
65
|
+
function verifyReceipt(payload, signature, publicKey) {
|
|
66
|
+
try {
|
|
67
|
+
if (!signature.startsWith("ed25519:")) {
|
|
68
|
+
return false;
|
|
69
|
+
}
|
|
70
|
+
const canonical = canonicalize(payload);
|
|
71
|
+
const message = new TextEncoder().encode(canonical);
|
|
72
|
+
const sigBytes = base64ToBytes(signature.slice("ed25519:".length));
|
|
73
|
+
const pubKeyBytes = import_ed25519.etc.hexToBytes(publicKey);
|
|
74
|
+
return (0, import_ed25519.verify)(sigBytes, message, pubKeyBytes);
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function bytesToBase64(bytes) {
|
|
80
|
+
return Buffer.from(bytes).toString("base64");
|
|
81
|
+
}
|
|
82
|
+
function base64ToBytes(b64) {
|
|
83
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// src/keys.ts
|
|
87
|
+
var import_ed255192 = require("@noble/ed25519");
|
|
88
|
+
function generateKeyPair() {
|
|
89
|
+
const privateKeyBytes = import_ed255192.utils.randomPrivateKey();
|
|
90
|
+
const publicKeyBytes = (0, import_ed255192.getPublicKey)(privateKeyBytes);
|
|
91
|
+
return {
|
|
92
|
+
privateKey: import_ed255192.etc.bytesToHex(privateKeyBytes),
|
|
93
|
+
publicKey: import_ed255192.etc.bytesToHex(publicKeyBytes)
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
function getPublicKeyFromPrivate(privateKeyHex) {
|
|
97
|
+
const privKeyBytes = import_ed255192.etc.hexToBytes(privateKeyHex);
|
|
98
|
+
const pubKeyBytes = (0, import_ed255192.getPublicKey)(privKeyBytes);
|
|
99
|
+
return import_ed255192.etc.bytesToHex(pubKeyBytes);
|
|
100
|
+
}
|
|
101
|
+
// Annotate the CommonJS export names for ESM import in node:
|
|
102
|
+
0 && (module.exports = {
|
|
103
|
+
canonicalize,
|
|
104
|
+
generateKeyPair,
|
|
105
|
+
getPublicKeyFromPrivate,
|
|
106
|
+
getSignablePayload,
|
|
107
|
+
signReceipt,
|
|
108
|
+
verifyReceipt
|
|
109
|
+
});
|
|
110
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/index.ts","../src/canonical.ts","../src/sign.ts","../src/keys.ts"],"sourcesContent":["export { canonicalize } from './canonical'\nexport { signReceipt, verifyReceipt, getSignablePayload } from './sign'\nexport { generateKeyPair, getPublicKeyFromPrivate } from './keys'\n","import type { SignablePayload } from '@agent-receipts/schema'\n\n/**\n * Produce a canonical JSON string from a SignablePayload.\n * Keys are sorted alphabetically. Output is deterministic.\n *\n * IMPORTANT: SignablePayload contains only flat primitives (strings, nulls).\n * No nested objects or arrays. Simple key sorting is sufficient here.\n *\n * For hashing arbitrary user input/output (in the SDK), use deep canonical\n * sorting — that's a different function in the SDK package.\n */\nexport function canonicalize(payload: SignablePayload): string {\n const sorted = Object.keys(payload).sort()\n return JSON.stringify(payload, sorted)\n}\n","import { sign, verify, etc, getPublicKey } from '@noble/ed25519'\nimport { sha512 } from '@noble/hashes/sha512'\nimport type { SignablePayload } from '@agent-receipts/schema'\nimport { canonicalize } from './canonical'\n\n// Configure noble/ed25519 to use sha512 for sync operations.\n// This is required by @noble/ed25519 v2 — without it, sync sign/verify throw.\netc.sha512Sync = (...messages: Uint8Array[]): Uint8Array =>\n sha512(etc.concatBytes(...messages))\n\n/**\n * Extract the signable fields from a full receipt object.\n * Maps receipt fields to the SignablePayload structure.\n *\n * NOTE: The API uses `timestamp` while the DB uses `created_at`.\n * This function expects the API format (timestamp).\n */\nexport function getSignablePayload(receipt: {\n receipt_id: string\n chain_id: string\n receipt_type: string\n agent_id: string\n org_id: string\n action: string\n input_hash: string\n output_hash: string | null\n status: string\n timestamp: string\n completed_at: string | null\n environment: string\n [key: string]: unknown\n}): SignablePayload {\n return {\n receipt_id: receipt.receipt_id,\n chain_id: receipt.chain_id,\n receipt_type: receipt.receipt_type as SignablePayload['receipt_type'],\n agent_id: receipt.agent_id,\n org_id: receipt.org_id,\n action: receipt.action,\n input_hash: receipt.input_hash,\n output_hash: receipt.output_hash,\n status: receipt.status as SignablePayload['status'],\n timestamp: receipt.timestamp,\n completed_at: receipt.completed_at,\n environment: receipt.environment as SignablePayload['environment'],\n }\n}\n\n/**\n * Sign a receipt payload with Ed25519.\n *\n * @param payload - The signable fields (12 deterministic fields)\n * @param privateKey - Ed25519 private key as hex string (64 chars = 32 bytes)\n * @returns Signature string in format \"ed25519:<base64>\"\n */\nexport function signReceipt(\n payload: SignablePayload,\n privateKey: string\n): string {\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const privKeyBytes = etc.hexToBytes(privateKey)\n const signature = sign(message, privKeyBytes)\n return `ed25519:${bytesToBase64(signature)}`\n}\n\n/**\n * Verify a receipt signature.\n *\n * @param payload - The signable fields (must match what was signed)\n * @param signature - Signature string in format \"ed25519:<base64>\"\n * @param publicKey - Ed25519 public key as hex string (64 chars = 32 bytes)\n * @returns true if signature is valid\n */\nexport function verifyReceipt(\n payload: SignablePayload,\n signature: string,\n publicKey: string\n): boolean {\n try {\n if (!signature.startsWith('ed25519:')) {\n return false\n }\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const sigBytes = base64ToBytes(signature.slice('ed25519:'.length))\n const pubKeyBytes = etc.hexToBytes(publicKey)\n return verify(sigBytes, message, pubKeyBytes)\n } catch {\n return false\n }\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64')\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n return new Uint8Array(Buffer.from(b64, 'base64'))\n}\n","import { getPublicKey, etc, utils } from '@noble/ed25519'\n\n/**\n * Generate a new Ed25519 key pair.\n *\n * @returns { privateKey: string, publicKey: string } — both hex-encoded (64 chars each)\n */\nexport function generateKeyPair(): {\n privateKey: string\n publicKey: string\n} {\n const privateKeyBytes = utils.randomPrivateKey()\n const publicKeyBytes = getPublicKey(privateKeyBytes)\n return {\n privateKey: etc.bytesToHex(privateKeyBytes),\n publicKey: etc.bytesToHex(publicKeyBytes),\n }\n}\n\n/**\n * Derive public key from private key.\n *\n * @param privateKeyHex - Ed25519 private key as hex string (64 chars)\n * @returns Public key as hex string (64 chars)\n */\nexport function getPublicKeyFromPrivate(privateKeyHex: string): string {\n const privKeyBytes = etc.hexToBytes(privateKeyHex)\n const pubKeyBytes = getPublicKey(privKeyBytes)\n return etc.bytesToHex(pubKeyBytes)\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACYO,SAAS,aAAa,SAAkC;AAC7D,QAAM,SAAS,OAAO,KAAK,OAAO,EAAE,KAAK;AACzC,SAAO,KAAK,UAAU,SAAS,MAAM;AACvC;;;ACfA,qBAAgD;AAChD,oBAAuB;AAMvB,mBAAI,aAAa,IAAI,iBACnB,sBAAO,mBAAI,YAAY,GAAG,QAAQ,CAAC;AAS9B,SAAS,mBAAmB,SAcf;AAClB,SAAO;AAAA,IACL,YAAY,QAAQ;AAAA,IACpB,UAAU,QAAQ;AAAA,IAClB,cAAc,QAAQ;AAAA,IACtB,UAAU,QAAQ;AAAA,IAClB,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,IAChB,YAAY,QAAQ;AAAA,IACpB,aAAa,QAAQ;AAAA,IACrB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,cAAc,QAAQ;AAAA,IACtB,aAAa,QAAQ;AAAA,EACvB;AACF;AASO,SAAS,YACd,SACA,YACQ;AACR,QAAM,YAAY,aAAa,OAAO;AACtC,QAAM,UAAU,IAAI,YAAY,EAAE,OAAO,SAAS;AAClD,QAAM,eAAe,mBAAI,WAAW,UAAU;AAC9C,QAAM,gBAAY,qBAAK,SAAS,YAAY;AAC5C,SAAO,WAAW,cAAc,SAAS,CAAC;AAC5C;AAUO,SAAS,cACd,SACA,WACA,WACS;AACT,MAAI;AACF,QAAI,CAAC,UAAU,WAAW,UAAU,GAAG;AACrC,aAAO;AAAA,IACT;AACA,UAAM,YAAY,aAAa,OAAO;AACtC,UAAM,UAAU,IAAI,YAAY,EAAE,OAAO,SAAS;AAClD,UAAM,WAAW,cAAc,UAAU,MAAM,WAAW,MAAM,CAAC;AACjE,UAAM,cAAc,mBAAI,WAAW,SAAS;AAC5C,eAAO,uBAAO,UAAU,SAAS,WAAW;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,cAAc,OAA2B;AAChD,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ;AAC7C;AAEA,SAAS,cAAc,KAAyB;AAC9C,SAAO,IAAI,WAAW,OAAO,KAAK,KAAK,QAAQ,CAAC;AAClD;;;ACnGA,IAAAA,kBAAyC;AAOlC,SAAS,kBAGd;AACA,QAAM,kBAAkB,sBAAM,iBAAiB;AAC/C,QAAM,qBAAiB,8BAAa,eAAe;AACnD,SAAO;AAAA,IACL,YAAY,oBAAI,WAAW,eAAe;AAAA,IAC1C,WAAW,oBAAI,WAAW,cAAc;AAAA,EAC1C;AACF;AAQO,SAAS,wBAAwB,eAA+B;AACrE,QAAM,eAAe,oBAAI,WAAW,aAAa;AACjD,QAAM,kBAAc,8BAAa,YAAY;AAC7C,SAAO,oBAAI,WAAW,WAAW;AACnC;","names":["import_ed25519"]}
|
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// src/canonical.ts
|
|
2
|
+
function canonicalize(payload) {
|
|
3
|
+
const sorted = Object.keys(payload).sort();
|
|
4
|
+
return JSON.stringify(payload, sorted);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/sign.ts
|
|
8
|
+
import { sign, verify, etc } from "@noble/ed25519";
|
|
9
|
+
import { sha512 } from "@noble/hashes/sha512";
|
|
10
|
+
etc.sha512Sync = (...messages) => sha512(etc.concatBytes(...messages));
|
|
11
|
+
function getSignablePayload(receipt) {
|
|
12
|
+
return {
|
|
13
|
+
receipt_id: receipt.receipt_id,
|
|
14
|
+
chain_id: receipt.chain_id,
|
|
15
|
+
receipt_type: receipt.receipt_type,
|
|
16
|
+
agent_id: receipt.agent_id,
|
|
17
|
+
org_id: receipt.org_id,
|
|
18
|
+
action: receipt.action,
|
|
19
|
+
input_hash: receipt.input_hash,
|
|
20
|
+
output_hash: receipt.output_hash,
|
|
21
|
+
status: receipt.status,
|
|
22
|
+
timestamp: receipt.timestamp,
|
|
23
|
+
completed_at: receipt.completed_at,
|
|
24
|
+
environment: receipt.environment
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
function signReceipt(payload, privateKey) {
|
|
28
|
+
const canonical = canonicalize(payload);
|
|
29
|
+
const message = new TextEncoder().encode(canonical);
|
|
30
|
+
const privKeyBytes = etc.hexToBytes(privateKey);
|
|
31
|
+
const signature = sign(message, privKeyBytes);
|
|
32
|
+
return `ed25519:${bytesToBase64(signature)}`;
|
|
33
|
+
}
|
|
34
|
+
function verifyReceipt(payload, signature, publicKey) {
|
|
35
|
+
try {
|
|
36
|
+
if (!signature.startsWith("ed25519:")) {
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
const canonical = canonicalize(payload);
|
|
40
|
+
const message = new TextEncoder().encode(canonical);
|
|
41
|
+
const sigBytes = base64ToBytes(signature.slice("ed25519:".length));
|
|
42
|
+
const pubKeyBytes = etc.hexToBytes(publicKey);
|
|
43
|
+
return verify(sigBytes, message, pubKeyBytes);
|
|
44
|
+
} catch {
|
|
45
|
+
return false;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
function bytesToBase64(bytes) {
|
|
49
|
+
return Buffer.from(bytes).toString("base64");
|
|
50
|
+
}
|
|
51
|
+
function base64ToBytes(b64) {
|
|
52
|
+
return new Uint8Array(Buffer.from(b64, "base64"));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// src/keys.ts
|
|
56
|
+
import { getPublicKey as getPublicKey2, etc as etc2, utils } from "@noble/ed25519";
|
|
57
|
+
function generateKeyPair() {
|
|
58
|
+
const privateKeyBytes = utils.randomPrivateKey();
|
|
59
|
+
const publicKeyBytes = getPublicKey2(privateKeyBytes);
|
|
60
|
+
return {
|
|
61
|
+
privateKey: etc2.bytesToHex(privateKeyBytes),
|
|
62
|
+
publicKey: etc2.bytesToHex(publicKeyBytes)
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function getPublicKeyFromPrivate(privateKeyHex) {
|
|
66
|
+
const privKeyBytes = etc2.hexToBytes(privateKeyHex);
|
|
67
|
+
const pubKeyBytes = getPublicKey2(privKeyBytes);
|
|
68
|
+
return etc2.bytesToHex(pubKeyBytes);
|
|
69
|
+
}
|
|
70
|
+
export {
|
|
71
|
+
canonicalize,
|
|
72
|
+
generateKeyPair,
|
|
73
|
+
getPublicKeyFromPrivate,
|
|
74
|
+
getSignablePayload,
|
|
75
|
+
signReceipt,
|
|
76
|
+
verifyReceipt
|
|
77
|
+
};
|
|
78
|
+
//# sourceMappingURL=index.mjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/canonical.ts","../src/sign.ts","../src/keys.ts"],"sourcesContent":["import type { SignablePayload } from '@agent-receipts/schema'\n\n/**\n * Produce a canonical JSON string from a SignablePayload.\n * Keys are sorted alphabetically. Output is deterministic.\n *\n * IMPORTANT: SignablePayload contains only flat primitives (strings, nulls).\n * No nested objects or arrays. Simple key sorting is sufficient here.\n *\n * For hashing arbitrary user input/output (in the SDK), use deep canonical\n * sorting — that's a different function in the SDK package.\n */\nexport function canonicalize(payload: SignablePayload): string {\n const sorted = Object.keys(payload).sort()\n return JSON.stringify(payload, sorted)\n}\n","import { sign, verify, etc, getPublicKey } from '@noble/ed25519'\nimport { sha512 } from '@noble/hashes/sha512'\nimport type { SignablePayload } from '@agent-receipts/schema'\nimport { canonicalize } from './canonical'\n\n// Configure noble/ed25519 to use sha512 for sync operations.\n// This is required by @noble/ed25519 v2 — without it, sync sign/verify throw.\netc.sha512Sync = (...messages: Uint8Array[]): Uint8Array =>\n sha512(etc.concatBytes(...messages))\n\n/**\n * Extract the signable fields from a full receipt object.\n * Maps receipt fields to the SignablePayload structure.\n *\n * NOTE: The API uses `timestamp` while the DB uses `created_at`.\n * This function expects the API format (timestamp).\n */\nexport function getSignablePayload(receipt: {\n receipt_id: string\n chain_id: string\n receipt_type: string\n agent_id: string\n org_id: string\n action: string\n input_hash: string\n output_hash: string | null\n status: string\n timestamp: string\n completed_at: string | null\n environment: string\n [key: string]: unknown\n}): SignablePayload {\n return {\n receipt_id: receipt.receipt_id,\n chain_id: receipt.chain_id,\n receipt_type: receipt.receipt_type as SignablePayload['receipt_type'],\n agent_id: receipt.agent_id,\n org_id: receipt.org_id,\n action: receipt.action,\n input_hash: receipt.input_hash,\n output_hash: receipt.output_hash,\n status: receipt.status as SignablePayload['status'],\n timestamp: receipt.timestamp,\n completed_at: receipt.completed_at,\n environment: receipt.environment as SignablePayload['environment'],\n }\n}\n\n/**\n * Sign a receipt payload with Ed25519.\n *\n * @param payload - The signable fields (12 deterministic fields)\n * @param privateKey - Ed25519 private key as hex string (64 chars = 32 bytes)\n * @returns Signature string in format \"ed25519:<base64>\"\n */\nexport function signReceipt(\n payload: SignablePayload,\n privateKey: string\n): string {\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const privKeyBytes = etc.hexToBytes(privateKey)\n const signature = sign(message, privKeyBytes)\n return `ed25519:${bytesToBase64(signature)}`\n}\n\n/**\n * Verify a receipt signature.\n *\n * @param payload - The signable fields (must match what was signed)\n * @param signature - Signature string in format \"ed25519:<base64>\"\n * @param publicKey - Ed25519 public key as hex string (64 chars = 32 bytes)\n * @returns true if signature is valid\n */\nexport function verifyReceipt(\n payload: SignablePayload,\n signature: string,\n publicKey: string\n): boolean {\n try {\n if (!signature.startsWith('ed25519:')) {\n return false\n }\n const canonical = canonicalize(payload)\n const message = new TextEncoder().encode(canonical)\n const sigBytes = base64ToBytes(signature.slice('ed25519:'.length))\n const pubKeyBytes = etc.hexToBytes(publicKey)\n return verify(sigBytes, message, pubKeyBytes)\n } catch {\n return false\n }\n}\n\nfunction bytesToBase64(bytes: Uint8Array): string {\n return Buffer.from(bytes).toString('base64')\n}\n\nfunction base64ToBytes(b64: string): Uint8Array {\n return new Uint8Array(Buffer.from(b64, 'base64'))\n}\n","import { getPublicKey, etc, utils } from '@noble/ed25519'\n\n/**\n * Generate a new Ed25519 key pair.\n *\n * @returns { privateKey: string, publicKey: string } — both hex-encoded (64 chars each)\n */\nexport function generateKeyPair(): {\n privateKey: string\n publicKey: string\n} {\n const privateKeyBytes = utils.randomPrivateKey()\n const publicKeyBytes = getPublicKey(privateKeyBytes)\n return {\n privateKey: etc.bytesToHex(privateKeyBytes),\n publicKey: etc.bytesToHex(publicKeyBytes),\n }\n}\n\n/**\n * Derive public key from private key.\n *\n * @param privateKeyHex - Ed25519 private key as hex string (64 chars)\n * @returns Public key as hex string (64 chars)\n */\nexport function getPublicKeyFromPrivate(privateKeyHex: string): string {\n const privKeyBytes = etc.hexToBytes(privateKeyHex)\n const pubKeyBytes = getPublicKey(privKeyBytes)\n return etc.bytesToHex(pubKeyBytes)\n}\n"],"mappings":";AAYO,SAAS,aAAa,SAAkC;AAC7D,QAAM,SAAS,OAAO,KAAK,OAAO,EAAE,KAAK;AACzC,SAAO,KAAK,UAAU,SAAS,MAAM;AACvC;;;ACfA,SAAS,MAAM,QAAQ,WAAyB;AAChD,SAAS,cAAc;AAMvB,IAAI,aAAa,IAAI,aACnB,OAAO,IAAI,YAAY,GAAG,QAAQ,CAAC;AAS9B,SAAS,mBAAmB,SAcf;AAClB,SAAO;AAAA,IACL,YAAY,QAAQ;AAAA,IACpB,UAAU,QAAQ;AAAA,IAClB,cAAc,QAAQ;AAAA,IACtB,UAAU,QAAQ;AAAA,IAClB,QAAQ,QAAQ;AAAA,IAChB,QAAQ,QAAQ;AAAA,IAChB,YAAY,QAAQ;AAAA,IACpB,aAAa,QAAQ;AAAA,IACrB,QAAQ,QAAQ;AAAA,IAChB,WAAW,QAAQ;AAAA,IACnB,cAAc,QAAQ;AAAA,IACtB,aAAa,QAAQ;AAAA,EACvB;AACF;AASO,SAAS,YACd,SACA,YACQ;AACR,QAAM,YAAY,aAAa,OAAO;AACtC,QAAM,UAAU,IAAI,YAAY,EAAE,OAAO,SAAS;AAClD,QAAM,eAAe,IAAI,WAAW,UAAU;AAC9C,QAAM,YAAY,KAAK,SAAS,YAAY;AAC5C,SAAO,WAAW,cAAc,SAAS,CAAC;AAC5C;AAUO,SAAS,cACd,SACA,WACA,WACS;AACT,MAAI;AACF,QAAI,CAAC,UAAU,WAAW,UAAU,GAAG;AACrC,aAAO;AAAA,IACT;AACA,UAAM,YAAY,aAAa,OAAO;AACtC,UAAM,UAAU,IAAI,YAAY,EAAE,OAAO,SAAS;AAClD,UAAM,WAAW,cAAc,UAAU,MAAM,WAAW,MAAM,CAAC;AACjE,UAAM,cAAc,IAAI,WAAW,SAAS;AAC5C,WAAO,OAAO,UAAU,SAAS,WAAW;AAAA,EAC9C,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAAS,cAAc,OAA2B;AAChD,SAAO,OAAO,KAAK,KAAK,EAAE,SAAS,QAAQ;AAC7C;AAEA,SAAS,cAAc,KAAyB;AAC9C,SAAO,IAAI,WAAW,OAAO,KAAK,KAAK,QAAQ,CAAC;AAClD;;;ACnGA,SAAS,gBAAAA,eAAc,OAAAC,MAAK,aAAa;AAOlC,SAAS,kBAGd;AACA,QAAM,kBAAkB,MAAM,iBAAiB;AAC/C,QAAM,iBAAiBD,cAAa,eAAe;AACnD,SAAO;AAAA,IACL,YAAYC,KAAI,WAAW,eAAe;AAAA,IAC1C,WAAWA,KAAI,WAAW,cAAc;AAAA,EAC1C;AACF;AAQO,SAAS,wBAAwB,eAA+B;AACrE,QAAM,eAAeA,KAAI,WAAW,aAAa;AACjD,QAAM,cAAcD,cAAa,YAAY;AAC7C,SAAOC,KAAI,WAAW,WAAW;AACnC;","names":["getPublicKey","etc"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@agent-receipts/crypto",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ed25519 signing and verification for the Action Receipt Protocol",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"author": "Amin <amin@webaes.co> (https://webaes.co)",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/webaesbyamin/agent-receipts",
|
|
10
|
+
"directory": "packages/crypto"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/webaesbyamin/agent-receipts#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/webaesbyamin/agent-receipts/issues"
|
|
15
|
+
},
|
|
16
|
+
"keywords": [
|
|
17
|
+
"agent-receipts",
|
|
18
|
+
"ed25519",
|
|
19
|
+
"signing",
|
|
20
|
+
"verification",
|
|
21
|
+
"cryptography",
|
|
22
|
+
"ai-agents",
|
|
23
|
+
"mcp"
|
|
24
|
+
],
|
|
25
|
+
"engines": {
|
|
26
|
+
"node": ">=20.0.0"
|
|
27
|
+
},
|
|
28
|
+
"main": "./dist/index.js",
|
|
29
|
+
"module": "./dist/index.mjs",
|
|
30
|
+
"types": "./dist/index.d.ts",
|
|
31
|
+
"exports": {
|
|
32
|
+
".": {
|
|
33
|
+
"types": "./dist/index.d.ts",
|
|
34
|
+
"import": "./dist/index.mjs",
|
|
35
|
+
"require": "./dist/index.js"
|
|
36
|
+
}
|
|
37
|
+
},
|
|
38
|
+
"bin": {
|
|
39
|
+
"generate-keys": "./bin/generate-keys.ts"
|
|
40
|
+
},
|
|
41
|
+
"files": [
|
|
42
|
+
"dist",
|
|
43
|
+
"bin"
|
|
44
|
+
],
|
|
45
|
+
"dependencies": {
|
|
46
|
+
"@noble/ed25519": "^2.1.0",
|
|
47
|
+
"@noble/hashes": "^1.7.0",
|
|
48
|
+
"@agent-receipts/schema": "0.1.0"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@types/node": "^20.14.0",
|
|
52
|
+
"tsup": "^8.2.0",
|
|
53
|
+
"typescript": "^5.4.0",
|
|
54
|
+
"vitest": "^2.1.0"
|
|
55
|
+
},
|
|
56
|
+
"scripts": {
|
|
57
|
+
"build": "tsup",
|
|
58
|
+
"dev": "tsup --watch",
|
|
59
|
+
"test": "vitest run",
|
|
60
|
+
"typecheck": "tsc --noEmit"
|
|
61
|
+
}
|
|
62
|
+
}
|