gora8-signer 0.1.2 → 0.1.4
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 +23 -5
- package/dist/audit-log.d.ts +54 -1
- package/dist/audit-log.js +112 -15
- package/dist/cli.js +112 -15
- package/dist/index.d.ts +10 -5
- package/dist/index.js +6 -3
- package/dist/ipc-client.d.ts +19 -4
- package/dist/ipc-client.js +13 -3
- package/dist/ipc-server.js +6 -0
- package/dist/keystore.d.ts +50 -32
- package/dist/keystore.js +92 -62
- package/dist/kms-signing-backend.d.ts +31 -0
- package/dist/kms-signing-backend.js +182 -0
- package/dist/policy.d.ts +1 -1
- package/dist/rate-limit-backend.d.ts +76 -0
- package/dist/rate-limit-backend.js +143 -0
- package/dist/signer.d.ts +134 -45
- package/dist/signer.js +271 -105
- package/dist/signing-backend.d.ts +42 -0
- package/dist/signing-backend.js +58 -0
- package/package.json +18 -6
package/README.md
CHANGED
|
@@ -33,13 +33,15 @@ npm install gora8-signer
|
|
|
33
33
|
## Quick start — standalone signer process
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
# Generate
|
|
37
|
-
# automatically instead when available —
|
|
36
|
+
# Generate both keys — EVM and Solana — together (encrypted-file store
|
|
37
|
+
# shown here; OS keychain is used automatically instead when available —
|
|
38
|
+
# see Storage below)
|
|
38
39
|
export GORA8_SIGNER_KEK=$(openssl rand -hex 32) # dev only — see Storage
|
|
39
|
-
npx gora8-signer init my-agent-id
|
|
40
|
+
npx --package=gora8-signer gora8-signer init my-agent-id
|
|
41
|
+
# {"evmAddress":"0x...","solanaAddress":"..."}
|
|
40
42
|
|
|
41
43
|
# Run the signer as its own process
|
|
42
|
-
npx gora8-signer start my-agent-id
|
|
44
|
+
npx --package=gora8-signer gora8-signer start my-agent-id
|
|
43
45
|
```
|
|
44
46
|
|
|
45
47
|
Your agent's own code then talks to it over the socket:
|
|
@@ -48,9 +50,16 @@ Your agent's own code then talks to it over the socket:
|
|
|
48
50
|
import { SignerIpcClient } from "gora8-signer";
|
|
49
51
|
|
|
50
52
|
const signer = new SignerIpcClient("~/.gora8/signer/my-agent-id/signer.sock");
|
|
51
|
-
const
|
|
53
|
+
const evmAddress = await signer.address();
|
|
54
|
+
const solanaAddress = await signer.solanaAddress();
|
|
52
55
|
```
|
|
53
56
|
|
|
57
|
+
Both keys are entirely independent secrets — different curves
|
|
58
|
+
(secp256k1 vs. Ed25519), different storage entries — generated,
|
|
59
|
+
stored, and used the same way, on the same storage backends. An
|
|
60
|
+
agent that only ever transacts on one chain still gets both; the
|
|
61
|
+
unused one simply never gets asked to sign anything.
|
|
62
|
+
|
|
54
63
|
## Storage
|
|
55
64
|
|
|
56
65
|
Defaults to the OS-native keychain (macOS Keychain, Windows Credential
|
|
@@ -63,6 +72,15 @@ a mounted secret, whatever your deployment already uses — never gora8's.
|
|
|
63
72
|
The env-var-based provider shown above is for local development only;
|
|
64
73
|
implement `KekProvider` against your real KMS for production.
|
|
65
74
|
|
|
75
|
+
Auto-detection isn't airtight: some minimal container images (e.g.
|
|
76
|
+
Alpine) ship the OS-keychain library's native binding without a working
|
|
77
|
+
Secret Service underneath it, so it *loads* successfully but can't
|
|
78
|
+
actually store or retrieve anything — confirmed live, not hypothesized.
|
|
79
|
+
If you know your deployment target has no real OS keychain, set
|
|
80
|
+
`GORA8_SIGNER_SECRET_BACKEND=file` (or `keychain` to force the other
|
|
81
|
+
way, erroring loudly instead of silently falling back if it isn't
|
|
82
|
+
actually usable) rather than relying on auto-detection to figure it out.
|
|
83
|
+
|
|
66
84
|
## Policy enforcement
|
|
67
85
|
|
|
68
86
|
Before signing anything, the signer fetches your agent's current signed
|
package/dist/audit-log.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Hex } from "viem";
|
|
2
2
|
import { type MerkleTree } from "./merkle-tree.js";
|
|
3
|
+
import { type DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
3
4
|
export interface AuditEntry {
|
|
4
5
|
timestamp: string;
|
|
5
6
|
operation: string;
|
|
@@ -9,11 +10,63 @@ export interface AuditEntry {
|
|
|
9
10
|
counterparty: string | null;
|
|
10
11
|
txHash?: string;
|
|
11
12
|
}
|
|
12
|
-
export
|
|
13
|
+
export interface AuditLogBackend {
|
|
14
|
+
/** Appends one entry. Must preserve append order for the same reason
|
|
15
|
+
* Merkle-leaf ordering has to be stable — a backend whose writes can
|
|
16
|
+
* reorder or interleave under concurrency (multiple serverless
|
|
17
|
+
* instances appending at once, say) needs its own ordering
|
|
18
|
+
* guarantee (a monotonic sort key, a single-writer queue in front of
|
|
19
|
+
* it, etc.) or computeCommitment()'s root stops meaning "the log as
|
|
20
|
+
* it actually happened." Local, single-process storage gets this for
|
|
21
|
+
* free; a distributed backend has to earn it explicitly. */
|
|
22
|
+
append(entry: AuditEntry): Promise<void>;
|
|
23
|
+
readAll(): Promise<AuditEntry[]>;
|
|
24
|
+
}
|
|
25
|
+
/** File-backed, append-only JSONL — the original (and still the
|
|
26
|
+
* default local/persistent-process) storage, extracted here as one
|
|
27
|
+
* implementation of AuditLogBackend rather than baked into AuditLog
|
|
28
|
+
* itself. */
|
|
29
|
+
export declare class LocalFileAuditLogBackend implements AuditLogBackend {
|
|
13
30
|
private readonly filePath;
|
|
14
31
|
constructor(filePath: string);
|
|
15
32
|
append(entry: AuditEntry): Promise<void>;
|
|
16
33
|
readAll(): Promise<AuditEntry[]>;
|
|
34
|
+
}
|
|
35
|
+
/** DynamoDB-backed — the distributed case LocalFileAuditLogBackend's
|
|
36
|
+
* doc comment flagged as not yet built: multiple concurrent
|
|
37
|
+
* processes appending without a shared filesystem to serialize
|
|
38
|
+
* writes through. Ordering (this file's whole reason for calling it
|
|
39
|
+
* out as a backend-specific concern, not something the AuditLog
|
|
40
|
+
* orchestration layer can paper over) comes from a DynamoDB atomic
|
|
41
|
+
* counter, not wall-clock timestamps — two concurrent appends always
|
|
42
|
+
* get two different, strictly increasing sequence numbers via a
|
|
43
|
+
* single conditionless `ADD` (DynamoDB serializes this itself; no
|
|
44
|
+
* compare-and-swap needed here, unlike the rate-limit backend, since
|
|
45
|
+
* an atomic increment is exactly what's needed and DynamoDB provides
|
|
46
|
+
* it natively). Entries are stored under a different partition
|
|
47
|
+
* (`<key>#entries`, sort key = that sequence number) than the counter
|
|
48
|
+
* itself (`<key>#seq`), so `readAll()`'s Query never has to filter
|
|
49
|
+
* the counter item out. Requires a table with a String partition key
|
|
50
|
+
* named `pk` and a Number sort key named `sortKey`. */
|
|
51
|
+
export declare class DynamoDbAuditLogBackend implements AuditLogBackend {
|
|
52
|
+
private readonly client;
|
|
53
|
+
private readonly tableName;
|
|
54
|
+
private readonly key;
|
|
55
|
+
constructor(client: DynamoDBDocumentClient, tableName: string, key: string);
|
|
56
|
+
private nextSeq;
|
|
57
|
+
append(entry: AuditEntry): Promise<void>;
|
|
58
|
+
readAll(): Promise<AuditEntry[]>;
|
|
59
|
+
}
|
|
60
|
+
/** The orchestration layer — hash-chain leaf computation, Merkle
|
|
61
|
+
* commitment, and spend-tracking, all built on top of whatever
|
|
62
|
+
* AuditLogBackend is injected. Public API unchanged from before this
|
|
63
|
+
* file was split (append/readAll/spentSince/computeCommitment) — only
|
|
64
|
+
* the constructor changed, from a file path to a backend. */
|
|
65
|
+
export declare class AuditLog {
|
|
66
|
+
private readonly backend;
|
|
67
|
+
constructor(backend: AuditLogBackend);
|
|
68
|
+
append(entry: AuditEntry): Promise<void>;
|
|
69
|
+
readAll(): Promise<AuditEntry[]>;
|
|
17
70
|
/** Sum of `amount` across allowed entries since `since` — the local,
|
|
18
71
|
* independent spend tracker policy.ts's SpendTracker interface
|
|
19
72
|
* expects (see that file's doc comment on why this doesn't rely on
|
package/dist/audit-log.js
CHANGED
|
@@ -2,21 +2,12 @@ import { appendFile, readFile, mkdir } from "node:fs/promises";
|
|
|
2
2
|
import { dirname } from "node:path";
|
|
3
3
|
import { keccak256, toHex } from "viem";
|
|
4
4
|
import { buildMerkleTree } from "./merkle-tree.js";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
allowed: entry.allowed,
|
|
12
|
-
reason: entry.reason ?? null,
|
|
13
|
-
amount: entry.amount,
|
|
14
|
-
counterparty: entry.counterparty,
|
|
15
|
-
txHash: entry.txHash ?? null,
|
|
16
|
-
});
|
|
17
|
-
return keccak256(toHex(canonical));
|
|
18
|
-
}
|
|
19
|
-
export class AuditLog {
|
|
5
|
+
import { QueryCommand, PutCommand, UpdateCommand } from "@aws-sdk/lib-dynamodb";
|
|
6
|
+
/** File-backed, append-only JSONL — the original (and still the
|
|
7
|
+
* default local/persistent-process) storage, extracted here as one
|
|
8
|
+
* implementation of AuditLogBackend rather than baked into AuditLog
|
|
9
|
+
* itself. */
|
|
10
|
+
export class LocalFileAuditLogBackend {
|
|
20
11
|
filePath;
|
|
21
12
|
constructor(filePath) {
|
|
22
13
|
this.filePath = filePath;
|
|
@@ -39,6 +30,112 @@ export class AuditLog {
|
|
|
39
30
|
throw err;
|
|
40
31
|
}
|
|
41
32
|
}
|
|
33
|
+
}
|
|
34
|
+
/** DynamoDB-backed — the distributed case LocalFileAuditLogBackend's
|
|
35
|
+
* doc comment flagged as not yet built: multiple concurrent
|
|
36
|
+
* processes appending without a shared filesystem to serialize
|
|
37
|
+
* writes through. Ordering (this file's whole reason for calling it
|
|
38
|
+
* out as a backend-specific concern, not something the AuditLog
|
|
39
|
+
* orchestration layer can paper over) comes from a DynamoDB atomic
|
|
40
|
+
* counter, not wall-clock timestamps — two concurrent appends always
|
|
41
|
+
* get two different, strictly increasing sequence numbers via a
|
|
42
|
+
* single conditionless `ADD` (DynamoDB serializes this itself; no
|
|
43
|
+
* compare-and-swap needed here, unlike the rate-limit backend, since
|
|
44
|
+
* an atomic increment is exactly what's needed and DynamoDB provides
|
|
45
|
+
* it natively). Entries are stored under a different partition
|
|
46
|
+
* (`<key>#entries`, sort key = that sequence number) than the counter
|
|
47
|
+
* itself (`<key>#seq`), so `readAll()`'s Query never has to filter
|
|
48
|
+
* the counter item out. Requires a table with a String partition key
|
|
49
|
+
* named `pk` and a Number sort key named `sortKey`. */
|
|
50
|
+
export class DynamoDbAuditLogBackend {
|
|
51
|
+
client;
|
|
52
|
+
tableName;
|
|
53
|
+
key;
|
|
54
|
+
constructor(client, tableName, key) {
|
|
55
|
+
this.client = client;
|
|
56
|
+
this.tableName = tableName;
|
|
57
|
+
this.key = key;
|
|
58
|
+
}
|
|
59
|
+
async nextSeq() {
|
|
60
|
+
const res = await this.client.send(new UpdateCommand({
|
|
61
|
+
TableName: this.tableName,
|
|
62
|
+
Key: { pk: `${this.key}#seq`, sortKey: 0 },
|
|
63
|
+
UpdateExpression: "ADD seq :one",
|
|
64
|
+
ExpressionAttributeValues: { ":one": 1 },
|
|
65
|
+
ReturnValues: "UPDATED_NEW",
|
|
66
|
+
}));
|
|
67
|
+
const seq = res.Attributes?.seq;
|
|
68
|
+
if (typeof seq !== "number")
|
|
69
|
+
throw new Error(`DynamoDbAuditLogBackend: counter update for "${this.key}" returned no numeric seq.`);
|
|
70
|
+
return seq;
|
|
71
|
+
}
|
|
72
|
+
async append(entry) {
|
|
73
|
+
const seq = await this.nextSeq();
|
|
74
|
+
// AuditEntry's optional fields (reason, txHash) arrive as
|
|
75
|
+
// `undefined`, not omitted, whenever they're unset — fine for
|
|
76
|
+
// LocalFileAuditLogBackend (JSON.stringify silently drops undefined
|
|
77
|
+
// keys), but DynamoDB's default marshaller rejects `undefined`
|
|
78
|
+
// outright rather than dropping it (found live-verifying this: the
|
|
79
|
+
// very first signRegisterIdentity through a real CLI run threw here,
|
|
80
|
+
// since a successful signature's `reason` is always undefined). The
|
|
81
|
+
// JSON round-trip restores exactly the same "absent, not null"
|
|
82
|
+
// behavior the file backend already has, rather than requiring every
|
|
83
|
+
// caller to remember to construct their DynamoDBDocumentClient with
|
|
84
|
+
// `marshallOptions: { removeUndefinedValues: true }`.
|
|
85
|
+
const cleanEntry = JSON.parse(JSON.stringify(entry));
|
|
86
|
+
await this.client.send(new PutCommand({
|
|
87
|
+
TableName: this.tableName,
|
|
88
|
+
Item: { pk: `${this.key}#entries`, sortKey: seq, entry: cleanEntry },
|
|
89
|
+
}));
|
|
90
|
+
}
|
|
91
|
+
async readAll() {
|
|
92
|
+
const entries = [];
|
|
93
|
+
let exclusiveStartKey;
|
|
94
|
+
do {
|
|
95
|
+
const res = await this.client.send(new QueryCommand({
|
|
96
|
+
TableName: this.tableName,
|
|
97
|
+
KeyConditionExpression: "pk = :pk",
|
|
98
|
+
ExpressionAttributeValues: { ":pk": `${this.key}#entries` },
|
|
99
|
+
ScanIndexForward: true, // ascending sortKey — append order, same as the file backend's line order
|
|
100
|
+
ExclusiveStartKey: exclusiveStartKey,
|
|
101
|
+
}));
|
|
102
|
+
for (const item of res.Items ?? [])
|
|
103
|
+
entries.push(item.entry);
|
|
104
|
+
exclusiveStartKey = res.LastEvaluatedKey;
|
|
105
|
+
} while (exclusiveStartKey);
|
|
106
|
+
return entries;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function leafFor(entry) {
|
|
110
|
+
// Canonical JSON key order so the same logical entry always hashes to
|
|
111
|
+
// the same leaf regardless of object construction order.
|
|
112
|
+
const canonical = JSON.stringify({
|
|
113
|
+
timestamp: entry.timestamp,
|
|
114
|
+
operation: entry.operation,
|
|
115
|
+
allowed: entry.allowed,
|
|
116
|
+
reason: entry.reason ?? null,
|
|
117
|
+
amount: entry.amount,
|
|
118
|
+
counterparty: entry.counterparty,
|
|
119
|
+
txHash: entry.txHash ?? null,
|
|
120
|
+
});
|
|
121
|
+
return keccak256(toHex(canonical));
|
|
122
|
+
}
|
|
123
|
+
/** The orchestration layer — hash-chain leaf computation, Merkle
|
|
124
|
+
* commitment, and spend-tracking, all built on top of whatever
|
|
125
|
+
* AuditLogBackend is injected. Public API unchanged from before this
|
|
126
|
+
* file was split (append/readAll/spentSince/computeCommitment) — only
|
|
127
|
+
* the constructor changed, from a file path to a backend. */
|
|
128
|
+
export class AuditLog {
|
|
129
|
+
backend;
|
|
130
|
+
constructor(backend) {
|
|
131
|
+
this.backend = backend;
|
|
132
|
+
}
|
|
133
|
+
append(entry) {
|
|
134
|
+
return this.backend.append(entry);
|
|
135
|
+
}
|
|
136
|
+
readAll() {
|
|
137
|
+
return this.backend.readAll();
|
|
138
|
+
}
|
|
42
139
|
/** Sum of `amount` across allowed entries since `since` — the local,
|
|
43
140
|
* independent spend tracker policy.ts's SpendTracker interface
|
|
44
141
|
* expects (see that file's doc comment on why this doesn't rely on
|
package/dist/cli.js
CHANGED
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import {
|
|
4
|
+
import { KMSClient } from "@aws-sdk/client-kms";
|
|
5
|
+
import { DynamoDBClient } from "@aws-sdk/client-dynamodb";
|
|
6
|
+
import { DynamoDBDocumentClient } from "@aws-sdk/lib-dynamodb";
|
|
7
|
+
import { defaultEvmSigningBackend, defaultSolanaSigningBackend } from "./signing-backend.js";
|
|
8
|
+
import { KmsSigningBackend, SolanaKmsSigningBackend } from "./kms-signing-backend.js";
|
|
9
|
+
import { EnvKekProvider } from "./keystore.js";
|
|
5
10
|
import { PolicyEngine } from "./policy.js";
|
|
6
|
-
import { AuditLog } from "./audit-log.js";
|
|
7
|
-
import {
|
|
11
|
+
import { AuditLog, LocalFileAuditLogBackend, DynamoDbAuditLogBackend } from "./audit-log.js";
|
|
12
|
+
import { LocalFileRateLimitBackend, DynamoDbRateLimitBackend } from "./rate-limit-backend.js";
|
|
13
|
+
import { Signer } from "./signer.js";
|
|
8
14
|
import { SignerIpcServer } from "./ipc-server.js";
|
|
9
15
|
// Minimal CLI so `gora8 deploy` (the Go CLI) can shell out to this
|
|
10
16
|
// package directly rather than needing its own reimplementation of key
|
|
@@ -16,6 +22,90 @@ import { SignerIpcServer } from "./ipc-server.js";
|
|
|
16
22
|
function dataDir(agentId) {
|
|
17
23
|
return join(homedir(), ".gora8", "signer", agentId);
|
|
18
24
|
}
|
|
25
|
+
// Per-chain backend selection, independent of each other (same
|
|
26
|
+
// reasoning as everywhere else in this codebase: EVM and Solana are two
|
|
27
|
+
// entirely separate secrets, so one chain can be KMS-backed while the
|
|
28
|
+
// other stays local without conflict). Presence of the KMS key id env
|
|
29
|
+
// var *is* the selection — no separate "backend kind" var to drift out
|
|
30
|
+
// of sync with it. `provisioning` is null for a KMS-backed chain: the
|
|
31
|
+
// key already exists in KMS, provisioned out of band through the KMS
|
|
32
|
+
// console/API (see LocalKeyProvisioning's doc comment in
|
|
33
|
+
// signing-backend.ts), so `init` has nothing to generate for it.
|
|
34
|
+
let sharedKmsClient;
|
|
35
|
+
function kmsClient() {
|
|
36
|
+
// process.env.AWS_REGION, if set, is picked up by the SDK's own
|
|
37
|
+
// default provider chain automatically — no gora8-specific region
|
|
38
|
+
// handling needed here.
|
|
39
|
+
if (!sharedKmsClient)
|
|
40
|
+
sharedKmsClient = new KMSClient({});
|
|
41
|
+
return sharedKmsClient;
|
|
42
|
+
}
|
|
43
|
+
let sharedDynamoDbClient;
|
|
44
|
+
function dynamoDbClient() {
|
|
45
|
+
if (!sharedDynamoDbClient)
|
|
46
|
+
sharedDynamoDbClient = DynamoDBDocumentClient.from(new DynamoDBClient({}));
|
|
47
|
+
return sharedDynamoDbClient;
|
|
48
|
+
}
|
|
49
|
+
// Same independent-per-concern selection as the signing backends above:
|
|
50
|
+
// presence of the table-name env var is the selection, no separate
|
|
51
|
+
// "backend kind" var. This is what actually closes the "persistent
|
|
52
|
+
// process assumed" gap a KMS-backed signing key alone doesn't close —
|
|
53
|
+
// KMS made the *key* stateless-deployment-safe; these make the rate
|
|
54
|
+
// limiter and audit log safe for the same topology (a Lambda/Cloud-Run
|
|
55
|
+
// invocation with no durable local disk, possibly multiple concurrent
|
|
56
|
+
// instances of the same agent).
|
|
57
|
+
function selectAuditLogBackend(agentId, dir) {
|
|
58
|
+
const tableName = process.env.GORA8_AUDIT_LOG_DYNAMODB_TABLE;
|
|
59
|
+
if (tableName) {
|
|
60
|
+
console.error(`Audit log backend: DynamoDB (table "${tableName}")`);
|
|
61
|
+
return new DynamoDbAuditLogBackend(dynamoDbClient(), tableName, agentId);
|
|
62
|
+
}
|
|
63
|
+
console.error("Audit log backend: local file");
|
|
64
|
+
return new LocalFileAuditLogBackend(join(dir, "audit.jsonl"));
|
|
65
|
+
}
|
|
66
|
+
function selectRateLimitBackend(agentId, dir) {
|
|
67
|
+
const tableName = process.env.GORA8_RATE_LIMIT_DYNAMODB_TABLE;
|
|
68
|
+
if (tableName) {
|
|
69
|
+
console.error(`Rate limit backend: DynamoDB (table "${tableName}")`);
|
|
70
|
+
return new DynamoDbRateLimitBackend(dynamoDbClient(), tableName, agentId);
|
|
71
|
+
}
|
|
72
|
+
console.error("Rate limit backend: local file");
|
|
73
|
+
return new LocalFileRateLimitBackend(join(dir, "rate-limit.json"));
|
|
74
|
+
}
|
|
75
|
+
async function selectEvmBackend(agentId, dir, kek) {
|
|
76
|
+
const keyId = process.env.GORA8_EVM_KMS_KEY_ID;
|
|
77
|
+
if (keyId) {
|
|
78
|
+
console.error(`EVM signing backend: AWS KMS (key "${keyId}")`);
|
|
79
|
+
return { backend: new KmsSigningBackend(kmsClient(), keyId), provisioning: null };
|
|
80
|
+
}
|
|
81
|
+
console.error("EVM signing backend: local (OS keychain / encrypted file)");
|
|
82
|
+
const local = await defaultEvmSigningBackend(agentId, join(dir, "key.enc.json"), kek);
|
|
83
|
+
return { backend: local, provisioning: local };
|
|
84
|
+
}
|
|
85
|
+
async function selectSolanaBackend(agentId, dir, kek) {
|
|
86
|
+
const keyId = process.env.GORA8_SOLANA_KMS_KEY_ID;
|
|
87
|
+
if (keyId) {
|
|
88
|
+
console.error(`Solana signing backend: AWS KMS (key "${keyId}")`);
|
|
89
|
+
return { backend: new SolanaKmsSigningBackend(kmsClient(), keyId), provisioning: null };
|
|
90
|
+
}
|
|
91
|
+
console.error("Solana signing backend: local (OS keychain / encrypted file)");
|
|
92
|
+
const local = await defaultSolanaSigningBackend(agentId, join(dir, "solana-key.enc.json"), kek);
|
|
93
|
+
return { backend: local, provisioning: local };
|
|
94
|
+
}
|
|
95
|
+
/** Shared by both chains' `init` handling: generate-and-store for a
|
|
96
|
+
* local backend (skipping if a key already exists), or just report the
|
|
97
|
+
* address for a KMS-backed one (`provisioning === null`) — there's
|
|
98
|
+
* nothing for `init` to generate against KMS. */
|
|
99
|
+
async function resolveOrGenerateAddress(label, agentId, backend, provisioning) {
|
|
100
|
+
if (!provisioning)
|
|
101
|
+
return backend.address();
|
|
102
|
+
const hasKey = await provisioning.hasKey();
|
|
103
|
+
if (hasKey) {
|
|
104
|
+
console.error(`A ${label} key already exists for agent "${agentId}". Not generating a new one.`);
|
|
105
|
+
return backend.address();
|
|
106
|
+
}
|
|
107
|
+
return provisioning.generateAndStore();
|
|
108
|
+
}
|
|
19
109
|
async function fetchMandate(agentId, apiBase) {
|
|
20
110
|
const res = await fetch(`${apiBase}/v1/agents/${agentId}/mandate`);
|
|
21
111
|
if (!res.ok)
|
|
@@ -30,18 +120,20 @@ async function main() {
|
|
|
30
120
|
process.exit(1);
|
|
31
121
|
}
|
|
32
122
|
const dir = dataDir(agentId);
|
|
33
|
-
const
|
|
34
|
-
const
|
|
123
|
+
const kek = new EnvKekProvider();
|
|
124
|
+
const { backend: evmSigningBackend, provisioning: evmProvisioning } = await selectEvmBackend(agentId, dir, kek);
|
|
125
|
+
const { backend: solanaSigningBackend, provisioning: solanaProvisioning } = await selectSolanaBackend(agentId, dir, kek);
|
|
126
|
+
const auditLog = new AuditLog(selectAuditLogBackend(agentId, dir));
|
|
35
127
|
switch (command) {
|
|
36
128
|
case "init": {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
const
|
|
44
|
-
console.log(
|
|
129
|
+
// Both chains handled together, same as `gora8 deploy` provisions
|
|
130
|
+
// both an EVM and a Solana wallet for every agent today — two
|
|
131
|
+
// entirely independent secrets (different curves, independently
|
|
132
|
+
// local-or-KMS per resolveOrGenerateAddress above), not one key
|
|
133
|
+
// used two ways.
|
|
134
|
+
const evmAddress = await resolveOrGenerateAddress("EVM", agentId, evmSigningBackend, evmProvisioning);
|
|
135
|
+
const solanaAddress = await resolveOrGenerateAddress("Solana", agentId, solanaSigningBackend, solanaProvisioning);
|
|
136
|
+
console.log(JSON.stringify({ evmAddress, solanaAddress }));
|
|
45
137
|
break;
|
|
46
138
|
}
|
|
47
139
|
case "start": {
|
|
@@ -62,10 +154,15 @@ async function main() {
|
|
|
62
154
|
const mandateAgentId = process.env.GORA8_AGENT_ID || agentId;
|
|
63
155
|
const policyEngine = new PolicyEngine({ fetchMandate: () => fetchMandate(mandateAgentId, apiBase) }, auditLog);
|
|
64
156
|
const signer = new Signer({
|
|
65
|
-
|
|
157
|
+
evmSigningBackend,
|
|
158
|
+
solanaSigningBackend,
|
|
66
159
|
policyEngine,
|
|
67
160
|
auditLog,
|
|
68
|
-
|
|
161
|
+
rateLimit: {
|
|
162
|
+
backend: selectRateLimitBackend(agentId, dir),
|
|
163
|
+
windowMs: 60_000,
|
|
164
|
+
maxCalls: 30, // sanity backstop
|
|
165
|
+
},
|
|
69
166
|
});
|
|
70
167
|
const socketPath = process.env.GORA8_SIGNER_SOCKET ?? join(dir, "signer.sock");
|
|
71
168
|
const server = new SignerIpcServer(signer, socketPath);
|
package/dist/index.d.ts
CHANGED
|
@@ -1,10 +1,15 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type {
|
|
1
|
+
export { OsKeychainSecretStore, EncryptedFileSecretStore, EnvKekProvider, defaultSecretStore } from "./keystore.js";
|
|
2
|
+
export type { SecretStore, KekProvider } from "./keystore.js";
|
|
3
|
+
export { defaultEvmSigningBackend, defaultSolanaSigningBackend } from "./signing-backend.js";
|
|
4
|
+
export type { EvmSigningBackend, SolanaSigningBackend, LocalKeyProvisioning } from "./signing-backend.js";
|
|
5
|
+
export { KmsSigningBackend, defaultKmsSigningBackend, SolanaKmsSigningBackend, defaultSolanaKmsSigningBackend } from "./kms-signing-backend.js";
|
|
3
6
|
export { PolicyEngine } from "./policy.js";
|
|
4
7
|
export type { Mandate, SigningRequest, PolicyDecision, PolicyEngineOptions, SpendTracker } from "./policy.js";
|
|
5
|
-
export { AuditLog } from "./audit-log.js";
|
|
6
|
-
export type { AuditEntry } from "./audit-log.js";
|
|
7
|
-
export {
|
|
8
|
+
export { AuditLog, LocalFileAuditLogBackend, DynamoDbAuditLogBackend } from "./audit-log.js";
|
|
9
|
+
export type { AuditEntry, AuditLogBackend } from "./audit-log.js";
|
|
10
|
+
export { InMemoryRateLimitBackend, LocalFileRateLimitBackend, DynamoDbRateLimitBackend } from "./rate-limit-backend.js";
|
|
11
|
+
export type { RateLimitStateBackend } from "./rate-limit-backend.js";
|
|
12
|
+
export { Signer } from "./signer.js";
|
|
8
13
|
export type { SignerOptions } from "./signer.js";
|
|
9
14
|
export { SignerIpcServer } from "./ipc-server.js";
|
|
10
15
|
export { SignerIpcClient } from "./ipc-client.js";
|
package/dist/index.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { OsKeychainSecretStore, EncryptedFileSecretStore, EnvKekProvider, defaultSecretStore } from "./keystore.js";
|
|
2
|
+
export { defaultEvmSigningBackend, defaultSolanaSigningBackend } from "./signing-backend.js";
|
|
3
|
+
export { KmsSigningBackend, defaultKmsSigningBackend, SolanaKmsSigningBackend, defaultSolanaKmsSigningBackend } from "./kms-signing-backend.js";
|
|
2
4
|
export { PolicyEngine } from "./policy.js";
|
|
3
|
-
export { AuditLog } from "./audit-log.js";
|
|
4
|
-
export {
|
|
5
|
+
export { AuditLog, LocalFileAuditLogBackend, DynamoDbAuditLogBackend } from "./audit-log.js";
|
|
6
|
+
export { InMemoryRateLimitBackend, LocalFileRateLimitBackend, DynamoDbRateLimitBackend } from "./rate-limit-backend.js";
|
|
7
|
+
export { Signer } from "./signer.js";
|
|
5
8
|
export { SignerIpcServer } from "./ipc-server.js";
|
|
6
9
|
export { SignerIpcClient } from "./ipc-client.js";
|
|
7
10
|
export { buildMerkleTree, getMerkleProof, verifyMerkleProof } from "./merkle-tree.js";
|
package/dist/ipc-client.d.ts
CHANGED
|
@@ -24,8 +24,8 @@ export declare class SignerIpcClient {
|
|
|
24
24
|
to: `0x${string}`;
|
|
25
25
|
value: bigint;
|
|
26
26
|
data: `0x${string}`;
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
executeNonce: bigint;
|
|
28
|
+
chainId: number;
|
|
29
29
|
}): Promise<{
|
|
30
30
|
signature: `0x${string}`;
|
|
31
31
|
}>;
|
|
@@ -35,16 +35,31 @@ export declare class SignerIpcClient {
|
|
|
35
35
|
}): Promise<{
|
|
36
36
|
signature: `0x${string}`;
|
|
37
37
|
}>;
|
|
38
|
+
signAgreementCommit(params: {
|
|
39
|
+
termsHash: `0x${string}`;
|
|
40
|
+
counterparty: `0x${string}`;
|
|
41
|
+
}): Promise<{
|
|
42
|
+
signature: `0x${string}`;
|
|
43
|
+
}>;
|
|
38
44
|
signWithdrawal(params: {
|
|
39
|
-
to: `0x${string}`;
|
|
40
|
-
amount: number;
|
|
41
45
|
unsignedTx: {
|
|
42
46
|
to: `0x${string}`;
|
|
43
47
|
value: bigint;
|
|
44
48
|
data: `0x${string}`;
|
|
49
|
+
nonce: number;
|
|
50
|
+
gas?: bigint;
|
|
51
|
+
gasPrice?: bigint;
|
|
45
52
|
};
|
|
46
53
|
chainId: number;
|
|
47
54
|
}): Promise<{
|
|
48
55
|
signedTransaction: `0x${string}`;
|
|
49
56
|
}>;
|
|
57
|
+
solanaAddress(): Promise<string>;
|
|
58
|
+
signSolanaTransaction(params: {
|
|
59
|
+
serializedTransaction: string;
|
|
60
|
+
amount: number | null;
|
|
61
|
+
counterparty: string | null;
|
|
62
|
+
}): Promise<{
|
|
63
|
+
signature: string;
|
|
64
|
+
}>;
|
|
50
65
|
}
|
package/dist/ipc-client.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createConnection } from "node:net";
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
// Thin client for the agent's own application code — the only thing it
|
|
4
|
-
// imports to request a signature. It never has access to
|
|
5
|
-
// PolicyEngine, or
|
|
6
|
-
// named requests over the socket and get back a signature or
|
|
4
|
+
// imports to request a signature. It never has access to the signing
|
|
5
|
+
// backend, PolicyEngine, or any key material at all; it can only send
|
|
6
|
+
// one of the named requests over the socket and get back a signature or
|
|
7
|
+
// a refusal.
|
|
7
8
|
export class SignerIpcClient {
|
|
8
9
|
socketPath;
|
|
9
10
|
constructor(socketPath) {
|
|
@@ -47,7 +48,16 @@ export class SignerIpcClient {
|
|
|
47
48
|
signAgreementTerms(params) {
|
|
48
49
|
return this.call("signAgreementTerms", params);
|
|
49
50
|
}
|
|
51
|
+
signAgreementCommit(params) {
|
|
52
|
+
return this.call("signAgreementCommit", params);
|
|
53
|
+
}
|
|
50
54
|
signWithdrawal(params) {
|
|
51
55
|
return this.call("signWithdrawal", params);
|
|
52
56
|
}
|
|
57
|
+
solanaAddress() {
|
|
58
|
+
return this.call("solanaAddress");
|
|
59
|
+
}
|
|
60
|
+
signSolanaTransaction(params) {
|
|
61
|
+
return this.call("signSolanaTransaction", params);
|
|
62
|
+
}
|
|
53
63
|
}
|
package/dist/ipc-server.js
CHANGED
|
@@ -68,8 +68,14 @@ export class SignerIpcServer {
|
|
|
68
68
|
return this.signer.signMandateExecute(params);
|
|
69
69
|
case "signAgreementTerms":
|
|
70
70
|
return this.signer.signAgreementTerms(params);
|
|
71
|
+
case "signAgreementCommit":
|
|
72
|
+
return this.signer.signAgreementCommit(params);
|
|
71
73
|
case "signWithdrawal":
|
|
72
74
|
return this.signer.signWithdrawal(params);
|
|
75
|
+
case "solanaAddress":
|
|
76
|
+
return this.signer.solanaAddress();
|
|
77
|
+
case "signSolanaTransaction":
|
|
78
|
+
return this.signer.signSolanaTransaction(params);
|
|
73
79
|
default:
|
|
74
80
|
throw new Error(`Unknown method: ${method}`);
|
|
75
81
|
}
|