@equalfi/ski 0.1.1 → 0.1.2
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 +33 -0
- package/bin/cli.js +8 -43
- package/lib/index.js +1 -0
- package/lib/session.js +152 -0
- package/package.json +3 -1
package/README.md
CHANGED
|
@@ -15,6 +15,39 @@ pnpm dlx @equalfi/ski
|
|
|
15
15
|
- Stores the password in your OS keychain (one prompt) via @napi-rs/keyring
|
|
16
16
|
- Prints the session key address to paste into the UI
|
|
17
17
|
|
|
18
|
+
## Runtime helper (for agents)
|
|
19
|
+
This package also exposes helpers to execute calls via `executeWithRuntimeValidation` using the session key.
|
|
20
|
+
|
|
21
|
+
```js
|
|
22
|
+
const { loadSessionWallet, buildRuntimeAuthorization, executeWithRuntimeValidation } = require('@equalfi/ski');
|
|
23
|
+
|
|
24
|
+
const wallet = await loadSessionWallet('position-1');
|
|
25
|
+
// build runtime authorization
|
|
26
|
+
const data = '0x...'; // ABI-encoded call (e.g., nonce())
|
|
27
|
+
const { authorization } = await buildRuntimeAuthorization({
|
|
28
|
+
moduleAddress: '0xSessionKeyValidationModule',
|
|
29
|
+
account: '0xTBA',
|
|
30
|
+
entityId: 7,
|
|
31
|
+
sender: wallet.address,
|
|
32
|
+
value: 0n,
|
|
33
|
+
data,
|
|
34
|
+
sessionWallet: wallet,
|
|
35
|
+
chainId: 31337,
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// or send directly
|
|
39
|
+
await executeWithRuntimeValidation({
|
|
40
|
+
rpcUrl: 'http://127.0.0.1:8545',
|
|
41
|
+
tbaAddress: '0xTBA',
|
|
42
|
+
moduleAddress: '0xSessionKeyValidationModule',
|
|
43
|
+
entityId: 7,
|
|
44
|
+
data,
|
|
45
|
+
value: 0n,
|
|
46
|
+
sessionWallet: wallet,
|
|
47
|
+
chainId: 31337,
|
|
48
|
+
});
|
|
49
|
+
```
|
|
50
|
+
|
|
18
51
|
## Notes
|
|
19
52
|
- Default label is `position-<tokenId>`
|
|
20
53
|
- If the key already exists, it asks before overwriting
|
package/bin/cli.js
CHANGED
|
@@ -5,15 +5,12 @@ const path = require('path');
|
|
|
5
5
|
const os = require('os');
|
|
6
6
|
const crypto = require('crypto');
|
|
7
7
|
const prompts = require('prompts');
|
|
8
|
-
const {
|
|
9
|
-
const { Wallet } = require('ethers');
|
|
8
|
+
const { createSessionKey } = require('../lib/session');
|
|
10
9
|
|
|
11
|
-
const SERVICE = 'equalfi-ski';
|
|
12
|
-
const SKILL_ID = 'equalfi';
|
|
13
10
|
const DEFAULT_ENTITY_ID = 7;
|
|
14
11
|
|
|
15
12
|
const resolvePaths = (label) => {
|
|
16
|
-
const baseDir = path.join(os.homedir(), '.openclaw', 'keys',
|
|
13
|
+
const baseDir = path.join(os.homedir(), '.openclaw', 'keys', 'equalfi');
|
|
17
14
|
return {
|
|
18
15
|
baseDir,
|
|
19
16
|
keystorePath: path.join(baseDir, `${label}.json`),
|
|
@@ -21,12 +18,6 @@ const resolvePaths = (label) => {
|
|
|
21
18
|
};
|
|
22
19
|
};
|
|
23
20
|
|
|
24
|
-
const keychainAccount = (label) => `session-key:${SKILL_ID}:${label}`;
|
|
25
|
-
|
|
26
|
-
async function ensureDir(dir) {
|
|
27
|
-
await fs.mkdir(dir, { recursive: true });
|
|
28
|
-
}
|
|
29
|
-
|
|
30
21
|
async function fileExists(p) {
|
|
31
22
|
try {
|
|
32
23
|
await fs.access(p);
|
|
@@ -61,8 +52,7 @@ async function main() {
|
|
|
61
52
|
const label = `position-${response.tokenId}`;
|
|
62
53
|
const entityId = Number(response.entityId ?? DEFAULT_ENTITY_ID);
|
|
63
54
|
|
|
64
|
-
const {
|
|
65
|
-
await ensureDir(baseDir);
|
|
55
|
+
const { keystorePath } = resolvePaths(label);
|
|
66
56
|
|
|
67
57
|
if (await fileExists(keystorePath)) {
|
|
68
58
|
const confirm = await prompts({
|
|
@@ -77,44 +67,19 @@ async function main() {
|
|
|
77
67
|
}
|
|
78
68
|
}
|
|
79
69
|
|
|
80
|
-
|
|
81
|
-
const password = crypto.randomBytes(32).toString('hex');
|
|
82
|
-
const keystore = await wallet.encrypt(password);
|
|
83
|
-
|
|
84
|
-
await fs.writeFile(keystorePath, keystore);
|
|
85
|
-
await fs.writeFile(
|
|
86
|
-
metaPath,
|
|
87
|
-
JSON.stringify(
|
|
88
|
-
{
|
|
89
|
-
address: wallet.address,
|
|
90
|
-
chainId: null,
|
|
91
|
-
skillId: SKILL_ID,
|
|
92
|
-
agentLabel: label,
|
|
93
|
-
positionTokenId: response.tokenId,
|
|
94
|
-
entityId,
|
|
95
|
-
createdAt: new Date().toISOString(),
|
|
96
|
-
keychainService: SERVICE,
|
|
97
|
-
keychainAccount: keychainAccount(label),
|
|
98
|
-
},
|
|
99
|
-
null,
|
|
100
|
-
2
|
|
101
|
-
)
|
|
102
|
-
);
|
|
103
|
-
|
|
70
|
+
let created;
|
|
104
71
|
try {
|
|
105
|
-
|
|
106
|
-
entry.setPassword(password);
|
|
72
|
+
created = await createSessionKey({ label, entityId, allowOverwrite: true });
|
|
107
73
|
} catch (err) {
|
|
108
|
-
console.error('Failed to
|
|
109
|
-
console.error('Keystore saved, but you must secure the password manually.');
|
|
74
|
+
console.error('Failed to create session key:', err?.message || err);
|
|
110
75
|
return;
|
|
111
76
|
}
|
|
112
77
|
|
|
113
78
|
console.log('\n✅ Session key created');
|
|
114
|
-
console.log(`Address: ${
|
|
79
|
+
console.log(`Address: ${created.address}`);
|
|
115
80
|
console.log(`Label: ${label}`);
|
|
116
81
|
console.log(`Entity: ${entityId}`);
|
|
117
|
-
console.log(`Stored: ${keystorePath}`);
|
|
82
|
+
console.log(`Stored: ${created.keystorePath}`);
|
|
118
83
|
console.log('\nPaste the address into the UI session key field and install the module/policy.');
|
|
119
84
|
}
|
|
120
85
|
|
package/lib/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
module.exports = require('./session');
|
package/lib/session.js
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
const fs = require('fs/promises');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const crypto = require('crypto');
|
|
5
|
+
const { Entry } = require('@napi-rs/keyring');
|
|
6
|
+
const { Wallet, Contract, JsonRpcProvider, AbiCoder, keccak256, toUtf8Bytes, getBytes } = require('ethers');
|
|
7
|
+
|
|
8
|
+
const SERVICE = 'equalfi-ski';
|
|
9
|
+
const SKILL_ID = 'equalfi';
|
|
10
|
+
|
|
11
|
+
const resolvePaths = (label) => {
|
|
12
|
+
const baseDir = path.join(os.homedir(), '.openclaw', 'keys', SKILL_ID);
|
|
13
|
+
return {
|
|
14
|
+
baseDir,
|
|
15
|
+
keystorePath: path.join(baseDir, `${label}.json`),
|
|
16
|
+
metaPath: path.join(baseDir, `${label}.meta.json`),
|
|
17
|
+
};
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
const keychainAccount = (label) => `session-key:${SKILL_ID}:${label}`;
|
|
21
|
+
|
|
22
|
+
async function ensureDir(dir) {
|
|
23
|
+
await fs.mkdir(dir, { recursive: true });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async function fileExists(p) {
|
|
27
|
+
try {
|
|
28
|
+
await fs.access(p);
|
|
29
|
+
return true;
|
|
30
|
+
} catch {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function moduleEntity(module, entityId) {
|
|
36
|
+
const clean = module.toLowerCase().replace(/^0x/, '');
|
|
37
|
+
const entityHex = BigInt(entityId).toString(16).padStart(8, '0');
|
|
38
|
+
return `0x${clean}${entityHex}`; // bytes24
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function createSessionKey({ label, entityId = 7, allowOverwrite = false }) {
|
|
42
|
+
const { baseDir, keystorePath, metaPath } = resolvePaths(label);
|
|
43
|
+
await ensureDir(baseDir);
|
|
44
|
+
|
|
45
|
+
if (!allowOverwrite && (await fileExists(keystorePath))) {
|
|
46
|
+
throw new Error(`Key already exists for ${label}.`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const wallet = Wallet.createRandom();
|
|
50
|
+
const password = crypto.randomBytes(32).toString('hex');
|
|
51
|
+
const keystore = await wallet.encrypt(password);
|
|
52
|
+
|
|
53
|
+
await fs.writeFile(keystorePath, keystore);
|
|
54
|
+
await fs.writeFile(
|
|
55
|
+
metaPath,
|
|
56
|
+
JSON.stringify(
|
|
57
|
+
{
|
|
58
|
+
address: wallet.address,
|
|
59
|
+
chainId: null,
|
|
60
|
+
skillId: SKILL_ID,
|
|
61
|
+
agentLabel: label,
|
|
62
|
+
entityId,
|
|
63
|
+
createdAt: new Date().toISOString(),
|
|
64
|
+
keychainService: SERVICE,
|
|
65
|
+
keychainAccount: keychainAccount(label),
|
|
66
|
+
},
|
|
67
|
+
null,
|
|
68
|
+
2
|
|
69
|
+
)
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
const entry = new Entry(SERVICE, keychainAccount(label));
|
|
73
|
+
entry.setPassword(password);
|
|
74
|
+
|
|
75
|
+
return { address: wallet.address, keystorePath, metaPath };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async function loadSessionWallet(label) {
|
|
79
|
+
const { keystorePath } = resolvePaths(label);
|
|
80
|
+
const entry = new Entry(SERVICE, keychainAccount(label));
|
|
81
|
+
const password = entry.getPassword();
|
|
82
|
+
if (!password) {
|
|
83
|
+
throw new Error(`Missing keychain entry for ${label}`);
|
|
84
|
+
}
|
|
85
|
+
const keystore = await fs.readFile(keystorePath, 'utf8');
|
|
86
|
+
const wallet = await Wallet.fromEncryptedJson(keystore, password);
|
|
87
|
+
return wallet;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function buildRuntimeAuthorization({
|
|
91
|
+
moduleAddress,
|
|
92
|
+
account,
|
|
93
|
+
entityId,
|
|
94
|
+
sender,
|
|
95
|
+
value = 0n,
|
|
96
|
+
data,
|
|
97
|
+
sessionWallet,
|
|
98
|
+
replayProtection,
|
|
99
|
+
chainId,
|
|
100
|
+
}) {
|
|
101
|
+
const moduleEnt = moduleEntity(moduleAddress, entityId);
|
|
102
|
+
const replay = replayProtection || `0x${crypto.randomBytes(32).toString('hex')}`;
|
|
103
|
+
const coder = AbiCoder.defaultAbiCoder();
|
|
104
|
+
const tag = keccak256(toUtf8Bytes('AGENT_WALLET_SESSION_RUNTIME_V1'));
|
|
105
|
+
const payload = coder.encode(
|
|
106
|
+
['bytes32','uint256','address','address','uint32','address','uint256','bytes32','bytes32'],
|
|
107
|
+
[tag, BigInt(chainId), moduleAddress, account, entityId, sender, BigInt(value), keccak256(data), replay]
|
|
108
|
+
);
|
|
109
|
+
const payloadHash = keccak256(payload);
|
|
110
|
+
const signature = sessionWallet.signMessageSync ? sessionWallet.signMessageSync(getBytes(payloadHash)) : sessionWallet.signMessage(getBytes(payloadHash));
|
|
111
|
+
const moduleAuth = coder.encode(['address','bytes32','bytes'], [sessionWallet.address, replay, signature]);
|
|
112
|
+
const authorization = coder.encode(['bytes24','bytes'], [moduleEnt, moduleAuth]);
|
|
113
|
+
return { authorization, replayProtection: replay, signature, moduleEntity: moduleEnt };
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function executeWithRuntimeValidation({
|
|
117
|
+
rpcUrl,
|
|
118
|
+
tbaAddress,
|
|
119
|
+
moduleAddress,
|
|
120
|
+
entityId,
|
|
121
|
+
data,
|
|
122
|
+
value = 0n,
|
|
123
|
+
sessionWallet,
|
|
124
|
+
chainId,
|
|
125
|
+
}) {
|
|
126
|
+
const provider = new JsonRpcProvider(rpcUrl);
|
|
127
|
+
const wallet = sessionWallet.connect(provider);
|
|
128
|
+
const { authorization } = await buildRuntimeAuthorization({
|
|
129
|
+
moduleAddress,
|
|
130
|
+
account: tbaAddress,
|
|
131
|
+
entityId,
|
|
132
|
+
sender: wallet.address,
|
|
133
|
+
value,
|
|
134
|
+
data,
|
|
135
|
+
sessionWallet: wallet,
|
|
136
|
+
chainId,
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
const tba = new Contract(
|
|
140
|
+
tbaAddress,
|
|
141
|
+
['function executeWithRuntimeValidation(bytes data, bytes authorization) payable returns (bytes)'],
|
|
142
|
+
wallet
|
|
143
|
+
);
|
|
144
|
+
return tba.executeWithRuntimeValidation(data, authorization, { value });
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
module.exports = {
|
|
148
|
+
createSessionKey,
|
|
149
|
+
loadSessionWallet,
|
|
150
|
+
buildRuntimeAuthorization,
|
|
151
|
+
executeWithRuntimeValidation,
|
|
152
|
+
};
|
package/package.json
CHANGED
|
@@ -1,13 +1,15 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@equalfi/ski",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "EqualFi Session Key Installer (interactive CLI)",
|
|
5
5
|
"license": "MIT",
|
|
6
|
+
"main": "lib/index.js",
|
|
6
7
|
"bin": {
|
|
7
8
|
"ski": "bin/cli.js"
|
|
8
9
|
},
|
|
9
10
|
"files": [
|
|
10
11
|
"bin",
|
|
12
|
+
"lib",
|
|
11
13
|
"README.md"
|
|
12
14
|
],
|
|
13
15
|
"engines": {
|