sigrank 0.0.178 → 0.0.179

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +100 -0
  2. package/MCP_SERVER_DESCRIPTION.md +141 -0
  3. package/README.md +8 -0
  4. package/adapters/index.mjs +865 -0
  5. package/adapters/tokenpull.mjs +576 -0
  6. package/adapters.mjs +1 -865
  7. package/analytics/cascade.mjs +274 -0
  8. package/cascade.mjs +1 -274
  9. package/cli.mjs +1 -1853
  10. package/connect.mjs +1 -13
  11. package/identity/connect.mjs +13 -0
  12. package/identity/keystore.mjs +242 -0
  13. package/identity/sign.mjs +106 -0
  14. package/index.mjs +29 -153
  15. package/keystore.mjs +1 -242
  16. package/narrate.mjs +1 -77
  17. package/package.json +29 -13
  18. package/preflight.mjs +1 -191
  19. package/presentation/cli.mjs +1874 -0
  20. package/presentation/narrate.mjs +77 -0
  21. package/presentation/tui.mjs +2668 -0
  22. package/prompts/check-my-efficiency.md +1 -0
  23. package/prompts/compare-with-leader.md +1 -0
  24. package/prompts/simulate-improvement.md +1 -0
  25. package/resources/class-tiers.md +23 -0
  26. package/resources/data-policy.md +27 -0
  27. package/resources/install-guide.md +59 -0
  28. package/resources/privacy-model.md +30 -0
  29. package/resources/scoring-formula.md +28 -0
  30. package/review.mjs +263 -0
  31. package/sign.mjs +1 -106
  32. package/submit/index.mjs +309 -0
  33. package/submit/preflight.mjs +191 -0
  34. package/submit.mjs +1 -303
  35. package/tokenpull.mjs +1 -576
  36. package/tools/_framing.mjs +139 -0
  37. package/tools/_helpers.mjs +258 -0
  38. package/tools/_report.mjs +262 -0
  39. package/tools/_schemas.mjs +394 -0
  40. package/tools/_verifiers.mjs +148 -0
  41. package/tools/compare-operators.mjs +79 -0
  42. package/tools/compare-self.mjs +122 -0
  43. package/tools/describe-power-user.mjs +68 -0
  44. package/tools/diagnose-cascade.mjs +192 -0
  45. package/tools/enroll.mjs +121 -0
  46. package/tools/get-best-operator.mjs +58 -0
  47. package/tools/get-leaderboard.mjs +26 -0
  48. package/tools/get-operator.mjs +33 -0
  49. package/tools/index.mjs +257 -0
  50. package/tools/optimize-efficiency.mjs +160 -0
  51. package/tools/rank-paste.mjs +51 -0
  52. package/tools/rank-windows.mjs +120 -0
  53. package/tools/self-improve.mjs +423 -0
  54. package/tools/simulate-change.mjs +207 -0
  55. package/tools/submit-paste.mjs +118 -0
  56. package/tools/submit-verified.mjs +153 -0
  57. package/tools/suggest-improvements.mjs +168 -0
  58. package/tools/tokenpull-compare.mjs +123 -0
  59. package/tools/tokenpull-submit.mjs +234 -0
  60. package/tools/tokenpull.mjs +113 -0
  61. package/tools/tokscale-breakdown.mjs +159 -0
  62. package/tools/tokscale-competitive-intel.mjs +45 -0
  63. package/tools/tokscale-cost-analysis.mjs +31 -0
  64. package/tools/tokscale-developer-profile.mjs +36 -0
  65. package/tools/tokscale-device-profile.mjs +35 -0
  66. package/tools/tokscale-market-share.mjs +38 -0
  67. package/tools/tokscale-mcp-usage.mjs +33 -0
  68. package/tools/tokscale-model-trends.mjs +32 -0
  69. package/tools/watch-tokenpull.mjs +134 -0
  70. package/tools.mjs +1 -3897
  71. package/tui.mjs +4 -2650
  72. package/banner.svg +0 -7
@@ -0,0 +1 @@
1
+ Pull my local token usage with tokenpull, compute my yield, and then fetch the leaderboard so I can see where I rank. Show me my class tier and my key metrics (Yield, Leverage, Velocity).
@@ -0,0 +1 @@
1
+ Fetch the SigRank leaderboard with get_leaderboard, then pull my local token usage with tokenpull. Compare my yield, leverage, and velocity against the top operator. What's the gap and what would I need to change to close it?
@@ -0,0 +1 @@
1
+ First pull my local token usage with tokenpull to get my current pillars. Then use simulate_change to answer: "what if I {{change}}?" Show me the yield delta and whether my class tier would change.
@@ -0,0 +1,23 @@
1
+ # SigRank Class Tiers
2
+
3
+ Every operator is classified into one of three tiers based on their Yield (Υ = Cache Reads × Output / Input²).
4
+
5
+ ## Burner (Υ < 1.0)
6
+ - **Profile:** Raw volume operators. High input tokens, low cache reuse.
7
+ - **Behavior:** Brute-force — lots of context fed in, relatively little output back.
8
+ - **Typical:** New AI users, verbose prompters, no session continuity.
9
+ - **Fix:** Build cache across sessions. Stop re-explaining context. Use --continue.
10
+
11
+ ## Builder (1.0 ≤ Υ < 10.0)
12
+ - **Profile:** Balanced operators. Moderate cache, decent output efficiency.
13
+ - **Behavior:** Productive — reasonable input-to-output ratio, some cache leverage.
14
+ - **Typical:** Experienced AI coders who use CLAUDE.md, project context, and session continuity.
15
+ - **Fix:** Increase cache reads by reusing sessions. Reduce input by trimming unnecessary context.
16
+
17
+ ## 10xer (Υ ≥ 10.0)
18
+ - **Profile:** Cascade-optimized operators. High cache reads, minimal input, efficient output.
19
+ - **Behavior:** Surgical — minimal new input, maximum cache reuse, high-yield output.
20
+ - **Typical:** Power users with long-running sessions, tight context windows, and aggressive cache strategies.
21
+ - **Maintain:** Keep cache hit rate high. Avoid context bloat. Monitor with watch_tokenpull.
22
+
23
+ Tiers are recalculated on every submission. Your tier can change between windows (7d, 30d, 90d, all-time).
@@ -0,0 +1,27 @@
1
+ # SigRank Data Policy
2
+
3
+ SigRank is a public AI-operator leaderboard. This policy summarizes what data we collect, how we use it, and how you can delete it.
4
+
5
+ ## What we collect
6
+
7
+ - **Token telemetry** from your local agent: token counts, content lengths, model identifiers, and platform/source tags. We use these to compute your score on the leaderboard.
8
+ - **Account identifiers** when you sign in (email from your OAuth provider) so we can link your operator profile to your login.
9
+ - **Optional profile fields** you choose to add: display name, handle, bio, location, links, avatar.
10
+
11
+ ## What we never collect
12
+
13
+ We do **not** read or transmit the content of your prompts or AI conversations. The local agent extracts only numeric token counts and metadata; transcripts never leave your device.
14
+
15
+ ## Consent
16
+
17
+ Before you connect a device, you must agree to the current Terms of Service and Privacy Policy. We record the timestamp and version of your consent.
18
+
19
+ ## Your choices
20
+
21
+ - **Pause data collection** at any time from [signalaf.com/settings](https://signalaf.com/settings). While paused, your agent submissions are rejected and no new telemetry is stored.
22
+ - **Delete your data** from Settings to erase all snapshots, scored metrics, ranks, devices, and enrollment codes while keeping your account and profile.
23
+ - **Delete your account** from Settings to fully sever your identity. This runs Delete your data first, then anonymizes your board row (kept as aggregate history), removes your login, and cancels billing.
24
+
25
+ ## Contact
26
+
27
+ Questions: hello@signalaf.com
@@ -0,0 +1,59 @@
1
+ # SigRank Quick Start
2
+
3
+ ## Install
4
+
5
+ ```bash
6
+ npx sigrank
7
+ ```
8
+
9
+ No global install needed. The MCP server starts automatically when an AI client (Claude, Cursor, Cline) connects.
10
+
11
+ ## First Submission
12
+
13
+ 1. **Enroll** — register your operator codename:
14
+ ```bash
15
+ npx sigrank enroll
16
+ ```
17
+
18
+ 2. **Pull + Submit** — scan your local AI session logs and publish:
19
+ ```bash
20
+ npx sigrank submit
21
+ ```
22
+
23
+ 3. **Dry Run** — see exactly what would be sent before publishing:
24
+ ```bash
25
+ npx sigrank submit --dry-run
26
+ ```
27
+
28
+ ## Token Pull Sources
29
+
30
+ SigRank bundles two token readers as npm dependencies:
31
+ - **ccusage** — Claude Code session logs
32
+ - **tokscale** — multi-platform token telemetry
33
+
34
+ A third source, **token-dashboard**, is read from `~/.claude/token-dashboard.db` (SQLite).
35
+ That DB is created by [Nate's token-dashboard](https://github.com/nateherkai/token-dashboard) (Python):
36
+ ```bash
37
+ git clone https://github.com/nateherkai/token-dashboard.git
38
+ cd token-dashboard
39
+ python3 cli.py dashboard
40
+ ```
41
+ Run the scan once to create the DB; SigRank reads it directly via sqlite3 after that.
42
+
43
+ ## MCP Client Setup
44
+
45
+ Add to your MCP client config:
46
+ ```json
47
+ {
48
+ "mcpServers": {
49
+ "sigrank": {
50
+ "command": "npx",
51
+ "args": ["sigrank"]
52
+ }
53
+ }
54
+ }
55
+ ```
56
+
57
+ ## Live Board
58
+
59
+ Visit https://signalaf.com to see the global leaderboard.
@@ -0,0 +1,30 @@
1
+ # SigRank Privacy Model
2
+
3
+ ## What leaves your machine
4
+
5
+ **Only four numbers per submission:**
6
+ - Input tokens
7
+ - Output tokens
8
+ - Cache create tokens
9
+ - Cache read tokens
10
+
11
+ **That's it.** No prompts, no code, no file contents, no conversation text.
12
+
13
+ ## How it works
14
+
15
+ 1. **Local-first:** All token pulling happens on your machine. SigRank reads session logs from ~/.claude, ~/.codex, ~/.local/share/amp, etc.
16
+ 2. **Token counts only:** The MCP tools extract integer counts from log metadata. The actual content of your conversations is never read, parsed, or transmitted.
17
+ 3. **Signed submission:** Submissions are ed25519-signed. The board verifies authenticity without seeing your data.
18
+ 4. **No auth required:** No API keys, no OAuth, no account needed to read the leaderboard. Enrollment only requires a codename.
19
+
20
+ ## What SigRank can NOT see
21
+
22
+ - Your prompts or messages
23
+ - Your code or file contents
24
+ - Your tool calls or their results
25
+ - Which AI platform you use (beyond token counts)
26
+ - Your identity (only your chosen codename)
27
+
28
+ ## Verification
29
+
30
+ 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,28 @@
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 Tier Thresholds
21
+
22
+ | Tier | Yield Range | Profile |
23
+ |------|-------------|---------|
24
+ | **Burner** | Υ < 1.0 | Raw volume — high input, low cache reuse, brute-force output |
25
+ | **Builder** | 1.0 ≤ Υ < 10.0 | Balanced — moderate cache, decent output efficiency |
26
+ | **10xer** | Υ ≥ 10.0 | Cascade-optimized — high cache reads, minimal input, efficient output |
27
+
28
+ 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
- // sign.mjs — canonical JSON + ed25519 signing for SigRank signed snapshot submit.
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';