gora8-signer 0.1.2 → 0.1.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 +12 -3
- package/dist/cli.js +21 -10
- package/dist/index.d.ts +2 -2
- package/dist/index.js +1 -1
- package/dist/ipc-client.d.ts +11 -0
- package/dist/ipc-client.js +6 -0
- package/dist/ipc-server.js +4 -0
- package/dist/keystore.d.ts +40 -28
- package/dist/keystore.js +101 -52
- package/dist/policy.d.ts +1 -1
- package/dist/signer.d.ts +63 -3
- package/dist/signer.js +94 -11
- package/package.json +11 -6
package/README.md
CHANGED
|
@@ -33,10 +33,12 @@ 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
40
|
npx gora8-signer init my-agent-id
|
|
41
|
+
# {"evmAddress":"0x...","solanaAddress":"..."}
|
|
40
42
|
|
|
41
43
|
# Run the signer as its own process
|
|
42
44
|
npx gora8-signer start my-agent-id
|
|
@@ -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
|
package/dist/cli.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { join } from "node:path";
|
|
3
3
|
import { homedir } from "node:os";
|
|
4
|
-
import { defaultKeyStore, EnvKekProvider } from "./keystore.js";
|
|
4
|
+
import { defaultKeyStore, defaultSolanaKeyStore, EnvKekProvider } from "./keystore.js";
|
|
5
5
|
import { PolicyEngine } from "./policy.js";
|
|
6
6
|
import { AuditLog } from "./audit-log.js";
|
|
7
7
|
import { Signer, SlidingWindowRateLimiter } from "./signer.js";
|
|
@@ -30,18 +30,28 @@ async function main() {
|
|
|
30
30
|
process.exit(1);
|
|
31
31
|
}
|
|
32
32
|
const dir = dataDir(agentId);
|
|
33
|
-
const
|
|
33
|
+
const kek = new EnvKekProvider();
|
|
34
|
+
const keyStore = await defaultKeyStore(agentId, join(dir, "key.enc.json"), kek);
|
|
35
|
+
const solanaKeyStore = await defaultSolanaKeyStore(agentId, join(dir, "solana-key.enc.json"), kek);
|
|
34
36
|
const auditLog = new AuditLog(join(dir, "audit.jsonl"));
|
|
35
37
|
switch (command) {
|
|
36
38
|
case "init": {
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
39
|
+
// Both keys generated together, same as `gora8 deploy` provisions
|
|
40
|
+
// both an EVM and a Solana wallet for every agent today — two
|
|
41
|
+
// entirely independent secrets (different curves, different
|
|
42
|
+
// storage entries), not one key used two ways. Each is checked and
|
|
43
|
+
// reported separately so a partial prior init (e.g. EVM exists,
|
|
44
|
+
// Solana doesn't yet — the state every pre-Solana-self-custody
|
|
45
|
+
// agent is in) doesn't block generating just the missing one.
|
|
46
|
+
const hasEvm = await keyStore.hasKey();
|
|
47
|
+
const evmAddress = hasEvm ? await keyStore.address() : await keyStore.generateAndStore();
|
|
48
|
+
if (hasEvm)
|
|
49
|
+
console.error(`An EVM key already exists for agent "${agentId}". Not generating a new one.`);
|
|
50
|
+
const hasSolana = await solanaKeyStore.hasKey();
|
|
51
|
+
const solanaAddress = hasSolana ? await solanaKeyStore.address() : await solanaKeyStore.generateAndStore();
|
|
52
|
+
if (hasSolana)
|
|
53
|
+
console.error(`A Solana key already exists for agent "${agentId}". Not generating a new one.`);
|
|
54
|
+
console.log(JSON.stringify({ evmAddress, solanaAddress }));
|
|
45
55
|
break;
|
|
46
56
|
}
|
|
47
57
|
case "start": {
|
|
@@ -63,6 +73,7 @@ async function main() {
|
|
|
63
73
|
const policyEngine = new PolicyEngine({ fetchMandate: () => fetchMandate(mandateAgentId, apiBase) }, auditLog);
|
|
64
74
|
const signer = new Signer({
|
|
65
75
|
keyStore,
|
|
76
|
+
solanaKeyStore,
|
|
66
77
|
policyEngine,
|
|
67
78
|
auditLog,
|
|
68
79
|
rateLimiter: new SlidingWindowRateLimiter(30, 60_000), // 30 signatures/min sanity backstop
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export {
|
|
2
|
-
export type { KeyStore, KekProvider } from "./keystore.js";
|
|
1
|
+
export { OsKeychainSecretStore, EncryptedFileSecretStore, EnvKekProvider, defaultKeyStore, defaultSolanaKeyStore } from "./keystore.js";
|
|
2
|
+
export type { KeyStore, SolanaKeyStore, SecretStore, KekProvider } from "./keystore.js";
|
|
3
3
|
export { PolicyEngine } from "./policy.js";
|
|
4
4
|
export type { Mandate, SigningRequest, PolicyDecision, PolicyEngineOptions, SpendTracker } from "./policy.js";
|
|
5
5
|
export { AuditLog } from "./audit-log.js";
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { OsKeychainSecretStore, EncryptedFileSecretStore, EnvKekProvider, defaultKeyStore, defaultSolanaKeyStore } from "./keystore.js";
|
|
2
2
|
export { PolicyEngine } from "./policy.js";
|
|
3
3
|
export { AuditLog } from "./audit-log.js";
|
|
4
4
|
export { Signer, SlidingWindowRateLimiter } from "./signer.js";
|
package/dist/ipc-client.d.ts
CHANGED
|
@@ -42,9 +42,20 @@ export declare class SignerIpcClient {
|
|
|
42
42
|
to: `0x${string}`;
|
|
43
43
|
value: bigint;
|
|
44
44
|
data: `0x${string}`;
|
|
45
|
+
nonce: number;
|
|
46
|
+
gas?: bigint;
|
|
47
|
+
gasPrice?: bigint;
|
|
45
48
|
};
|
|
46
49
|
chainId: number;
|
|
47
50
|
}): Promise<{
|
|
48
51
|
signedTransaction: `0x${string}`;
|
|
49
52
|
}>;
|
|
53
|
+
solanaAddress(): Promise<string>;
|
|
54
|
+
signSolanaTransaction(params: {
|
|
55
|
+
serializedTransaction: string;
|
|
56
|
+
amount: number | null;
|
|
57
|
+
counterparty: string | null;
|
|
58
|
+
}): Promise<{
|
|
59
|
+
signature: string;
|
|
60
|
+
}>;
|
|
50
61
|
}
|
package/dist/ipc-client.js
CHANGED
|
@@ -50,4 +50,10 @@ export class SignerIpcClient {
|
|
|
50
50
|
signWithdrawal(params) {
|
|
51
51
|
return this.call("signWithdrawal", params);
|
|
52
52
|
}
|
|
53
|
+
solanaAddress() {
|
|
54
|
+
return this.call("solanaAddress");
|
|
55
|
+
}
|
|
56
|
+
signSolanaTransaction(params) {
|
|
57
|
+
return this.call("signSolanaTransaction", params);
|
|
58
|
+
}
|
|
53
59
|
}
|
package/dist/ipc-server.js
CHANGED
|
@@ -70,6 +70,10 @@ export class SignerIpcServer {
|
|
|
70
70
|
return this.signer.signAgreementTerms(params);
|
|
71
71
|
case "signWithdrawal":
|
|
72
72
|
return this.signer.signWithdrawal(params);
|
|
73
|
+
case "solanaAddress":
|
|
74
|
+
return this.signer.solanaAddress();
|
|
75
|
+
case "signSolanaTransaction":
|
|
76
|
+
return this.signer.signSolanaTransaction(params);
|
|
73
77
|
default:
|
|
74
78
|
throw new Error(`Unknown method: ${method}`);
|
|
75
79
|
}
|
package/dist/keystore.d.ts
CHANGED
|
@@ -1,33 +1,44 @@
|
|
|
1
1
|
import type { Address, Hex } from "viem";
|
|
2
|
+
export interface SecretStore {
|
|
3
|
+
hasSecret(): Promise<boolean>;
|
|
4
|
+
/** Persists `secret` — throws if one is already stored; callers must
|
|
5
|
+
* not use this to overwrite an existing secret. */
|
|
6
|
+
store(secret: string): Promise<void>;
|
|
7
|
+
/** The only way to read the secret. `fn`'s return value escapes this
|
|
8
|
+
* scope; the raw secret string itself never does. */
|
|
9
|
+
withSecret<T>(fn: (secret: string) => Promise<T> | T): Promise<T>;
|
|
10
|
+
}
|
|
2
11
|
export interface KeyStore {
|
|
3
|
-
/** True once a key has been generated and stored — callers check this
|
|
4
|
-
* before generateAndStore() to avoid silently overwriting an existing
|
|
5
|
-
* key (which would orphan whatever address was already registered
|
|
6
|
-
* on-chain). */
|
|
7
12
|
hasKey(): Promise<boolean>;
|
|
8
|
-
/** Generates a fresh key, persists it, and returns only the derived
|
|
9
|
-
* address — never the key itself. Throws if a key already exists;
|
|
10
|
-
* callers must not call this to "reset" a store. */
|
|
11
13
|
generateAndStore(): Promise<Address>;
|
|
12
|
-
/** The only way to use the key. `fn`'s return value escapes this
|
|
13
|
-
* scope; the raw private key string itself never does. */
|
|
14
14
|
withPrivateKey<T>(fn: (privateKey: Hex) => Promise<T> | T): Promise<T>;
|
|
15
15
|
address(): Promise<Address>;
|
|
16
16
|
}
|
|
17
|
-
export
|
|
17
|
+
export interface SolanaKeyStore {
|
|
18
|
+
hasKey(): Promise<boolean>;
|
|
19
|
+
generateAndStore(): Promise<string>;
|
|
20
|
+
/** `fn` receives the raw 64-byte Solana secret key, matching
|
|
21
|
+
* `@solana/web3.js`'s `Keypair.fromSecretKey()` input — same scoped-
|
|
22
|
+
* access discipline as EVM's `withPrivateKey`. */
|
|
23
|
+
withSecretKey<T>(fn: (secretKey: Uint8Array) => Promise<T> | T): Promise<T>;
|
|
24
|
+
address(): Promise<string>;
|
|
25
|
+
}
|
|
26
|
+
export declare class OsKeychainSecretStore implements SecretStore {
|
|
18
27
|
private readonly service;
|
|
19
28
|
private readonly account;
|
|
20
|
-
|
|
29
|
+
/** `account` should be unique per (agentId, key kind) pair — e.g.
|
|
30
|
+
* `"<agentId>"` for the EVM key, `"<agentId>:solana"` for Solana —
|
|
31
|
+
* so the two keys never collide in the same OS keychain. */
|
|
32
|
+
constructor(account: string, service?: string);
|
|
21
33
|
private entry;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
address(): Promise<Address>;
|
|
34
|
+
hasSecret(): Promise<boolean>;
|
|
35
|
+
store(secret: string): Promise<void>;
|
|
36
|
+
withSecret<T>(fn: (secret: string) => Promise<T> | T): Promise<T>;
|
|
26
37
|
}
|
|
27
38
|
export interface KekProvider {
|
|
28
|
-
/** Returns a 32-byte key-encryption key. Called once per
|
|
29
|
-
*
|
|
30
|
-
*
|
|
39
|
+
/** Returns a 32-byte key-encryption key. Called once per store()/
|
|
40
|
+
* withSecret() — implementations are expected to cache/memoize their
|
|
41
|
+
* own upstream fetch if it's expensive. */
|
|
31
42
|
getKek(): Promise<Buffer>;
|
|
32
43
|
}
|
|
33
44
|
/** Development/local-only KEK provider — reads a hex-encoded 32-byte key
|
|
@@ -36,25 +47,26 @@ export interface KekProvider {
|
|
|
36
47
|
* cloud account, and so self-hosted deployments have something to point
|
|
37
48
|
* at before wiring up their own KMS-backed provider (see this file's
|
|
38
49
|
* doc comment on KekProvider — implement one against AWS KMS/GCP KMS/
|
|
39
|
-
* age and pass it to
|
|
50
|
+
* age and pass it to EncryptedFileSecretStore instead, for anything real). */
|
|
40
51
|
export declare class EnvKekProvider implements KekProvider {
|
|
41
52
|
private readonly envVar;
|
|
42
53
|
constructor(envVar?: string);
|
|
43
54
|
getKek(): Promise<Buffer>;
|
|
44
55
|
}
|
|
45
|
-
export declare class
|
|
56
|
+
export declare class EncryptedFileSecretStore implements SecretStore {
|
|
46
57
|
private readonly filePath;
|
|
47
58
|
private readonly kek;
|
|
48
59
|
constructor(filePath: string, kek: KekProvider);
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
address(): Promise<Address>;
|
|
60
|
+
hasSecret(): Promise<boolean>;
|
|
61
|
+
store(secret: string): Promise<void>;
|
|
62
|
+
withSecret<T>(fn: (secret: string) => Promise<T> | T): Promise<T>;
|
|
53
63
|
private writeEncrypted;
|
|
54
64
|
private readEncrypted;
|
|
55
65
|
}
|
|
56
|
-
/** Picks OS keychain by default, falling back to the encrypted file
|
|
57
|
-
* store only when the OS keychain genuinely isn't available (headless
|
|
58
|
-
* container with no Secret Service, etc.) — never silently prefers the
|
|
59
|
-
* weaker option. */
|
|
60
66
|
export declare function defaultKeyStore(agentId: string, fallbackFilePath: string, kek: KekProvider): Promise<KeyStore>;
|
|
67
|
+
/** Same defaulting behavior as `defaultKeyStore`, for the Solana key —
|
|
68
|
+
* `fallbackFilePath` must be a different path than the EVM store's
|
|
69
|
+
* (e.g. `solana-key.enc.json` next to `key.enc.json`), and the OS
|
|
70
|
+
* keychain account is automatically suffixed so the two never collide
|
|
71
|
+
* under the same service/account pair. */
|
|
72
|
+
export declare function defaultSolanaKeyStore(agentId: string, fallbackFilePath: string, kek: KekProvider): Promise<SolanaKeyStore>;
|
package/dist/keystore.js
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
|
|
2
|
+
import { Keypair } from "@solana/web3.js";
|
|
2
3
|
import { randomBytes, createCipheriv, createDecipheriv, scryptSync } from "node:crypto";
|
|
3
4
|
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
4
5
|
import { dirname } from "node:path";
|
|
5
|
-
function
|
|
6
|
+
function deriveEvmAddress(privateKey) {
|
|
6
7
|
return privateKeyToAccount(privateKey).address;
|
|
7
8
|
}
|
|
8
9
|
// ── OS-native keychain store (default) ─────────────────────────────────
|
|
@@ -13,54 +14,46 @@ function deriveAddress(privateKey) {
|
|
|
13
14
|
// because it's the closest local equivalent to what a TEE-based vendor
|
|
14
15
|
// gives you: the OS, not this process's own memory or a plain file, is
|
|
15
16
|
// what actually guards the secret at rest.
|
|
16
|
-
export class
|
|
17
|
+
export class OsKeychainSecretStore {
|
|
17
18
|
service;
|
|
18
19
|
account;
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
/** `account` should be unique per (agentId, key kind) pair — e.g.
|
|
21
|
+
* `"<agentId>"` for the EVM key, `"<agentId>:solana"` for Solana —
|
|
22
|
+
* so the two keys never collide in the same OS keychain. */
|
|
23
|
+
constructor(account, service = "gora8-signer") {
|
|
24
|
+
this.service = service;
|
|
25
|
+
this.account = account;
|
|
22
26
|
}
|
|
23
27
|
async entry() {
|
|
24
28
|
const { Entry } = await import("@napi-rs/keyring");
|
|
25
29
|
return new Entry(this.service, this.account);
|
|
26
30
|
}
|
|
27
|
-
async
|
|
31
|
+
async hasSecret() {
|
|
28
32
|
try {
|
|
29
33
|
const entry = await this.entry();
|
|
30
34
|
// @napi-rs/keyring's getPassword() returns null for a missing entry
|
|
31
|
-
// rather than throwing — treating "didn't throw" as "
|
|
32
|
-
//
|
|
33
|
-
// backend that behaves this way: hasKey() would report true for an
|
|
34
|
-
// agent that has never had a key generated, and the subsequent
|
|
35
|
-
// withPrivateKey()/address() call would then try to derive an
|
|
36
|
-
// address from null and crash. A missing entry is only genuinely
|
|
37
|
-
// "no key" when the returned password is a non-empty string.
|
|
35
|
+
// rather than throwing — treating "didn't throw" as "secret exists"
|
|
36
|
+
// is a false positive on every backend that behaves this way.
|
|
38
37
|
return typeof entry.getPassword() === "string";
|
|
39
38
|
}
|
|
40
39
|
catch {
|
|
41
40
|
return false;
|
|
42
41
|
}
|
|
43
42
|
}
|
|
44
|
-
async
|
|
45
|
-
if (await this.
|
|
46
|
-
throw new Error(`A
|
|
47
|
-
"Refusing to overwrite — generating a new key would orphan the address already registered on-chain.");
|
|
43
|
+
async store(secret) {
|
|
44
|
+
if (await this.hasSecret()) {
|
|
45
|
+
throw new Error(`A secret already exists for "${this.account}" in the OS keychain. Refusing to overwrite.`);
|
|
48
46
|
}
|
|
49
|
-
const privateKey = generatePrivateKey();
|
|
50
47
|
const entry = await this.entry();
|
|
51
|
-
entry.setPassword(
|
|
52
|
-
return deriveAddress(privateKey);
|
|
48
|
+
entry.setPassword(secret);
|
|
53
49
|
}
|
|
54
|
-
async
|
|
50
|
+
async withSecret(fn) {
|
|
55
51
|
const entry = await this.entry();
|
|
56
|
-
const
|
|
57
|
-
if (typeof
|
|
58
|
-
throw new Error(`No
|
|
52
|
+
const secret = entry.getPassword();
|
|
53
|
+
if (typeof secret !== "string") {
|
|
54
|
+
throw new Error(`No secret found for "${this.account}" in the OS keychain — run \`gora8-signer init\` first.`);
|
|
59
55
|
}
|
|
60
|
-
return fn(
|
|
61
|
-
}
|
|
62
|
-
async address() {
|
|
63
|
-
return this.withPrivateKey((pk) => deriveAddress(pk));
|
|
56
|
+
return fn(secret);
|
|
64
57
|
}
|
|
65
58
|
}
|
|
66
59
|
/** Development/local-only KEK provider — reads a hex-encoded 32-byte key
|
|
@@ -69,7 +62,7 @@ export class OsKeychainStore {
|
|
|
69
62
|
* cloud account, and so self-hosted deployments have something to point
|
|
70
63
|
* at before wiring up their own KMS-backed provider (see this file's
|
|
71
64
|
* doc comment on KekProvider — implement one against AWS KMS/GCP KMS/
|
|
72
|
-
* age and pass it to
|
|
65
|
+
* age and pass it to EncryptedFileSecretStore instead, for anything real). */
|
|
73
66
|
export class EnvKekProvider {
|
|
74
67
|
envVar;
|
|
75
68
|
constructor(envVar = "GORA8_SIGNER_KEK") {
|
|
@@ -78,7 +71,7 @@ export class EnvKekProvider {
|
|
|
78
71
|
async getKek() {
|
|
79
72
|
const hex = process.env[this.envVar];
|
|
80
73
|
if (!hex) {
|
|
81
|
-
throw new Error(`${this.envVar} is not set.
|
|
74
|
+
throw new Error(`${this.envVar} is not set. EncryptedFileSecretStore needs a 32-byte hex-encoded key-encryption ` +
|
|
82
75
|
"key — generate one with `openssl rand -hex 32` for local/dev use, or implement a real " +
|
|
83
76
|
"KekProvider against your own KMS for production (see keystore.ts's doc comment).");
|
|
84
77
|
}
|
|
@@ -89,14 +82,14 @@ export class EnvKekProvider {
|
|
|
89
82
|
return buf;
|
|
90
83
|
}
|
|
91
84
|
}
|
|
92
|
-
export class
|
|
85
|
+
export class EncryptedFileSecretStore {
|
|
93
86
|
filePath;
|
|
94
87
|
kek;
|
|
95
88
|
constructor(filePath, kek) {
|
|
96
89
|
this.filePath = filePath;
|
|
97
90
|
this.kek = kek;
|
|
98
91
|
}
|
|
99
|
-
async
|
|
92
|
+
async hasSecret() {
|
|
100
93
|
try {
|
|
101
94
|
await readFile(this.filePath);
|
|
102
95
|
return true;
|
|
@@ -105,23 +98,17 @@ export class EncryptedFileStore {
|
|
|
105
98
|
return false;
|
|
106
99
|
}
|
|
107
100
|
}
|
|
108
|
-
async
|
|
109
|
-
if (await this.
|
|
110
|
-
throw new Error(`A
|
|
111
|
-
"would orphan the address already registered on-chain.");
|
|
101
|
+
async store(secret) {
|
|
102
|
+
if (await this.hasSecret()) {
|
|
103
|
+
throw new Error(`A secret already exists at ${this.filePath}. Refusing to overwrite.`);
|
|
112
104
|
}
|
|
113
|
-
|
|
114
|
-
await this.writeEncrypted(privateKey);
|
|
115
|
-
return deriveAddress(privateKey);
|
|
116
|
-
}
|
|
117
|
-
async withPrivateKey(fn) {
|
|
118
|
-
const privateKey = await this.readEncrypted();
|
|
119
|
-
return fn(privateKey);
|
|
105
|
+
await this.writeEncrypted(secret);
|
|
120
106
|
}
|
|
121
|
-
async
|
|
122
|
-
|
|
107
|
+
async withSecret(fn) {
|
|
108
|
+
const secret = await this.readEncrypted();
|
|
109
|
+
return fn(secret);
|
|
123
110
|
}
|
|
124
|
-
async writeEncrypted(
|
|
111
|
+
async writeEncrypted(secret) {
|
|
125
112
|
const salt = randomBytes(16);
|
|
126
113
|
const kekMaterial = await this.kek.getKek();
|
|
127
114
|
// scrypt-derive a per-store encryption key from the raw KEK + a
|
|
@@ -131,7 +118,7 @@ export class EncryptedFileStore {
|
|
|
131
118
|
const derivedKey = scryptSync(kekMaterial, salt, 32);
|
|
132
119
|
const iv = randomBytes(12);
|
|
133
120
|
const cipher = createCipheriv("aes-256-gcm", derivedKey, iv);
|
|
134
|
-
const ciphertext = Buffer.concat([cipher.update(
|
|
121
|
+
const ciphertext = Buffer.concat([cipher.update(secret, "utf8"), cipher.final()]);
|
|
135
122
|
const authTag = cipher.getAuthTag();
|
|
136
123
|
const payload = {
|
|
137
124
|
version: 1,
|
|
@@ -157,18 +144,80 @@ export class EncryptedFileStore {
|
|
|
157
144
|
/** Picks OS keychain by default, falling back to the encrypted file
|
|
158
145
|
* store only when the OS keychain genuinely isn't available (headless
|
|
159
146
|
* container with no Secret Service, etc.) — never silently prefers the
|
|
160
|
-
* weaker option.
|
|
161
|
-
|
|
147
|
+
* weaker option. Shared by both EVM's and Solana's key stores below —
|
|
148
|
+
* `account`/`fallbackFilePath` must already be kind-specific (see
|
|
149
|
+
* OsKeychainSecretStore's doc comment). */
|
|
150
|
+
async function defaultSecretStore(account, fallbackFilePath, kek) {
|
|
162
151
|
try {
|
|
163
152
|
await import("@napi-rs/keyring");
|
|
164
|
-
const store = new
|
|
153
|
+
const store = new OsKeychainSecretStore(account);
|
|
165
154
|
// Probe for a usable keychain backend (this throws on headless Linux
|
|
166
155
|
// hosts with no Secret Service daemon running) without touching any
|
|
167
156
|
// real credential.
|
|
168
|
-
await store.
|
|
157
|
+
await store.hasSecret();
|
|
169
158
|
return store;
|
|
170
159
|
}
|
|
171
160
|
catch {
|
|
172
|
-
return new
|
|
161
|
+
return new EncryptedFileSecretStore(fallbackFilePath, kek);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** EVM (secp256k1) key store — the original `KeyStore`, now a thin
|
|
165
|
+
* curve-specific wrapper over the generic `SecretStore` above rather
|
|
166
|
+
* than owning its own storage logic. */
|
|
167
|
+
class EvmKeyStore {
|
|
168
|
+
store;
|
|
169
|
+
constructor(store) {
|
|
170
|
+
this.store = store;
|
|
171
|
+
}
|
|
172
|
+
hasKey() {
|
|
173
|
+
return this.store.hasSecret();
|
|
174
|
+
}
|
|
175
|
+
async generateAndStore() {
|
|
176
|
+
const privateKey = generatePrivateKey();
|
|
177
|
+
await this.store.store(privateKey);
|
|
178
|
+
return deriveEvmAddress(privateKey);
|
|
179
|
+
}
|
|
180
|
+
withPrivateKey(fn) {
|
|
181
|
+
return this.store.withSecret((secret) => fn(secret));
|
|
182
|
+
}
|
|
183
|
+
address() {
|
|
184
|
+
return this.withPrivateKey((pk) => deriveEvmAddress(pk));
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
/** Solana (Ed25519) key store — same storage backends as `EvmKeyStore`,
|
|
188
|
+
* a completely separate secret (never derived from or related to the
|
|
189
|
+
* EVM key), stored under its own account/file so the two can be
|
|
190
|
+
* generated, rotated, or lost independently. The secret key is
|
|
191
|
+
* persisted base64-encoded (`Keypair.secretKey` is a raw 64-byte
|
|
192
|
+
* buffer, not directly JSON/string-safe). */
|
|
193
|
+
class Ed25519SolanaKeyStore {
|
|
194
|
+
store;
|
|
195
|
+
constructor(store) {
|
|
196
|
+
this.store = store;
|
|
197
|
+
}
|
|
198
|
+
hasKey() {
|
|
199
|
+
return this.store.hasSecret();
|
|
200
|
+
}
|
|
201
|
+
async generateAndStore() {
|
|
202
|
+
const keypair = Keypair.generate();
|
|
203
|
+
await this.store.store(Buffer.from(keypair.secretKey).toString("base64"));
|
|
204
|
+
return keypair.publicKey.toBase58();
|
|
205
|
+
}
|
|
206
|
+
withSecretKey(fn) {
|
|
207
|
+
return this.store.withSecret((secret) => fn(Buffer.from(secret, "base64")));
|
|
208
|
+
}
|
|
209
|
+
address() {
|
|
210
|
+
return this.withSecretKey((sk) => Keypair.fromSecretKey(sk).publicKey.toBase58());
|
|
173
211
|
}
|
|
174
212
|
}
|
|
213
|
+
export async function defaultKeyStore(agentId, fallbackFilePath, kek) {
|
|
214
|
+
return new EvmKeyStore(await defaultSecretStore(agentId, fallbackFilePath, kek));
|
|
215
|
+
}
|
|
216
|
+
/** Same defaulting behavior as `defaultKeyStore`, for the Solana key —
|
|
217
|
+
* `fallbackFilePath` must be a different path than the EVM store's
|
|
218
|
+
* (e.g. `solana-key.enc.json` next to `key.enc.json`), and the OS
|
|
219
|
+
* keychain account is automatically suffixed so the two never collide
|
|
220
|
+
* under the same service/account pair. */
|
|
221
|
+
export async function defaultSolanaKeyStore(agentId, fallbackFilePath, kek) {
|
|
222
|
+
return new Ed25519SolanaKeyStore(await defaultSecretStore(`${agentId}:solana`, fallbackFilePath, kek));
|
|
223
|
+
}
|
package/dist/policy.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface SigningRequest {
|
|
|
19
19
|
* operations that don't move value (identity registration, the
|
|
20
20
|
* EIP-7702 delegation itself). */
|
|
21
21
|
amount: number | null;
|
|
22
|
-
counterparty:
|
|
22
|
+
counterparty: string | null;
|
|
23
23
|
/** Contract + 4-byte selector, for a MandateEnforcer.execute()-shaped
|
|
24
24
|
* call — checked against an allowlist if one is configured (see
|
|
25
25
|
* PolicyEngine's constructor). Mandate.spending/counterpartyRequirements
|
package/dist/signer.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Address, type Hex } from "viem";
|
|
2
|
-
import type { KeyStore } from "./keystore.js";
|
|
2
|
+
import type { KeyStore, SolanaKeyStore } from "./keystore.js";
|
|
3
3
|
import type { PolicyEngine } from "./policy.js";
|
|
4
4
|
import { AuditLog } from "./audit-log.js";
|
|
5
5
|
interface RateLimiter {
|
|
@@ -22,11 +22,14 @@ export interface SignerOptions {
|
|
|
22
22
|
policyEngine: PolicyEngine;
|
|
23
23
|
auditLog: AuditLog;
|
|
24
24
|
rateLimiter?: RateLimiter;
|
|
25
|
+
solanaKeyStore?: SolanaKeyStore;
|
|
25
26
|
}
|
|
26
27
|
export declare class Signer {
|
|
27
28
|
private readonly options;
|
|
28
29
|
constructor(options: SignerOptions);
|
|
29
30
|
address(): Promise<Address>;
|
|
31
|
+
private requireSolanaKeyStore;
|
|
32
|
+
solanaAddress(): Promise<string>;
|
|
30
33
|
private logAndEnforce;
|
|
31
34
|
/** One-time: signs the register() call that mints this agent's
|
|
32
35
|
* ERC-8004 identity, owned by this signer's own address. Takes an
|
|
@@ -36,7 +39,22 @@ export declare class Signer {
|
|
|
36
39
|
* already exists in erc8004-client.ts's server-side counterpart) and
|
|
37
40
|
* to keep this signer's own network surface at zero: it never makes
|
|
38
41
|
* an RPC call, only signs exactly what it's handed. `agentURI` is
|
|
39
|
-
* accepted for the audit log entry, not used to build the call.
|
|
42
|
+
* accepted for the audit log entry, not used to build the call.
|
|
43
|
+
*
|
|
44
|
+
* Signs via `account.signTransaction()` directly, not a WalletClient —
|
|
45
|
+
* a real bug, found live: `createWalletClient(...).signTransaction()`
|
|
46
|
+
* silently calls its transport for `eth_chainId` (and, once that's
|
|
47
|
+
* reachable, fee-market data) whenever the tx is missing a `type` or
|
|
48
|
+
* fee fields, which the offline placeholder above can never answer —
|
|
49
|
+
* contradicting this method's own "never makes an RPC call" claim.
|
|
50
|
+
* `viem/accounts`' local account type has no such dependency; the
|
|
51
|
+
* trade-off is that a caller-omitted fee/gas field now needs an
|
|
52
|
+
* explicit local default (below) instead of network-derived current
|
|
53
|
+
* values — correct for keeping this signer's network surface at
|
|
54
|
+
* zero, but the caller (erc8004-client.ts) supplying real values,
|
|
55
|
+
* which it already has the RPC access to compute, remains the better
|
|
56
|
+
* long-term fix; these defaults are a safe fallback, not a reason not
|
|
57
|
+
* to. */
|
|
40
58
|
signRegisterIdentity(params: {
|
|
41
59
|
agentURI: string;
|
|
42
60
|
unsignedTx: {
|
|
@@ -44,6 +62,7 @@ export declare class Signer {
|
|
|
44
62
|
nonce: number;
|
|
45
63
|
data: Hex;
|
|
46
64
|
gas?: bigint;
|
|
65
|
+
gasPrice?: bigint;
|
|
47
66
|
};
|
|
48
67
|
chainId: number;
|
|
49
68
|
}): Promise<{
|
|
@@ -103,7 +122,22 @@ export declare class Signer {
|
|
|
103
122
|
}>;
|
|
104
123
|
/** Owner-initiated funds-out — same policy checks apply (a withdrawal
|
|
105
124
|
* is still constrained by the Mandate's spend limits and, if
|
|
106
|
-
* configured, allowed-counterparty list).
|
|
125
|
+
* configured, allowed-counterparty list).
|
|
126
|
+
*
|
|
127
|
+
* Checked against the actual caller before fixing, not assumed: as of
|
|
128
|
+
* this writing, gora8's own server never sends this operation —
|
|
129
|
+
* services/payment/onchain.ts's withdrawEvm() routes a withdrawal
|
|
130
|
+
* through executeViaMandate() (signMandateExecute, unaffected by this
|
|
131
|
+
* bug) instead, since a withdrawal is exactly the kind of spend
|
|
132
|
+
* Authority already has to constrain. This method is nonetheless real
|
|
133
|
+
* public API surface (agent-ts's sign-handler.ts routes to it) a
|
|
134
|
+
* third-party integration could call directly, so it gets the same
|
|
135
|
+
* fix as signRegisterIdentity above rather than being left broken
|
|
136
|
+
* because gora8's own server happens not to exercise it. Adds a
|
|
137
|
+
* required `nonce` (this method cannot produce a valid signed
|
|
138
|
+
* transaction without one, whichever caller eventually supplies it)
|
|
139
|
+
* and optional gas/fee fields, same shape and defaulting as
|
|
140
|
+
* signRegisterIdentity. */
|
|
107
141
|
signWithdrawal(params: {
|
|
108
142
|
to: Address;
|
|
109
143
|
amount: number;
|
|
@@ -111,10 +145,36 @@ export declare class Signer {
|
|
|
111
145
|
to: Address;
|
|
112
146
|
value: bigint;
|
|
113
147
|
data: Hex;
|
|
148
|
+
nonce: number;
|
|
149
|
+
gas?: bigint;
|
|
150
|
+
gasPrice?: bigint;
|
|
114
151
|
};
|
|
115
152
|
chainId: number;
|
|
116
153
|
}): Promise<{
|
|
117
154
|
signedTransaction: Hex;
|
|
118
155
|
}>;
|
|
156
|
+
/** Signs this agent's own required-signer slot in a pre-built Solana
|
|
157
|
+
* transaction — the same "server builds it, signer only authorizes"
|
|
158
|
+
* division as the EVM Mandate-execute path (signMandateExecute
|
|
159
|
+
* above). The caller (gora8's server) already set the transaction's
|
|
160
|
+
* recent blockhash and fee payer (gora8's own fee-payer wallet, a
|
|
161
|
+
* separate signer this package never touches) before serializing it
|
|
162
|
+
* with `requireAllSignatures: false` — so signing here needs no RPC
|
|
163
|
+
* call either, the same zero-network-surface property every other
|
|
164
|
+
* method in this file holds to (unlike the EVM signTransaction bug
|
|
165
|
+
* fixed elsewhere in this file, Solana's `Transaction.partialSign()`
|
|
166
|
+
* has no such hidden network dependency — verified by construction:
|
|
167
|
+
* it operates purely on the already-deserialized message). Returns
|
|
168
|
+
* only this signer's own signature, base64-encoded; the caller
|
|
169
|
+
* merges it into the full transaction and adds the fee payer's
|
|
170
|
+
* signature before broadcasting — this package still never
|
|
171
|
+
* broadcasts anything, same discipline as the EVM side. */
|
|
172
|
+
signSolanaTransaction(params: {
|
|
173
|
+
serializedTransaction: string;
|
|
174
|
+
amount: number | null;
|
|
175
|
+
counterparty: string | null;
|
|
176
|
+
}): Promise<{
|
|
177
|
+
signature: string;
|
|
178
|
+
}>;
|
|
119
179
|
}
|
|
120
180
|
export {};
|
package/dist/signer.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createWalletClient, http, defineChain } from "viem";
|
|
2
|
+
import { Transaction, Keypair } from "@solana/web3.js";
|
|
2
3
|
/** Simple sliding-window limiter — no external dependency needed for
|
|
3
4
|
* something this small. */
|
|
4
5
|
export class SlidingWindowRateLimiter {
|
|
@@ -26,6 +27,17 @@ export class SlidingWindowRateLimiter {
|
|
|
26
27
|
// access is needed or granted here; broadcasting is gora8's relay
|
|
27
28
|
// wallet's job, not this signer's — see this file's own doc comment.
|
|
28
29
|
const OFFLINE_PLACEHOLDER_RPC_URL = "http://127.0.0.1:0";
|
|
30
|
+
// Safe, deliberately generous offline defaults for a legacy-type
|
|
31
|
+
// transaction whose caller didn't supply gas/fee fields — a real
|
|
32
|
+
// per-chain current price would be lower on every chain this signer
|
|
33
|
+
// targets today (all low-fee L2s), so overpaying trivially here is the
|
|
34
|
+
// safe failure direction; underpricing (or a gas limit too low for a
|
|
35
|
+
// contract call to complete) is the unsafe one. Deliberately not
|
|
36
|
+
// fetched from a real RPC, per this file's zero-network-surface design —
|
|
37
|
+
// see signRegisterIdentity's doc comment for why a caller-supplied real
|
|
38
|
+
// value should be preferred once available.
|
|
39
|
+
const DEFAULT_GAS_PRICE = 1000000000n; // 1 gwei
|
|
40
|
+
const DEFAULT_GAS_LIMIT = 300000n; // generous for a simple contract call (e.g. ERC-8004 register())
|
|
29
41
|
function genericChain(chainId) {
|
|
30
42
|
return defineChain({
|
|
31
43
|
id: chainId,
|
|
@@ -42,6 +54,15 @@ export class Signer {
|
|
|
42
54
|
async address() {
|
|
43
55
|
return this.options.keyStore.address();
|
|
44
56
|
}
|
|
57
|
+
requireSolanaKeyStore() {
|
|
58
|
+
if (!this.options.solanaKeyStore) {
|
|
59
|
+
throw new Error("No Solana key configured for this signer — run `gora8-signer init <agent-id>` with Solana support, or pass solanaKeyStore explicitly.");
|
|
60
|
+
}
|
|
61
|
+
return this.options.solanaKeyStore;
|
|
62
|
+
}
|
|
63
|
+
async solanaAddress() {
|
|
64
|
+
return this.requireSolanaKeyStore().address();
|
|
65
|
+
}
|
|
45
66
|
async logAndEnforce(operation, request) {
|
|
46
67
|
this.options.rateLimiter?.checkAndRecord();
|
|
47
68
|
const decision = await this.options.policyEngine.evaluate(request);
|
|
@@ -66,20 +87,35 @@ export class Signer {
|
|
|
66
87
|
* already exists in erc8004-client.ts's server-side counterpart) and
|
|
67
88
|
* to keep this signer's own network surface at zero: it never makes
|
|
68
89
|
* an RPC call, only signs exactly what it's handed. `agentURI` is
|
|
69
|
-
* accepted for the audit log entry, not used to build the call.
|
|
90
|
+
* accepted for the audit log entry, not used to build the call.
|
|
91
|
+
*
|
|
92
|
+
* Signs via `account.signTransaction()` directly, not a WalletClient —
|
|
93
|
+
* a real bug, found live: `createWalletClient(...).signTransaction()`
|
|
94
|
+
* silently calls its transport for `eth_chainId` (and, once that's
|
|
95
|
+
* reachable, fee-market data) whenever the tx is missing a `type` or
|
|
96
|
+
* fee fields, which the offline placeholder above can never answer —
|
|
97
|
+
* contradicting this method's own "never makes an RPC call" claim.
|
|
98
|
+
* `viem/accounts`' local account type has no such dependency; the
|
|
99
|
+
* trade-off is that a caller-omitted fee/gas field now needs an
|
|
100
|
+
* explicit local default (below) instead of network-derived current
|
|
101
|
+
* values — correct for keeping this signer's network surface at
|
|
102
|
+
* zero, but the caller (erc8004-client.ts) supplying real values,
|
|
103
|
+
* which it already has the RPC access to compute, remains the better
|
|
104
|
+
* long-term fix; these defaults are a safe fallback, not a reason not
|
|
105
|
+
* to. */
|
|
70
106
|
async signRegisterIdentity(params) {
|
|
71
107
|
await this.logAndEnforce("signRegisterIdentity", { amount: null, counterparty: null, contract: null, selector: null });
|
|
72
108
|
return this.options.keyStore.withPrivateKey(async (privateKey) => {
|
|
73
109
|
const { privateKeyToAccount } = await import("viem/accounts");
|
|
74
110
|
const account = privateKeyToAccount(privateKey);
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
chain: genericChain(params.chainId),
|
|
111
|
+
const signedTransaction = await account.signTransaction({
|
|
112
|
+
type: "legacy",
|
|
113
|
+
chainId: params.chainId,
|
|
79
114
|
to: params.unsignedTx.to,
|
|
80
115
|
nonce: params.unsignedTx.nonce,
|
|
81
116
|
data: params.unsignedTx.data,
|
|
82
|
-
gas: params.unsignedTx.gas,
|
|
117
|
+
gas: params.unsignedTx.gas ?? DEFAULT_GAS_LIMIT,
|
|
118
|
+
gasPrice: params.unsignedTx.gasPrice ?? DEFAULT_GAS_PRICE,
|
|
83
119
|
});
|
|
84
120
|
return { signedTransaction };
|
|
85
121
|
});
|
|
@@ -152,21 +188,68 @@ export class Signer {
|
|
|
152
188
|
}
|
|
153
189
|
/** Owner-initiated funds-out — same policy checks apply (a withdrawal
|
|
154
190
|
* is still constrained by the Mandate's spend limits and, if
|
|
155
|
-
* configured, allowed-counterparty list).
|
|
191
|
+
* configured, allowed-counterparty list).
|
|
192
|
+
*
|
|
193
|
+
* Checked against the actual caller before fixing, not assumed: as of
|
|
194
|
+
* this writing, gora8's own server never sends this operation —
|
|
195
|
+
* services/payment/onchain.ts's withdrawEvm() routes a withdrawal
|
|
196
|
+
* through executeViaMandate() (signMandateExecute, unaffected by this
|
|
197
|
+
* bug) instead, since a withdrawal is exactly the kind of spend
|
|
198
|
+
* Authority already has to constrain. This method is nonetheless real
|
|
199
|
+
* public API surface (agent-ts's sign-handler.ts routes to it) a
|
|
200
|
+
* third-party integration could call directly, so it gets the same
|
|
201
|
+
* fix as signRegisterIdentity above rather than being left broken
|
|
202
|
+
* because gora8's own server happens not to exercise it. Adds a
|
|
203
|
+
* required `nonce` (this method cannot produce a valid signed
|
|
204
|
+
* transaction without one, whichever caller eventually supplies it)
|
|
205
|
+
* and optional gas/fee fields, same shape and defaulting as
|
|
206
|
+
* signRegisterIdentity. */
|
|
156
207
|
async signWithdrawal(params) {
|
|
157
208
|
await this.logAndEnforce("signWithdrawal", { amount: params.amount, counterparty: params.to, contract: null, selector: null });
|
|
158
209
|
return this.options.keyStore.withPrivateKey(async (privateKey) => {
|
|
159
210
|
const { privateKeyToAccount } = await import("viem/accounts");
|
|
160
211
|
const account = privateKeyToAccount(privateKey);
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
|
|
212
|
+
const signedTransaction = await account.signTransaction({
|
|
213
|
+
type: "legacy",
|
|
214
|
+
chainId: params.chainId,
|
|
164
215
|
to: params.unsignedTx.to,
|
|
165
216
|
value: params.unsignedTx.value,
|
|
166
217
|
data: params.unsignedTx.data,
|
|
167
|
-
|
|
218
|
+
nonce: params.unsignedTx.nonce,
|
|
219
|
+
gas: params.unsignedTx.gas ?? DEFAULT_GAS_LIMIT,
|
|
220
|
+
gasPrice: params.unsignedTx.gasPrice ?? DEFAULT_GAS_PRICE,
|
|
168
221
|
});
|
|
169
222
|
return { signedTransaction };
|
|
170
223
|
});
|
|
171
224
|
}
|
|
225
|
+
/** Signs this agent's own required-signer slot in a pre-built Solana
|
|
226
|
+
* transaction — the same "server builds it, signer only authorizes"
|
|
227
|
+
* division as the EVM Mandate-execute path (signMandateExecute
|
|
228
|
+
* above). The caller (gora8's server) already set the transaction's
|
|
229
|
+
* recent blockhash and fee payer (gora8's own fee-payer wallet, a
|
|
230
|
+
* separate signer this package never touches) before serializing it
|
|
231
|
+
* with `requireAllSignatures: false` — so signing here needs no RPC
|
|
232
|
+
* call either, the same zero-network-surface property every other
|
|
233
|
+
* method in this file holds to (unlike the EVM signTransaction bug
|
|
234
|
+
* fixed elsewhere in this file, Solana's `Transaction.partialSign()`
|
|
235
|
+
* has no such hidden network dependency — verified by construction:
|
|
236
|
+
* it operates purely on the already-deserialized message). Returns
|
|
237
|
+
* only this signer's own signature, base64-encoded; the caller
|
|
238
|
+
* merges it into the full transaction and adds the fee payer's
|
|
239
|
+
* signature before broadcasting — this package still never
|
|
240
|
+
* broadcasts anything, same discipline as the EVM side. */
|
|
241
|
+
async signSolanaTransaction(params) {
|
|
242
|
+
await this.logAndEnforce("signSolanaTransaction", { amount: params.amount, counterparty: params.counterparty, contract: null, selector: null });
|
|
243
|
+
const solanaKeyStore = this.requireSolanaKeyStore();
|
|
244
|
+
return solanaKeyStore.withSecretKey(async (secretKey) => {
|
|
245
|
+
const keypair = Keypair.fromSecretKey(secretKey);
|
|
246
|
+
const transaction = Transaction.from(Buffer.from(params.serializedTransaction, "base64"));
|
|
247
|
+
transaction.partialSign(keypair);
|
|
248
|
+
const entry = transaction.signatures.find((s) => s.publicKey.equals(keypair.publicKey));
|
|
249
|
+
if (!entry?.signature) {
|
|
250
|
+
throw new Error("Signing produced no signature for this agent's Solana public key — the transaction doesn't name it as a required signer.");
|
|
251
|
+
}
|
|
252
|
+
return { signature: Buffer.from(entry.signature).toString("base64") };
|
|
253
|
+
});
|
|
254
|
+
}
|
|
172
255
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gora8-signer",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Local, self-custodied key signing for gora8 agents — the key is generated and used wherever your agent runs, never inside gora8's own infrastructure. See SELF_CUSTODY_ARCHITECTURE.md in the gora8 monorepo for the full design.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -15,7 +15,11 @@
|
|
|
15
15
|
"bin": {
|
|
16
16
|
"gora8-signer": "./dist/cli.js"
|
|
17
17
|
},
|
|
18
|
-
"files": [
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"README.md",
|
|
21
|
+
"LICENSE"
|
|
22
|
+
],
|
|
19
23
|
"scripts": {
|
|
20
24
|
"build": "tsc",
|
|
21
25
|
"prepublishOnly": "npm run build"
|
|
@@ -24,11 +28,12 @@
|
|
|
24
28
|
"node": ">=18"
|
|
25
29
|
},
|
|
26
30
|
"devDependencies": {
|
|
27
|
-
"
|
|
28
|
-
"
|
|
31
|
+
"@types/node": "^20.0.0",
|
|
32
|
+
"typescript": "^5.6.0"
|
|
29
33
|
},
|
|
30
34
|
"dependencies": {
|
|
31
|
-
"
|
|
32
|
-
"@
|
|
35
|
+
"@napi-rs/keyring": "^1.3.0",
|
|
36
|
+
"@solana/web3.js": "^1.98.4",
|
|
37
|
+
"viem": "^2.21.0"
|
|
33
38
|
}
|
|
34
39
|
}
|