sigrank 0.19.0 → 1.0.1
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/CHANGELOG.md +158 -0
- package/MCP_SERVER_DESCRIPTION.md +172 -0
- package/README.md +89 -41
- package/adapters/index.mjs +1100 -0
- package/adapters/tokenpull.mjs +568 -0
- package/adapters.mjs +1 -865
- package/analytics/cascade.mjs +338 -0
- package/cascade.mjs +1 -274
- package/cli.mjs +1 -1853
- package/connect.mjs +1 -13
- package/identity/connect.mjs +13 -0
- package/identity/keystore.mjs +231 -0
- package/identity/sign.mjs +106 -0
- package/index.mjs +47 -162
- package/keystore.mjs +1 -242
- package/lib/constants.mjs +137 -0
- package/lib/pkg-version.mjs +59 -0
- package/manifest.json +46 -0
- package/narrate.mjs +1 -77
- package/package.json +41 -16
- package/preflight.mjs +1 -191
- package/presentation/cli.mjs +1926 -0
- package/presentation/narrate.mjs +219 -0
- package/presentation/tui-themes.mjs +189 -0
- package/presentation/tui.mjs +3316 -0
- package/prompts/check-my-efficiency.md +1 -0
- package/prompts/compare-with-leader.md +1 -0
- package/prompts/simulate-improvement.md +1 -0
- package/proxy.mjs +492 -0
- package/resources/class-tiers.md +81 -0
- package/resources/data-policy.md +31 -0
- package/resources/install-guide.md +59 -0
- package/resources/privacy-model.md +44 -0
- package/resources/scoring-formula.md +24 -0
- package/review.mjs +263 -0
- package/sign.mjs +1 -106
- package/submit/index.mjs +297 -0
- package/submit/preflight.mjs +191 -0
- package/submit.mjs +1 -303
- package/tokenpull.mjs +1 -576
- package/tokscale_analytics.mjs +864 -0
- package/tools/_framing.mjs +217 -0
- package/tools/_helpers.mjs +253 -0
- package/tools/_peer-matching.mjs +361 -0
- package/tools/_report.mjs +262 -0
- package/tools/_schemas.mjs +492 -0
- package/tools/_verifiers.mjs +148 -0
- package/tools/compare-operators.mjs +80 -0
- package/tools/compare-self.mjs +124 -0
- package/tools/describe-power-user.mjs +78 -0
- package/tools/diagnose-cascade.mjs +192 -0
- package/tools/discover-peers.mjs +198 -0
- package/tools/enroll.mjs +119 -0
- package/tools/get-best-operator.mjs +59 -0
- package/tools/get-leaderboard.mjs +27 -0
- package/tools/get-operator.mjs +33 -0
- package/tools/index.mjs +229 -0
- package/tools/optimize-efficiency.mjs +170 -0
- package/tools/rank-paste.mjs +51 -0
- package/tools/rank-windows.mjs +120 -0
- package/tools/self-improve.mjs +423 -0
- package/tools/simulate-change.mjs +207 -0
- package/tools/submit-paste.mjs +121 -0
- package/tools/submit-verified.mjs +153 -0
- package/tools/suggest-improvements.mjs +168 -0
- package/tools/tokenpull-compare.mjs +123 -0
- package/tools/tokenpull-submit.mjs +235 -0
- package/tools/tokenpull.mjs +113 -0
- package/tools/tokscale-breakdown.mjs +119 -0
- package/tools/tokscale-competitive-intel.mjs +45 -0
- package/tools/tokscale-cost-analysis.mjs +31 -0
- package/tools/tokscale-developer-profile.mjs +36 -0
- package/tools/tokscale-device-profile.mjs +35 -0
- package/tools/tokscale-market-share.mjs +38 -0
- package/tools/tokscale-mcp-usage.mjs +33 -0
- package/tools/tokscale-model-trends.mjs +32 -0
- package/tools/watch-tokenpull.mjs +134 -0
- package/tools.mjs +1 -3897
- package/tui.mjs +4 -2650
- package/banner.svg +0 -7
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# SigRank Privacy Model
|
|
2
|
+
|
|
3
|
+
## What leaves your machine
|
|
4
|
+
|
|
5
|
+
**Every submission path sends only four numbers:**
|
|
6
|
+
- Input tokens
|
|
7
|
+
- Output tokens
|
|
8
|
+
- Cache create tokens
|
|
9
|
+
- Cache read tokens
|
|
10
|
+
|
|
11
|
+
That's the entire payload — no prompts, no code, no file contents, no conversation text. The MCP parses your input locally and sends only the four token counts to the server, which re-parses and re-scores them authoritatively.
|
|
12
|
+
|
|
13
|
+
The optional `sigrank proxy` is a local transport path to Anthropic/OpenAI, not a
|
|
14
|
+
submission path to SigRank. When explicitly enabled, it necessarily receives and
|
|
15
|
+
forwards provider-bound API keys, prompts, tool calls, and responses in memory.
|
|
16
|
+
It does not persist that content or send it to the SigRank service; it appends
|
|
17
|
+
only provider-reported token counts, model/backend metadata, and timestamps to
|
|
18
|
+
`~/.sigrank-mcp/proxy-sessions.jsonl`.
|
|
19
|
+
|
|
20
|
+
- **Signed path** (`submit_verified`, `watch_tokenpull` with `submit:true`): the four numbers travel with the device's public key, codename, window, and an ed25519 signature. The board verifies the signature without seeing your data.
|
|
21
|
+
- **Paste path** (`submit_paste`, `tokenpull_submit`): the MCP parses your pasted token counts locally, then sends only the four canonical numbers to the server's web-paste endpoint. Even if you paste mixed text (prose + numbers), only the extracted token counts are transmitted — the raw text never leaves your machine.
|
|
22
|
+
|
|
23
|
+
## How it works
|
|
24
|
+
|
|
25
|
+
1. **Local-first:** All token pulling happens on your machine. SigRank reads session logs from ~/.claude, ~/.codex, ~/.local/share/amp, etc. The optional proxy also runs only on loopback and only when manually started.
|
|
26
|
+
2. **Token-only persistence:** Local-log adapters extract integer counts from log metadata without reading conversation content. The optional proxy forwards request/response bytes transiently but persists only usage metadata. On the paste path, the MCP parses the paste locally and sends only the four extracted numbers — the raw paste text stays on your machine.
|
|
27
|
+
3. **Signed submission:** Ranked submissions (`submit_verified`, `watch_tokenpull` with `submit:true`) are ed25519-signed with a device-bound key generated locally at enrollment. The board verifies the signature without seeing your data. Paste submissions (`submit_paste`, `tokenpull_submit`) are unsigned and go through the web-paste endpoint with a codename only — but still send only the four token counts, not the raw paste.
|
|
28
|
+
4. **Read tools need no auth:** No API keys, no OAuth, no account needed to read the leaderboard or operator profiles. **Enrollment requires a connect code** from signalaf.com → Settings → New key (the code binds your device's public key to your operator server-side); a codename alone is not enough for the signed/ranked path.
|
|
29
|
+
|
|
30
|
+
## What the SigRank service can NOT see
|
|
31
|
+
|
|
32
|
+
- Your prompts or messages
|
|
33
|
+
- Your code or file contents
|
|
34
|
+
- Your tool calls or their results
|
|
35
|
+
- Which AI platform you use (beyond token counts)
|
|
36
|
+
- Your identity (only your chosen codename)
|
|
37
|
+
|
|
38
|
+
The local proxy process is different from the SigRank service: if you opt in, it
|
|
39
|
+
handles your provider traffic in memory solely to forward it to Anthropic or
|
|
40
|
+
OpenAI. That content is never stored in the proxy JSONL file.
|
|
41
|
+
|
|
42
|
+
## Verification
|
|
43
|
+
|
|
44
|
+
The submit_verified tool uses ed25519 signing. The board's source_attestations table records the signature for audit. You can verify your own submissions via get_operator.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# SigRank Scoring Formula
|
|
2
|
+
|
|
3
|
+
## Yield (Υ) — the headline metric
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
Υ = Cache Reads × Output / Input²
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
Yield rewards operators who maximize output while minimizing input — and who build cache to amortize that input across sessions. A high yield means you're getting more done with less, efficiently.
|
|
10
|
+
|
|
11
|
+
## Derived Metrics
|
|
12
|
+
|
|
13
|
+
| Metric | Formula | Meaning |
|
|
14
|
+
|--------|---------|---------|
|
|
15
|
+
| **SNR** | Output / (Input + CacheCreate) | Signal-to-noise: how much of your token spend is productive output vs. overhead |
|
|
16
|
+
| **Leverage** | Cache Reads / Input | How well you reuse cached context — higher = better cache utilization |
|
|
17
|
+
| **Velocity** | Output / Input | Raw output efficiency — how much output you generate per token of input |
|
|
18
|
+
| **10xDEV** | Composite score | Weighted blend of yield, leverage, and velocity for cross-platform comparison |
|
|
19
|
+
|
|
20
|
+
## Class Tiers
|
|
21
|
+
|
|
22
|
+
Class tiers are based on **total tokens accumulated**, not yield ranges. The 8-tier experience ladder (descending: ARCH+, ARCH, POWER, BASE, SEEKER, REFINER, BEARER, IGNITER) measures operator progression. TRANSMITTER is a separate peak badge (K.00), not a ladder tier. See `class-tiers.md` for the full ladder and thresholds.
|
|
23
|
+
|
|
24
|
+
The formula is deterministic and computed locally. No network calls needed for scoring.
|
package/review.mjs
ADDED
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review.mjs — `sigrank review` subcommand.
|
|
3
|
+
*
|
|
4
|
+
* Runs an operator's numbers through all three reference operating ratios
|
|
5
|
+
* (AA avg, HCM, Codex PU) and validates results to identify correct placement.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* npx sigrank review <handle> # pull from raw data (if available)
|
|
9
|
+
* npx sigrank review --output N --cache-read N --combined-input N
|
|
10
|
+
*
|
|
11
|
+
* The cache_write convergence test:
|
|
12
|
+
* When we split combined_input into input + cache_write using each ratio's
|
|
13
|
+
* velocity, the cache_write values should converge. If one ratio produces
|
|
14
|
+
* a cache_write far below the others, that ratio is broken for this operator.
|
|
15
|
+
*
|
|
16
|
+
* Transcribed by Devin from owner (djm) analysis and direction.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { cascade, classify, round } from "./cascade.mjs";
|
|
20
|
+
|
|
21
|
+
// Reference operating ratios (cache_read : input : output, input=1)
|
|
22
|
+
const RATIOS = {
|
|
23
|
+
"AA avg": {
|
|
24
|
+
cache: 3.5,
|
|
25
|
+
velocity: 0.5,
|
|
26
|
+
label: "3.5:1:0.5",
|
|
27
|
+
description: "all-users average",
|
|
28
|
+
},
|
|
29
|
+
HCM: {
|
|
30
|
+
cache: 20,
|
|
31
|
+
velocity: 0.1,
|
|
32
|
+
label: "20:1:0.1",
|
|
33
|
+
description: "human center of mass",
|
|
34
|
+
},
|
|
35
|
+
"Codex PU": {
|
|
36
|
+
cache: 243,
|
|
37
|
+
velocity: 1.03,
|
|
38
|
+
label: "243:1:1.03",
|
|
39
|
+
description: "Codex power-user",
|
|
40
|
+
},
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
function fmtTokens(n) {
|
|
44
|
+
if (n >= 1e12) return `${(n / 1e12).toFixed(2)}T`;
|
|
45
|
+
if (n >= 1e9) return `${(n / 1e9).toFixed(2)}B`;
|
|
46
|
+
if (n >= 1e6) return `${(n / 1e6).toFixed(1)}M`;
|
|
47
|
+
if (n >= 1e3) return `${(n / 1e3).toFixed(1)}K`;
|
|
48
|
+
return String(n);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function runRatios(output, cacheRead, combinedInput) {
|
|
52
|
+
const results = {};
|
|
53
|
+
|
|
54
|
+
for (const [name, r] of Object.entries(RATIOS)) {
|
|
55
|
+
const v = r.velocity;
|
|
56
|
+
const cRef = r.cache;
|
|
57
|
+
|
|
58
|
+
const inputEst = Math.round(output / v);
|
|
59
|
+
const cacheWriteEst = combinedInput - inputEst;
|
|
60
|
+
|
|
61
|
+
const leverage = cacheRead / inputEst;
|
|
62
|
+
const velocity = v;
|
|
63
|
+
const yieldVal = leverage * velocity;
|
|
64
|
+
|
|
65
|
+
results[name] = {
|
|
66
|
+
label: r.label,
|
|
67
|
+
description: r.description,
|
|
68
|
+
refCache: cRef,
|
|
69
|
+
refVelocity: v,
|
|
70
|
+
input: inputEst,
|
|
71
|
+
output,
|
|
72
|
+
cacheWrite: cacheWriteEst,
|
|
73
|
+
cacheRead,
|
|
74
|
+
leverage,
|
|
75
|
+
velocity,
|
|
76
|
+
yield: yieldVal,
|
|
77
|
+
levMatch: leverage / cRef,
|
|
78
|
+
cwValid: cacheWriteEst > 0,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
// Convergence test
|
|
83
|
+
const cwValues = Object.values(results)
|
|
84
|
+
.filter((r) => r.cwValid)
|
|
85
|
+
.map((r) => r.cacheWrite);
|
|
86
|
+
|
|
87
|
+
for (const [name, r] of Object.entries(results)) {
|
|
88
|
+
if (!r.cwValid) {
|
|
89
|
+
r.cwStatus = "INVALID (negative)";
|
|
90
|
+
r.cwConverges = false;
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
const others = Object.entries(results)
|
|
94
|
+
.filter(([n]) => n !== name && results[n].cwValid)
|
|
95
|
+
.map(([, v]) => v.cacheWrite);
|
|
96
|
+
if (others.length > 0) {
|
|
97
|
+
const otherMedian = [...others].sort((a, b) => a - b)[
|
|
98
|
+
Math.floor(others.length / 2)
|
|
99
|
+
];
|
|
100
|
+
if (r.cacheWrite < otherMedian * 0.5) {
|
|
101
|
+
r.cwStatus = `OUTLIER (cache_write ${fmtTokens(r.cacheWrite)} is >50% below peers)`;
|
|
102
|
+
r.cwConverges = false;
|
|
103
|
+
} else if (r.cacheWrite < otherMedian * 0.7) {
|
|
104
|
+
r.cwStatus = `LOW (cache_write ${fmtTokens(r.cacheWrite)} is below peers)`;
|
|
105
|
+
r.cwConverges = false;
|
|
106
|
+
} else {
|
|
107
|
+
r.cwStatus = "VALID (converges with peers)";
|
|
108
|
+
r.cwConverges = true;
|
|
109
|
+
}
|
|
110
|
+
} else {
|
|
111
|
+
r.cwStatus = "VALID";
|
|
112
|
+
r.cwConverges = true;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return results;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function classifyYield(y) {
|
|
120
|
+
if (y >= 100) return "POWER";
|
|
121
|
+
if (y >= 10) return "SEEKER";
|
|
122
|
+
if (y >= 1) return "IGNITER";
|
|
123
|
+
if (y > 0) return "BURNER";
|
|
124
|
+
return "BASE";
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// ANSI colors
|
|
128
|
+
const bold = (s) => `\x1b[1m${s}\x1b[0m`;
|
|
129
|
+
const dim = (s) => `\x1b[2m${s}\x1b[0m`;
|
|
130
|
+
const green = (s) => `\x1b[32m${s}\x1b[0m`;
|
|
131
|
+
const red = (s) => `\x1b[31m${s}\x1b[0m`;
|
|
132
|
+
const yellow = (s) => `\x1b[33m${s}\x1b[0m`;
|
|
133
|
+
const cyan = (s) => `\x1b[36m${s}\x1b[0m`;
|
|
134
|
+
|
|
135
|
+
export async function runReview(opts) {
|
|
136
|
+
const { handle, output, cacheRead, combinedInput } = opts;
|
|
137
|
+
|
|
138
|
+
if (!output || !cacheRead || !combinedInput) {
|
|
139
|
+
console.error(
|
|
140
|
+
red(
|
|
141
|
+
"\n Missing required values. Usage:\n" +
|
|
142
|
+
" npx sigrank review --output N --cache-read N --combined-input N\n",
|
|
143
|
+
),
|
|
144
|
+
);
|
|
145
|
+
process.exit(1);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const results = runRatios(output, cacheRead, combinedInput);
|
|
149
|
+
|
|
150
|
+
console.log();
|
|
151
|
+
console.log(` ${"=".repeat(80)}`);
|
|
152
|
+
console.log(` RATIO REVIEW${handle ? `: ${handle}` : " (custom)"}`);
|
|
153
|
+
console.log(` ${"=".repeat(80)}`);
|
|
154
|
+
console.log();
|
|
155
|
+
|
|
156
|
+
// Real telemetry
|
|
157
|
+
console.log(` ${bold("REAL TELEMETRY (constants):")}`);
|
|
158
|
+
console.log(` Output: ${output.toLocaleString()} (${fmtTokens(output)})`);
|
|
159
|
+
console.log(` Cache read: ${cacheRead.toLocaleString()} (${fmtTokens(cacheRead)})`);
|
|
160
|
+
console.log(` Combined input: ${combinedInput.toLocaleString()} (${fmtTokens(combinedInput)})`);
|
|
161
|
+
console.log();
|
|
162
|
+
console.log(` ${bold("REFERENCE RATIOS (cache_read : input : output, input=1):")}`);
|
|
163
|
+
for (const [name, r] of Object.entries(RATIOS)) {
|
|
164
|
+
console.log(` ${name.padEnd(10)} ${r.label.padEnd(12)} (${r.description})`);
|
|
165
|
+
}
|
|
166
|
+
console.log();
|
|
167
|
+
|
|
168
|
+
// Side-by-side table
|
|
169
|
+
const names = ["AA avg", "HCM", "Codex PU"];
|
|
170
|
+
const headerRow = ` ${"".padEnd(14)} ${"AA avg".padStart(14)} ${"HCM".padStart(14)} ${"Codex PU".padStart(14)}`;
|
|
171
|
+
const subHeaderRow = ` ${"".padEnd(14)} ${"3.5:1:0.5".padStart(14)} ${"20:1:0.1".padStart(14)} ${"243:1:1.03".padStart(14)}`;
|
|
172
|
+
const divider = ` ${"-".repeat(14)} ${"-".repeat(14)} ${"-".repeat(14)} ${"-".repeat(14)}`;
|
|
173
|
+
|
|
174
|
+
console.log(headerRow);
|
|
175
|
+
console.log(subHeaderRow);
|
|
176
|
+
console.log(divider);
|
|
177
|
+
|
|
178
|
+
const dataRows = [
|
|
179
|
+
["Input", "input", "int"],
|
|
180
|
+
["Output", "output", "int"],
|
|
181
|
+
["Cache write", "cacheWrite", "int"],
|
|
182
|
+
["Cache read", "cacheRead", "int"],
|
|
183
|
+
["Velocity", "velocity", "float"],
|
|
184
|
+
["Leverage", "leverage", "lev"],
|
|
185
|
+
["Yield", "yield", "float"],
|
|
186
|
+
];
|
|
187
|
+
|
|
188
|
+
for (const [label, key, type] of dataRows) {
|
|
189
|
+
const vals = names.map((n) => {
|
|
190
|
+
const v = results[n][key];
|
|
191
|
+
if (type === "float") return v.toFixed(2).padStart(14);
|
|
192
|
+
if (type === "lev") return `${v.toFixed(1)}:1`.padStart(14);
|
|
193
|
+
return v.toLocaleString().padStart(14);
|
|
194
|
+
});
|
|
195
|
+
console.log(` ${label.padEnd(14)} ${vals[0]} ${vals[1]} ${vals[2]}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
console.log(divider);
|
|
199
|
+
const classes = names.map((n) => classifyYield(results[n].yield).padStart(14));
|
|
200
|
+
console.log(` ${"Class".padEnd(14)} ${classes[0]} ${classes[1]} ${classes[2]}`);
|
|
201
|
+
console.log();
|
|
202
|
+
|
|
203
|
+
// Cache write validation
|
|
204
|
+
console.log(` ${bold("CACHE WRITE VALIDATION:")}`);
|
|
205
|
+
console.log();
|
|
206
|
+
for (const name of names) {
|
|
207
|
+
const r = results[name];
|
|
208
|
+
const marker = r.cwConverges ? green("PASS") : red("FAIL");
|
|
209
|
+
console.log(` ${name.padEnd(10)} cache_write = ${fmtTokens(r.cacheWrite).padStart(10)} [${marker}] ${r.cwStatus}`);
|
|
210
|
+
}
|
|
211
|
+
console.log();
|
|
212
|
+
|
|
213
|
+
// Leverage match
|
|
214
|
+
console.log(` ${bold("LEVERAGE MATCH (actual cache_read/input vs reference cache term):")}`);
|
|
215
|
+
console.log();
|
|
216
|
+
for (const name of names) {
|
|
217
|
+
const r = results[name];
|
|
218
|
+
let matchStatus;
|
|
219
|
+
if (r.levMatch < 0.5) matchStatus = "POOR (ratio does not fit this operator)";
|
|
220
|
+
else if (r.levMatch < 0.8 || r.levMatch > 2.0) matchStatus = "WEAK (leverage far from reference)";
|
|
221
|
+
else if (r.levMatch >= 0.8 && r.levMatch <= 1.3) matchStatus = "STRONG (leverage matches reference)";
|
|
222
|
+
else matchStatus = "MODERATE";
|
|
223
|
+
console.log(
|
|
224
|
+
` ${name.padEnd(10)} actual ${r.leverage.toFixed(1)}:1 vs ref ${r.refCache.toFixed(1)}:1 = ${r.levMatch.toFixed(2)}x [${matchStatus}]`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
console.log();
|
|
228
|
+
|
|
229
|
+
// Recommendation
|
|
230
|
+
const valid = Object.entries(results).filter(([, r]) => r.cwConverges);
|
|
231
|
+
const validNames = valid.map(([n]) => n);
|
|
232
|
+
|
|
233
|
+
console.log(` ${bold("RECOMMENDATION:")}`);
|
|
234
|
+
console.log();
|
|
235
|
+
|
|
236
|
+
if (valid.length === 0) {
|
|
237
|
+
console.log(` ${red("No ratio passes cache_write validation. Manual review needed.")}`);
|
|
238
|
+
} else if (valid.length === 1) {
|
|
239
|
+
const [name, r] = valid[0];
|
|
240
|
+
console.log(` ${green(name)} (${r.label}) is the only ratio that passes cache_write validation.`);
|
|
241
|
+
console.log(` Yield: ${r.yield.toFixed(2)} Class: ${classifyYield(r.yield)}`);
|
|
242
|
+
} else {
|
|
243
|
+
const eliminated = names.filter((n) => !validNames.includes(n));
|
|
244
|
+
if (eliminated.length > 0) {
|
|
245
|
+
console.log(` ${dim("Eliminated:")} ${eliminated.join(", ")} (cache_write outlier)`);
|
|
246
|
+
}
|
|
247
|
+
console.log(` ${dim("Candidates:")} ${validNames.join(", ")}`);
|
|
248
|
+
console.log();
|
|
249
|
+
|
|
250
|
+
// Best leverage match
|
|
251
|
+
const [bestName, best] = valid.reduce((best, cur) =>
|
|
252
|
+
Math.abs(cur[1].levMatch - 1.0) < Math.abs(best[1].levMatch - 1.0) ? cur : best
|
|
253
|
+
);
|
|
254
|
+
|
|
255
|
+
console.log(` ${green("Best fit:")} ${bestName} (${best.label})`);
|
|
256
|
+
console.log(` Leverage match: ${best.levMatch.toFixed(2)}x (actual ${best.leverage.toFixed(1)}:1 vs ref ${best.refCache.toFixed(1)}:1)`);
|
|
257
|
+
console.log(` Yield: ${best.yield.toFixed(2)} Class: ${classifyYield(best.yield)}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
console.log();
|
|
261
|
+
console.log(` Run ${cyan("`npx sigrank`")} to get a real signed snapshot with proper cache_write.`);
|
|
262
|
+
console.log();
|
|
263
|
+
}
|
package/sign.mjs
CHANGED
|
@@ -1,106 +1 @@
|
|
|
1
|
-
|
|
2
|
-
//
|
|
3
|
-
// BYTE-COMPATIBLE PORT of sigrank-app lib/ingest/signature.ts. This is the highest-
|
|
4
|
-
// risk file in the client: the server VERIFIES the signature over ITS recomputation
|
|
5
|
-
// of these exact canonical bytes, so a single divergent byte hard-rejects (422) every
|
|
6
|
-
// verified submission. Keep this a line-for-line mirror of the server canonicalizer:
|
|
7
|
-
// canonical JSON = recursively sorted keys, compact separators, UTF-8, with the
|
|
8
|
-
// derived agent.signature + agent.snapshot_hash stripped before serialization.
|
|
9
|
-
// snapshot_hash = "sha256:" + hex(sha256(canonical_bytes)).
|
|
10
|
-
// public key = "ed25519:<base64 of the 32 raw verify-key bytes>".
|
|
11
|
-
// signature = base64 of the 64-byte ed25519 signature over canonical_bytes.
|
|
12
|
-
// The canon-parity fixture (tests/fixtures/canon_parity.json) + sign.test.mjs + the
|
|
13
|
-
// sigrank-app canon_parity gate together lock this byte-for-byte.
|
|
14
|
-
|
|
15
|
-
import {
|
|
16
|
-
createHash,
|
|
17
|
-
createPrivateKey,
|
|
18
|
-
createPublicKey,
|
|
19
|
-
sign as edSign,
|
|
20
|
-
verify as edVerify,
|
|
21
|
-
} from "node:crypto";
|
|
22
|
-
|
|
23
|
-
/** Agent fields excluded from the canonical (signed/hashed) body — they derive from it. */
|
|
24
|
-
const DERIVED_AGENT_FIELDS = ["signature", "snapshot_hash"];
|
|
25
|
-
|
|
26
|
-
/** Recursively sort object keys (matches the server's Object.keys(...).sort()); arrays keep order. */
|
|
27
|
-
function sortDeep(v) {
|
|
28
|
-
if (Array.isArray(v)) return v.map(sortDeep);
|
|
29
|
-
if (v && typeof v === "object") {
|
|
30
|
-
const out = {};
|
|
31
|
-
for (const k of Object.keys(v).sort()) out[k] = sortDeep(v[k]);
|
|
32
|
-
return out;
|
|
33
|
-
}
|
|
34
|
-
return v;
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
/** Canonical JSON string the agent signs/hashes (derived agent fields stripped). */
|
|
38
|
-
export function canonicalJson(payload) {
|
|
39
|
-
const clone = JSON.parse(JSON.stringify(payload));
|
|
40
|
-
const agent = clone.agent;
|
|
41
|
-
if (agent && typeof agent === "object") {
|
|
42
|
-
for (const f of DERIVED_AGENT_FIELDS) delete agent[f];
|
|
43
|
-
}
|
|
44
|
-
return JSON.stringify(sortDeep(clone));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** UTF-8 bytes of the canonical JSON — the exact bytes that get signed. */
|
|
48
|
-
export function canonicalBytes(payload) {
|
|
49
|
-
return Buffer.from(canonicalJson(payload), "utf-8");
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** "sha256:<hex>" over the canonical bytes (matches the server snapshotHash). */
|
|
53
|
-
export function snapshotHash(payload) {
|
|
54
|
-
return `sha256:${createHash("sha256").update(canonicalBytes(payload)).digest("hex")}`;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
/** SPKI DER prefix for a raw 32-byte ed25519 public key. */
|
|
58
|
-
const ED25519_SPKI_PREFIX = Buffer.from("302a300506032b6570032100", "hex");
|
|
59
|
-
|
|
60
|
-
/** Build a node KeyObject from an "ed25519:<base64>" (or bare base64) public key, or null. */
|
|
61
|
-
function publicKeyFrom(pk) {
|
|
62
|
-
try {
|
|
63
|
-
const body = pk.startsWith("ed25519:") ? pk.slice("ed25519:".length) : pk;
|
|
64
|
-
const raw = Buffer.from(body, "base64");
|
|
65
|
-
if (raw.length !== 32) return null;
|
|
66
|
-
return createPublicKey({
|
|
67
|
-
key: Buffer.concat([ED25519_SPKI_PREFIX, raw]),
|
|
68
|
-
format: "der",
|
|
69
|
-
type: "spki",
|
|
70
|
-
});
|
|
71
|
-
} catch {
|
|
72
|
-
return null;
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Sign the payload's canonical bytes with the PKCS8-DER-base64 private key → base64 sig.
|
|
78
|
-
* The returned string is the X-Agent-Signature header value.
|
|
79
|
-
*/
|
|
80
|
-
export function signPayload(payload, privateKeyPkcs8B64) {
|
|
81
|
-
const key = createPrivateKey({
|
|
82
|
-
key: Buffer.from(privateKeyPkcs8B64, "base64"),
|
|
83
|
-
format: "der",
|
|
84
|
-
type: "pkcs8",
|
|
85
|
-
});
|
|
86
|
-
return edSign(null, canonicalBytes(payload), key).toString("base64");
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Local verify (parity with the server's verifySignature) — used by the self-test
|
|
91
|
-
* and as a pre-send sanity check. Returns false on any malformation, never throws.
|
|
92
|
-
*/
|
|
93
|
-
export function verifyPayload(payload, signatureB64, publicKey) {
|
|
94
|
-
const key = publicKeyFrom(publicKey);
|
|
95
|
-
if (!key) return false;
|
|
96
|
-
try {
|
|
97
|
-
return edVerify(
|
|
98
|
-
null,
|
|
99
|
-
canonicalBytes(payload),
|
|
100
|
-
key,
|
|
101
|
-
Buffer.from(signatureB64, "base64"),
|
|
102
|
-
);
|
|
103
|
-
} catch {
|
|
104
|
-
return false;
|
|
105
|
-
}
|
|
106
|
-
}
|
|
1
|
+
export * from './identity/sign.mjs';
|