tersign 0.6.2 → 0.6.3
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/README.md +1 -1
- package/dist/intercept-bin.js +5 -3
- package/dist/keystore.d.ts +14 -2
- package/dist/keystore.js +59 -12
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +20 -13
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -178,7 +178,7 @@ Full URLs, readable without auth. If you are an agent, start here.
|
|
|
178
178
|
| Envelope API | `GET https://tersign.ai/v1/receipts/{digest}/envelope?venue={internet-court\|kleros\|uma\|generic}` |
|
|
179
179
|
| Ledger stats | `GET https://tersign.ai/v1/stats` |
|
|
180
180
|
| Ledger signer | `GET https://tersign.ai/v1/ledger` |
|
|
181
|
-
| Bundle verifier, out-of-band | https://tersign.ai/verify/v1/
|
|
181
|
+
| Bundle verifier, out-of-band | Fetch the checker from the address the archive's `VERIFY.md` section 0 names: the content address of the release the archive was built with, `https://tersign.ai/verify/sha256/<sha256 of its SHA256SUMS>/` (archives whose VERIFY.md names https://tersign.ai/verify/v1/ use that). Each release serves `verify_bundle.py` · `keccak.py` · `secp256k1.py` · `SHA256SUMS`, and its files never change; https://tersign.ai/verify/releases.json lists every release. A bundle ships its own checker; for evidence from an interested party fetch that release and diff the two. |
|
|
182
182
|
| llms.txt | https://raw.githubusercontent.com/tersignhq/tersign-js/main/llms.txt |
|
|
183
183
|
| Conformance vectors (RFC 8785 + keccak256, two-sided) | https://github.com/tersignhq/evidence-record-conformance |
|
|
184
184
|
| Sample compliance-fields record + digests | https://github.com/tersignhq/tersign-js/blob/main/test/fixtures/compliance-record.json |
|
package/dist/intercept-bin.js
CHANGED
|
@@ -25,7 +25,7 @@ import { DEFAULT_EVENTS, McpCapture } from './intercept/capture.js';
|
|
|
25
25
|
import { parseInterceptFlags } from './intercept/flags.js';
|
|
26
26
|
import { startIntercept } from './intercept/proxy.js';
|
|
27
27
|
import { EvidenceSink } from './intercept/sink.js';
|
|
28
|
-
import { resolveSignerKey } from './keystore.js';
|
|
28
|
+
import { ledgerApiKeyUnlessPlaceholder, resolveSignerKey } from './keystore.js';
|
|
29
29
|
const USAGE = 'usage: tersign intercept [--events tools/call,prompts/get] [--agent-id id] [--ledger url] -- <mcp server command> [args…]\n' +
|
|
30
30
|
'hosted ledger mode (TERSIGN_LEDGER_API_KEY + TERSIGN_LEDGER_SELLER_ID) requires the signer key\n' +
|
|
31
31
|
'registered for that sellerId — set TERSIGN_SELLER_KEY; rejected records fall back to ~/.tersign.\n' +
|
|
@@ -53,10 +53,12 @@ for (const e of (flags.events ?? '').split(',')) {
|
|
|
53
53
|
const envAgentId = process.env.TERSIGN_AGENT_ID;
|
|
54
54
|
const agentId = flags.agentId ?? (envAgentId !== undefined && envAgentId !== '' ? envAgentId : 'mcp-intercept');
|
|
55
55
|
const ledgerUrl = flags.ledger ?? process.env.TERSIGN_LEDGER_URL ?? 'https://tersign.ai';
|
|
56
|
-
|
|
56
|
+
// An MCP client launches this process, so an unsubstituted placeholder such as
|
|
57
|
+
// `${TERSIGN_LEDGER_API_KEY}` counts as unset here, as it does for the MCP server.
|
|
58
|
+
const apiKey = ledgerApiKeyUnlessPlaceholder(process.env.TERSIGN_LEDGER_API_KEY);
|
|
57
59
|
const sellerId = process.env.TERSIGN_LEDGER_SELLER_ID;
|
|
58
60
|
try {
|
|
59
|
-
const { key, source } = resolveSignerKey({ create: true });
|
|
61
|
+
const { key, source } = resolveSignerKey({ create: true, placeholderAsUnset: true });
|
|
60
62
|
const account = privateKeyToAccount(key);
|
|
61
63
|
console.error(`signing key: ${account.address} (${source})`);
|
|
62
64
|
const sink = new EvidenceSink({
|
package/dist/keystore.d.ts
CHANGED
|
@@ -4,7 +4,9 @@
|
|
|
4
4
|
* deployer by construction.
|
|
5
5
|
*
|
|
6
6
|
* Priority (first hit wins):
|
|
7
|
-
* 1. TERSIGN_SELLER_KEY env — explicit override; headless/CI/agent contract.
|
|
7
|
+
* 1. TERSIGN_SELLER_KEY env — explicit override; headless/CI/agent contract. Empty counts as
|
|
8
|
+
* unset; so does an unsubstituted placeholder such as `${TERSIGN_SELLER_KEY}`, but only for a
|
|
9
|
+
* caller that passes `placeholderAsUnset` (a process an MCP client launches).
|
|
8
10
|
* 2. macOS keychain, service `tersign-signer` — the at-rest default on darwin.
|
|
9
11
|
* 3. keyfile ~/.tersign/signer.key (0600) — portable fallback; created with a warning
|
|
10
12
|
* recommending the env/keychain paths.
|
|
@@ -17,7 +19,17 @@ export interface ResolvedSignerKey {
|
|
|
17
19
|
}
|
|
18
20
|
/** Resolve the deployer signing key. With `create: true`, a missing key is generated and
|
|
19
21
|
* persisted (keychain on darwin, else a 0600 keyfile); without it, resolution failure throws
|
|
20
|
-
* with the wiring instructions.
|
|
22
|
+
* with the wiring instructions. `env` is where TERSIGN_SELLER_KEY is read (default
|
|
23
|
+
* process.env). With `placeholderAsUnset: true` (passed only where an MCP client launches the
|
|
24
|
+
* process: the MCP server and `tersign intercept`), an unsubstituted placeholder there is treated
|
|
25
|
+
* as unset, and one stderr line says which key was used instead (stderr, never stdout: stdout is
|
|
26
|
+
* the MCP stdio channel). Without it, a placeholder gets the clear error like any malformed key. */
|
|
21
27
|
export declare function resolveSignerKey(opts?: {
|
|
22
28
|
create?: boolean;
|
|
29
|
+
env?: Record<string, string | undefined>;
|
|
30
|
+
placeholderAsUnset?: boolean;
|
|
23
31
|
}): ResolvedSignerKey;
|
|
32
|
+
/** TERSIGN_LEDGER_API_KEY as given, except that an unsubstituted placeholder is treated as unset,
|
|
33
|
+
* with one stderr line, so it is never sent as a credential. Only for processes an MCP client
|
|
34
|
+
* launches (the MCP server and `tersign intercept`); `""` and every other value pass through. */
|
|
35
|
+
export declare function ledgerApiKeyUnlessPlaceholder(value: string | undefined): string | undefined;
|
package/dist/keystore.js
CHANGED
|
@@ -2,9 +2,25 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { chmodSync, existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
3
3
|
import { homedir, userInfo } from 'node:os';
|
|
4
4
|
import { join } from 'node:path';
|
|
5
|
-
import { generatePrivateKey } from 'viem/accounts';
|
|
5
|
+
import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts';
|
|
6
6
|
const KEY_PATTERN = /^0x[0-9a-fA-F]{64}$/;
|
|
7
7
|
const KEYCHAIN_SERVICE = 'tersign-signer';
|
|
8
|
+
// A client config can carry `"TERSIGN_SELLER_KEY": "${TERSIGN_SELLER_KEY}"` for every secret a
|
|
9
|
+
// server declares. When that variable is not set where the client runs, a client can pass the
|
|
10
|
+
// text through unexpanded, so the server receives the placeholder itself.
|
|
11
|
+
// Only a WHOLE value of one of these shapes counts: `${NAME}`, `${NAME:-default}`,
|
|
12
|
+
// `${NAME-default}` or `$NAME`.
|
|
13
|
+
const PLACEHOLDER_PATTERN = /^\$\{[A-Za-z_][A-Za-z0-9_]*(:?-[^}]*)?\}$|^\$[A-Za-z_][A-Za-z0-9_]*$/;
|
|
14
|
+
/** True when an environment value is an unexpanded variable reference rather than a value. */
|
|
15
|
+
function isUnexpandedPlaceholder(value) {
|
|
16
|
+
return PLACEHOLDER_PATTERN.test(value);
|
|
17
|
+
}
|
|
18
|
+
/** The placeholder as it may be printed: a `:-default` / `-default` part is elided, since a
|
|
19
|
+
* default can itself be a secret. */
|
|
20
|
+
function shownPlaceholder(value) {
|
|
21
|
+
const withDefault = /^\$\{([A-Za-z_][A-Za-z0-9_]*)(:?-)/.exec(value);
|
|
22
|
+
return withDefault ? `\${${withDefault[1]}${withDefault[2]}…}` : value;
|
|
23
|
+
}
|
|
8
24
|
function keyfilePath() {
|
|
9
25
|
return join(homedir(), '.tersign', 'signer.key');
|
|
10
26
|
}
|
|
@@ -48,35 +64,66 @@ function writeKeychain(key) {
|
|
|
48
64
|
}
|
|
49
65
|
/** Resolve the deployer signing key. With `create: true`, a missing key is generated and
|
|
50
66
|
* persisted (keychain on darwin, else a 0600 keyfile); without it, resolution failure throws
|
|
51
|
-
* with the wiring instructions.
|
|
67
|
+
* with the wiring instructions. `env` is where TERSIGN_SELLER_KEY is read (default
|
|
68
|
+
* process.env). With `placeholderAsUnset: true` (passed only where an MCP client launches the
|
|
69
|
+
* process: the MCP server and `tersign intercept`), an unsubstituted placeholder there is treated
|
|
70
|
+
* as unset, and one stderr line says which key was used instead (stderr, never stdout: stdout is
|
|
71
|
+
* the MCP stdio channel). Without it, a placeholder gets the clear error like any malformed key. */
|
|
52
72
|
export function resolveSignerKey(opts = {}) {
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
73
|
+
const value = (opts.env ?? process.env).TERSIGN_SELLER_KEY;
|
|
74
|
+
const placeholder = opts.placeholderAsUnset === true && value !== undefined && isUnexpandedPlaceholder(value) ? value : undefined;
|
|
75
|
+
if (value !== undefined && value !== '' && placeholder === undefined) {
|
|
76
|
+
if (!KEY_PATTERN.test(value))
|
|
56
77
|
throw new Error('TERSIGN_SELLER_KEY must be a 0x-prefixed 32-byte hex key');
|
|
57
|
-
return { key:
|
|
78
|
+
return { key: value, source: 'env' };
|
|
79
|
+
}
|
|
80
|
+
const resolved = resolveStoredOrNew(opts.create === true, placeholder);
|
|
81
|
+
if (placeholder !== undefined) {
|
|
82
|
+
const where = resolved.generated
|
|
83
|
+
? `a newly generated key (kept in the ${resolved.source === 'keychain' ? 'OS keychain' : `keyfile ${keyfilePath()}`})`
|
|
84
|
+
: resolved.source === 'keychain'
|
|
85
|
+
? `the key from the OS keychain (service ${KEYCHAIN_SERVICE})`
|
|
86
|
+
: `the key from the keyfile ${keyfilePath()}`;
|
|
87
|
+
console.error(`tersign: TERSIGN_SELLER_KEY held the literal, unsubstituted text ${shownPlaceholder(placeholder)} and was ` +
|
|
88
|
+
`treated as unset; signing with ${where}, address ${privateKeyToAccount(resolved.key).address}.`);
|
|
58
89
|
}
|
|
90
|
+
return { key: resolved.key, source: resolved.source };
|
|
91
|
+
}
|
|
92
|
+
/** TERSIGN_LEDGER_API_KEY as given, except that an unsubstituted placeholder is treated as unset,
|
|
93
|
+
* with one stderr line, so it is never sent as a credential. Only for processes an MCP client
|
|
94
|
+
* launches (the MCP server and `tersign intercept`); `""` and every other value pass through. */
|
|
95
|
+
export function ledgerApiKeyUnlessPlaceholder(value) {
|
|
96
|
+
if (value === undefined || !isUnexpandedPlaceholder(value))
|
|
97
|
+
return value;
|
|
98
|
+
console.error(`tersign: TERSIGN_LEDGER_API_KEY held the literal, unsubstituted text ${shownPlaceholder(value)} and was ` +
|
|
99
|
+
'treated as unset; no API key is sent to the ledger.');
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
function resolveStoredOrNew(create, placeholder) {
|
|
59
103
|
const fromKeychain = readKeychain();
|
|
60
104
|
if (fromKeychain)
|
|
61
|
-
return { key: fromKeychain, source: 'keychain' };
|
|
105
|
+
return { key: fromKeychain, source: 'keychain', generated: false };
|
|
62
106
|
const file = keyfilePath();
|
|
63
107
|
if (existsSync(file)) {
|
|
64
108
|
const raw = readFileSync(file, 'utf8').trim();
|
|
65
109
|
if (!KEY_PATTERN.test(raw))
|
|
66
110
|
throw new Error(`${file} does not contain a 0x-prefixed 32-byte hex key`);
|
|
67
|
-
return { key: raw, source: 'keyfile' };
|
|
111
|
+
return { key: raw, source: 'keyfile', generated: false };
|
|
68
112
|
}
|
|
69
|
-
if (!
|
|
70
|
-
throw new Error(
|
|
113
|
+
if (!create) {
|
|
114
|
+
throw new Error((placeholder !== undefined
|
|
115
|
+
? `TERSIGN_SELLER_KEY held the unexpanded placeholder ${shownPlaceholder(placeholder)} and was treated as unset; `
|
|
116
|
+
: '') +
|
|
117
|
+
'no signing key found — set TERSIGN_SELLER_KEY, store one in the macOS keychain ' +
|
|
71
118
|
`(security add-generic-password -s ${KEYCHAIN_SERVICE} -a $USER -w 0x…), or rerun with key creation enabled`);
|
|
72
119
|
}
|
|
73
120
|
const key = generatePrivateKey();
|
|
74
121
|
if (writeKeychain(key))
|
|
75
|
-
return { key, source: 'keychain' };
|
|
122
|
+
return { key, source: 'keychain', generated: true };
|
|
76
123
|
mkdirSync(join(homedir(), '.tersign'), { recursive: true, mode: 0o700 });
|
|
77
124
|
writeFileSync(file, `${key}\n`, { mode: 0o600 });
|
|
78
125
|
chmodSync(file, 0o600);
|
|
79
126
|
console.error(`tersign: generated a new signing key at ${file} (0600). This key IS your evidence identity — ` +
|
|
80
127
|
'back it up, and prefer TERSIGN_SELLER_KEY or the OS keychain on shared machines.');
|
|
81
|
-
return { key, source: 'keyfile' };
|
|
128
|
+
return { key, source: 'keyfile', generated: true };
|
|
82
129
|
}
|
package/dist/mcp/server.d.ts
CHANGED
|
@@ -8,6 +8,6 @@ export declare function envDeps(env?: Record<string, string | undefined>): McpDe
|
|
|
8
8
|
* every client; mcp.test.ts pins it against package.json so a release bump can't drift it. */
|
|
9
9
|
export declare const MCP_SERVER_IDENTITY: {
|
|
10
10
|
readonly name: "tersign";
|
|
11
|
-
readonly version: "0.6.
|
|
11
|
+
readonly version: "0.6.3";
|
|
12
12
|
};
|
|
13
13
|
export declare function buildServer(deps: McpDeps): McpServer;
|
package/dist/mcp/server.js
CHANGED
|
@@ -3,7 +3,7 @@ import { z } from 'zod';
|
|
|
3
3
|
import { privateKeyToAccount } from 'viem/accounts';
|
|
4
4
|
import { Assure } from '../assure.js';
|
|
5
5
|
import { LedgerClient } from '../ledgerClient.js';
|
|
6
|
-
import { resolveSignerKey } from '../keystore.js';
|
|
6
|
+
import { ledgerApiKeyUnlessPlaceholder, resolveSignerKey } from '../keystore.js';
|
|
7
7
|
import { adjudicateDisputeTool, getDisputeTool, issueReceiptTool, openDisputeTool, recordDisclosureTool, recordRefundTool, submitEvidenceTool, verifyReceiptTool, verifyRecordTool, } from './tools.js';
|
|
8
8
|
/** MCP packaging: exposes assure as tools any MCP-speaking agent can call, so an agent
|
|
9
9
|
* (or its framework) can issue, verify, and chain receipts without importing the SDK.
|
|
@@ -14,12 +14,19 @@ export function envDeps(env = process.env) {
|
|
|
14
14
|
// that first call self-provisions a signer-keyed account; before this the MCP entry point threw
|
|
15
15
|
// instead, so `npx tersign` died on first run for anyone who had not already exported a key —
|
|
16
16
|
// and no directory or sandbox could introspect the server at all.
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
|
|
22
|
-
|
|
17
|
+
// Key precedence as in 0.6.2: `env`'s TERSIGN_SELLER_KEY when non-empty, else process.env's
|
|
18
|
+
// (`||`: an EMPTY value means unset — a client may fill a blank optional secret with ""; fixed
|
|
19
|
+
// 2026-09-27). The value is then read by the keystore itself, never handed raw to
|
|
20
|
+
// privateKeyToAccount, so a malformed value gets the keystore's clear error instead of a crypto
|
|
21
|
+
// library's. An MCP client launches this process, so an unsubstituted placeholder such as
|
|
22
|
+
// `${TERSIGN_SELLER_KEY}` also counts as unset (until 0.6.3 that text went straight to
|
|
23
|
+
// privateKeyToAccount and the server died at startup). test/listing.test.ts starts the registry
|
|
24
|
+
// command with the key set to "" and to the placeholder.
|
|
25
|
+
const sellerKey = env.TERSIGN_SELLER_KEY || process.env.TERSIGN_SELLER_KEY;
|
|
26
|
+
const account = privateKeyToAccount(resolveSignerKey({ create: true, env: { TERSIGN_SELLER_KEY: sellerKey }, placeholderAsUnset: true }).key);
|
|
27
|
+
// The listing marks the ledger API key secret too, so the same placeholder can reach it: treat
|
|
28
|
+
// it as unset rather than send it as a credential. "" keeps its old meaning.
|
|
29
|
+
const apiKey = ledgerApiKeyUnlessPlaceholder(env.TERSIGN_LEDGER_API_KEY);
|
|
23
30
|
const assure = new Assure({
|
|
24
31
|
signer: account,
|
|
25
32
|
issuer: {
|
|
@@ -27,12 +34,12 @@ export function envDeps(env = process.env) {
|
|
|
27
34
|
jurisdiction: env.TERSIGN_ISSUER_JURISDICTION ?? 'unknown',
|
|
28
35
|
...(env.TERSIGN_ISSUER_TAX_ID !== undefined ? { taxId: env.TERSIGN_ISSUER_TAX_ID } : {}),
|
|
29
36
|
},
|
|
30
|
-
...(env.TERSIGN_LEDGER_URL &&
|
|
31
|
-
? { ledger: { url: env.TERSIGN_LEDGER_URL, apiKey
|
|
37
|
+
...(env.TERSIGN_LEDGER_URL && apiKey && env.TERSIGN_LEDGER_SELLER_ID
|
|
38
|
+
? { ledger: { url: env.TERSIGN_LEDGER_URL, apiKey, sellerId: env.TERSIGN_LEDGER_SELLER_ID } }
|
|
32
39
|
: {}),
|
|
33
40
|
});
|
|
34
|
-
const ledger = env.TERSIGN_LEDGER_URL &&
|
|
35
|
-
? new LedgerClient({ url: env.TERSIGN_LEDGER_URL, apiKey
|
|
41
|
+
const ledger = env.TERSIGN_LEDGER_URL && apiKey && env.TERSIGN_LEDGER_SELLER_ID
|
|
42
|
+
? new LedgerClient({ url: env.TERSIGN_LEDGER_URL, apiKey, sellerId: env.TERSIGN_LEDGER_SELLER_ID })
|
|
36
43
|
: undefined;
|
|
37
44
|
return {
|
|
38
45
|
assure,
|
|
@@ -42,7 +49,7 @@ export function envDeps(env = process.env) {
|
|
|
42
49
|
? {
|
|
43
50
|
ledgerHttp: {
|
|
44
51
|
url: env.TERSIGN_LEDGER_URL,
|
|
45
|
-
...(
|
|
52
|
+
...(apiKey !== undefined ? { apiKey } : {}),
|
|
46
53
|
},
|
|
47
54
|
}
|
|
48
55
|
: {}),
|
|
@@ -53,7 +60,7 @@ function json(value) {
|
|
|
53
60
|
}
|
|
54
61
|
/** MUST match package.json name/version — the MCP handshake self-reports this identity to
|
|
55
62
|
* every client; mcp.test.ts pins it against package.json so a release bump can't drift it. */
|
|
56
|
-
export const MCP_SERVER_IDENTITY = { name: 'tersign', version: '0.6.
|
|
63
|
+
export const MCP_SERVER_IDENTITY = { name: 'tersign', version: '0.6.3' };
|
|
57
64
|
export function buildServer(deps) {
|
|
58
65
|
const server = new McpServer(MCP_SERVER_IDENTITY);
|
|
59
66
|
server.registerTool('issue_receipt', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tersign",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Tersign \u2014 the evidence layer for the agent economy. Counter-signed receipts, agent action records, idempotency enforcement, refunds, disputes, and jury-ready evidence envelopes for x402/agent-commerce sellers.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"type": "module",
|