@oracle-agent/oracle 0.3.1 → 0.3.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 CHANGED
@@ -18,18 +18,43 @@ alongside it — same split we run ourselves.
18
18
 
19
19
  ---
20
20
 
21
- ## Install
21
+ ## Start in 5 steps
22
+
23
+ Latest: **oracle `0.3.1`** · **operator `0.6.1`**
22
24
 
23
25
  ```bash
24
- # prepare-only (safe default)
25
- npm i @oracle-agent/oracle
26
+ # 1) install both packages
27
+ npm i @oracle-agent/oracle@0.3.1 @oracle-agent/operator@0.6.1
28
+
29
+ # 2) put keys ONLY on your machine (example: HL/EVM hex key file)
30
+ mkdir -p ~/.config/oracle/keys && chmod 700 ~/.config/oracle/keys
31
+ # write key, then: npx oracle-vault encrypt ~/.config/oracle/keys/evm.key
32
+ export HL_KEY_FILE=~/.config/oracle/keys/evm.key.vault.json
33
+ export ORACLE_VAULT_PASSPHRASE='...' # signer shell only — never in the agent chat
34
+ ```
35
+
36
+ ```js
37
+ // 3) prepare (no keys)
38
+ import { data } from "@oracle-agent/oracle";
39
+ const prepared = await data.call("hl-perps", "prepareOrder", { /* coin, side, size... */ });
40
+
41
+ // 4) keys local → operator signs (this is the sign step — no MetaMask)
42
+ import { hlSignAndSubmit } from "@oracle-agent/operator";
43
+ await hlSignAndSubmit(prepared, {
44
+ keyFile: process.env.HL_KEY_FILE,
45
+ userInitiated: true, // app confirmed the action; or use autonomous/daemon below
46
+ });
47
+ ```
26
48
 
27
- # full self-host lane (keys stay on YOUR machine) — same model we run
28
- npm i @oracle-agent/oracle @oracle-agent/operator
49
+ ```bash
50
+ # 5) unattended desk (no click each trade) — HL/Poly capped
51
+ export ORACLE_AUTONOMOUS_TRADING=1
52
+ # or: npx oracle-signer
29
53
  ```
30
54
 
31
- See the operator [SETUP](https://github.com/demi-hl/oracle-operator/blob/main/SETUP.md)
32
- for local key vault + Hyperliquid sign-and-submit.
55
+ Full vault / multi-chain / daemon detail → operator [SETUP](https://github.com/demi-hl/oracle-operator/blob/main/SETUP.md).
56
+
57
+ ---
33
58
 
34
59
  ## What Oracle is
35
60
 
package/SETUP.md CHANGED
@@ -293,3 +293,17 @@ npm run protocol:gate -- safe-erc20
293
293
 
294
294
  `prepareTemplateDeploy` will not stamp an unsigned deploy unless `forge test` passes.
295
295
  **Not a paid security-firm audit** — see each template `SECURITY.md`.
296
+
297
+
298
+ ## Optional keyed prepare stamps (hardening)
299
+
300
+ Integrity hash is always on. For a shared HMAC between prepare process and signer:
301
+
302
+ ```bash
303
+ # same secret on prepare host and signer host (not in the agent chat)
304
+ export ORACLE_STAMP_HMAC_SECRET='long-random-secret'
305
+ # signer-only: refuse envelopes without MAC
306
+ export ORACLE_STAMP_REQUIRE_MAC=1
307
+ ```
308
+
309
+ Default remains hash-only (public GO path). HMAC is opt-in hardening.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@oracle-agent/oracle",
3
- "version": "0.3.1",
3
+ "version": "0.3.2",
4
4
  "description": "Oracle: prepare-only multichain agent control plane. Policy-bounded intents for a user-signed wallet. Self-custody by default — the public package never takes your key. Built for Hermes; no model key required.",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -0,0 +1,12 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8"/>
5
+ <meta http-equiv="refresh" content="0; url=./oracle-splash/"/>
6
+ <link rel="canonical" href="./oracle-splash/"/>
7
+ <title>Oracle</title>
8
+ </head>
9
+ <body>
10
+ <p><a href="./oracle-splash/">Oracle splash</a></p>
11
+ </body>
12
+ </html>
@@ -146,6 +146,21 @@ export function verifyCapability(tag, { agentAddress, chainId = null, action = n
146
146
  * @returns {Promise<[string,string,string,string]>}
147
147
  */
148
148
  export async function mintCapability(ownerSigner, agentAddress, conditions = "") {
149
+ // Hardening: never accept a raw key string/hex. Real ethers Wallet/Signer objects are OK
150
+ // (they may expose .privateKey internally — that is the wallet, not a mistaken string).
151
+ if (typeof ownerSigner === "string" || typeof ownerSigner === "number") {
152
+ throw new Error("mintCapability refuses raw key material — pass an ethers Signer from the owner wallet");
153
+ }
154
+ if (!ownerSigner || typeof ownerSigner.signMessage !== "function" || typeof ownerSigner.getAddress !== "function") {
155
+ throw new Error("mintCapability: ownerSigner must implement getAddress() and signMessage()");
156
+ }
157
+ // Bare POJO that only carries a key field (no prototype signer) — refuse.
158
+ const proto = Object.getPrototypeOf(ownerSigner);
159
+ if (proto === Object.prototype || proto === null) {
160
+ if (ownerSigner.privateKey || ownerSigner.mnemonic) {
161
+ throw new Error("mintCapability refuses plain objects that only carry privateKey/mnemonic");
162
+ }
163
+ }
149
164
  parseConditions(conditions); // validate before signing
150
165
  const owner = await ownerSigner.getAddress();
151
166
  if (owner.toLowerCase() === String(agentAddress).toLowerCase()) {
package/src/index.mjs CHANGED
@@ -11,6 +11,7 @@ export {
11
11
  stampPrepared,
12
12
  assertPreparedEnvelope,
13
13
  computePrepareHash,
14
+ computePrepareMac,
14
15
  resolvePrepareMaxAgeMs,
15
16
  PREPARE_VERSION,
16
17
  PREPARE_MAX_AGE_MS,
@@ -1,20 +1,28 @@
1
1
  // Canonical prepare envelope shared by Oracle prepare helpers and the local operator.
2
- // The versioned checksum binds payload and freshness metadata. It detects mutation;
3
- // signer-owned policy, not this public checksum, authorizes execution.
2
+ // The versioned checksum binds payload and freshness metadata. It detects mutation.
3
+ // Optional HMAC (ORACLE_STAMP_HMAC_SECRET) adds keyed integrity when configured.
4
+ // Signer-owned policy, not this public checksum alone, authorizes execution.
4
5
 
5
- import { createHash } from "node:crypto";
6
+ import { createHash, createHmac, timingSafeEqual } from "node:crypto";
6
7
 
7
8
  export const PREPARE_VERSION = 2;
8
9
  /** Default max age for a prepared envelope at sign time (ms). */
9
10
  export const PREPARE_MAX_AGE_MS = 5 * 60 * 1000;
10
- /** Hard ceiling for authenticated envelope lifetime (ms). */
11
+ /**
12
+ * Hard ceiling for maxAgeMs. Caller/agent cannot widen past this.
13
+ * Override upward only via signer-process env ORACLE_PREPARE_HARD_MAX_AGE_MS.
14
+ */
11
15
  export const PREPARE_HARD_MAX_AGE_MS = (() => {
12
16
  const n = Number(process.env.ORACLE_PREPARE_HARD_MAX_AGE_MS);
13
- return Number.isFinite(n) && n > 0 ? n : 30 * 60 * 1000;
17
+ return Number.isFinite(n) && n > 0 ? n : 30 * 60 * 1000; // 30 minutes
14
18
  })();
15
19
 
20
+ /** Resolve max age: default 5m, never above hard max (even if caller passes MAX_SAFE_INTEGER). */
16
21
  export function resolvePrepareMaxAgeMs(requested) {
17
- const base = requested == null || requested === "" ? PREPARE_MAX_AGE_MS : Number(requested);
22
+ const base =
23
+ requested == null || requested === ""
24
+ ? PREPARE_MAX_AGE_MS
25
+ : Number(requested);
18
26
  if (!Number.isFinite(base) || base < 0) return PREPARE_MAX_AGE_MS;
19
27
  return Math.min(base, PREPARE_HARD_MAX_AGE_MS);
20
28
  }
@@ -26,11 +34,21 @@ function stableStringify(value) {
26
34
  return `{${keys.map((k) => `${JSON.stringify(k)}:${stableStringify(value[k])}`).join(",")}}`;
27
35
  }
28
36
 
37
+ function stampSecret(env = process.env) {
38
+ const s = String(env.ORACLE_STAMP_HMAC_SECRET || "").trim();
39
+ return s.length >= 16 ? s : "";
40
+ }
41
+
42
+ function requireMac(env = process.env) {
43
+ return String(env.ORACLE_STAMP_REQUIRE_MAC || "").trim() === "1";
44
+ }
45
+
29
46
  /** Fields that bind the envelope (must not include the stamp fields themselves). */
30
47
  export function prepareBodyForHash(prepared) {
31
48
  const {
32
49
  oraclePrepared,
33
50
  prepareHash,
51
+ prepareMac,
34
52
  note,
35
53
  ...body
36
54
  } = prepared || {};
@@ -42,11 +60,27 @@ export function computePrepareHash(prepared) {
42
60
  return createHash("sha256").update(stableStringify(body)).digest("hex");
43
61
  }
44
62
 
63
+ export function computePrepareMac(prepareHash, secret = stampSecret()) {
64
+ if (!secret) return "";
65
+ return createHmac("sha256", secret).update(String(prepareHash)).digest("hex");
66
+ }
67
+
68
+ function safeEqualHex(a, b) {
69
+ try {
70
+ const ba = Buffer.from(String(a), "hex");
71
+ const bb = Buffer.from(String(b), "hex");
72
+ if (ba.length === 0 || ba.length !== bb.length) return false;
73
+ return timingSafeEqual(ba, bb);
74
+ } catch {
75
+ return false;
76
+ }
77
+ }
78
+
45
79
  /**
46
80
  * Stamp a prepare result. Call at the end of every prepare helper.
47
81
  * @param {object} payload provider-specific prepare fields (action/tx/psbt/...)
48
82
  */
49
- export function stampPrepared(payload, { provider, kind } = {}) {
83
+ export function stampPrepared(payload, { provider, kind, env = process.env } = {}) {
50
84
  if (!payload || typeof payload !== "object") throw new Error("stampPrepared: payload object required");
51
85
  const base = {
52
86
  ...payload,
@@ -64,11 +98,15 @@ export function stampPrepared(payload, { provider, kind } = {}) {
64
98
  const prepareVersion = PREPARE_VERSION;
65
99
  const withMeta = { ...base, preparedAt, expiresAt, prepareVersion };
66
100
  const prepareHash = computePrepareHash(withMeta);
67
- return {
101
+ const secret = stampSecret(env);
102
+ const prepareMac = secret ? computePrepareMac(prepareHash, secret) : undefined;
103
+ const out = {
68
104
  ...withMeta,
69
105
  oraclePrepared: true,
70
106
  prepareHash,
71
107
  };
108
+ if (prepareMac) out.prepareMac = prepareMac;
109
+ return out;
72
110
  }
73
111
 
74
112
  /**
@@ -81,13 +119,14 @@ export function assertPreparedEnvelope(prepared, {
81
119
  providers = null,
82
120
  kinds = null,
83
121
  label = "operator",
122
+ env = process.env,
84
123
  } = {}) {
85
124
  if (!prepared || typeof prepared !== "object") {
86
125
  throw new Error(`${label}: prepared envelope object required`);
87
126
  }
88
127
  if (prepared.oraclePrepared !== true) {
89
128
  throw new Error(
90
- `${label}: prepared integrity envelope missing — use an @oracle-agent/oracle prepare result`
129
+ `${label}: prepared integrity envelope missing — use an @oracle-agent/oracle prepare result`,
91
130
  );
92
131
  }
93
132
  if (Number(prepared.prepareVersion) !== PREPARE_VERSION) {
@@ -97,6 +136,21 @@ export function assertPreparedEnvelope(prepared, {
97
136
  if (String(prepared.prepareHash || "") !== expected) {
98
137
  throw new Error(`${label}: prepareHash mismatch — payload was altered after prepare`);
99
138
  }
139
+
140
+ const secret = stampSecret(env);
141
+ const mac = String(prepared.prepareMac || "");
142
+ if (requireMac(env) || mac) {
143
+ if (!secret) {
144
+ throw new Error(
145
+ `${label}: prepareMac present or ORACLE_STAMP_REQUIRE_MAC=1 but ORACLE_STAMP_HMAC_SECRET missing in signer`,
146
+ );
147
+ }
148
+ const expectedMac = computePrepareMac(expected, secret);
149
+ if (!mac || !safeEqualHex(mac, expectedMac)) {
150
+ throw new Error(`${label}: prepareMac mismatch — stamp not from trusted preparer`);
151
+ }
152
+ }
153
+
100
154
  const preparedAt = Number(prepared.preparedAt);
101
155
  const expiresAt = Number(prepared.expiresAt);
102
156
  const effectiveMax = resolvePrepareMaxAgeMs(maxAgeMs);
@@ -112,7 +166,9 @@ export function assertPreparedEnvelope(prepared, {
112
166
  age > effectiveMax ||
113
167
  nowMs > expiresAt
114
168
  ) {
115
- throw new Error(`${label}: prepare envelope expired or clock-skewed (ageMs=${age}, max=${effectiveMax})`);
169
+ throw new Error(
170
+ `${label}: prepare envelope expired or clock-skewed (ageMs=${age}, max=${effectiveMax})`,
171
+ );
116
172
  }
117
173
  if (providers) {
118
174
  const allow = new Set(providers);