sememe 0.0.0-stage → 0.4.22
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/LICENSE +21 -0
- package/README.md +2777 -2
- package/bin/sememe.mjs +556 -0
- package/index.mjs +48 -0
- package/interface/ata-ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL.json +62 -0
- package/interface/native-11111111111111111111111111111111.json +226 -0
- package/interface/native-ComputeBudget111111111111111111111111111111.json +72 -0
- package/interface/raydium-675kPX9MHTjS2zt1qfr1NYHuzeLXfQM9H24wFSUt1Mp8.json +352 -0
- package/interface/token-TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA.json +334 -0
- package/interface/token-TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb.json +535 -0
- package/interface/token-metadata.json +132 -0
- package/package.json +71 -4
- package/src/account-list.mjs +308 -0
- package/src/account-union.mjs +523 -0
- package/src/account.mjs +199 -0
- package/src/acquire.mjs +145 -0
- package/src/adopt.mjs +206 -0
- package/src/ata-interface.mjs +276 -0
- package/src/ata.mjs +223 -0
- package/src/base58.mjs +96 -0
- package/src/canonical.mjs +24 -0
- package/src/cli-surface.mjs +300 -0
- package/src/codec.mjs +1118 -0
- package/src/conformance.mjs +431 -0
- package/src/corpus/harvest-compute-budget-2026-10-31.json +19122 -0
- package/src/corpus/harvest-system-2026-10-31.json +26090 -0
- package/src/decode.mjs +286 -0
- package/src/dialect.mjs +180 -0
- package/src/discriminators.mjs +192 -0
- package/src/drift.mjs +172 -0
- package/src/gap.mjs +205 -0
- package/src/lighthouse.mjs +168 -0
- package/src/lock.mjs +440 -0
- package/src/native-audit.mjs +56 -0
- package/src/native-corpus.mjs +57 -0
- package/src/native-interface.mjs +252 -0
- package/src/native.mjs +324 -0
- package/src/peer.mjs +95 -0
- package/src/program-id.mjs +74 -0
- package/src/pull.mjs +117 -0
- package/src/rawle.mjs +454 -0
- package/src/raydium-interface.mjs +747 -0
- package/src/raydium.mjs +391 -0
- package/src/receipt-digest.mjs +84 -0
- package/src/receipt-name.mjs +128 -0
- package/src/receipt-order.mjs +56 -0
- package/src/shank.mjs +73 -0
- package/src/short-forms.mjs +209 -0
- package/src/store-read.mjs +253 -0
- package/src/store-write.mjs +46 -0
- package/src/token-interface.mjs +394 -0
- package/src/token-metadata-interface.mjs +371 -0
- package/src/token-metadata.mjs +317 -0
- package/src/token.mjs +443 -0
- package/src/union.mjs +428 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/LICENSE.txt +201 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/compute-budget-interface/src/lib.rs +93 -0
- package/vendor/anza-solana-sdk/8db406e2edfafa25939ec782b77b5b6f30de164d/system-interface/src/instruction.rs +1899 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/LICENSE +21 -0
- package/vendor/lighthouse/4c579479c98635e419b1b167f08be02a71604a71/lighthouse.json +2769 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/LICENSE +201 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/instruction.rs +1189 -0
- package/vendor/raydium-amm/91739d8d9f5cf347e0ce14ebe4d2bb7069fb31b6/program/src/state.rs +1348 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/LICENSE +201 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/instruction.rs +169 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/interface/src/lib.rs +12 -0
- package/vendor/spl-associated-token-account/fbf5379e7398ac8b231c73ee8a9ff677b43dd0ec/program/src/processor.rs +308 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/LICENSE.txt +176 -0
- package/vendor/spl-token/737788ff5bcc3f1f44cfb222f25483187650be76/interface/src/instruction.rs +1963 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/LICENSE.txt +176 -0
- package/vendor/spl-token-2022/5251dcc07ecbcb721d6c4f0231090defadf00311/interface/src/instruction.rs +2868 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/LICENSE +201 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/error.rs +58 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/instruction.rs +596 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/lib.rs +21 -0
- package/vendor/spl-token-metadata-interface/7038969ec74a89f1afdb9bc97c112d4d26fead9a/interface/src/state.rs +229 -0
package/src/lock.mjs
ADDED
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
// sememe.lock — the fingerprint lockfile the README's contract promised and
|
|
2
|
+
// the tree never shipped.
|
|
3
|
+
//
|
|
4
|
+
// What this module is NOT: not distribution, not an update path, not a second
|
|
5
|
+
// acquisition channel. It records what a store ANSWERED with, so a project
|
|
6
|
+
// can commit one document that says "my decode calls answered with these
|
|
7
|
+
// fingerprints" and have CI refuse silent drift. The store reader
|
|
8
|
+
// (src/store-read.mjs) already refuses a tampered object on every read; the
|
|
9
|
+
// lock adds the cross-time check no single read can make: the store's answer
|
|
10
|
+
// is still THE answer it was pinned at.
|
|
11
|
+
//
|
|
12
|
+
// The document is canonical JSON with a trailing newline (the same
|
|
13
|
+
// serialization `pull`/`adopt` fingerprint their objects with), so diffs are
|
|
14
|
+
// stable and the file is reproducible from the store alone. Refusals are
|
|
15
|
+
// named, never guessed around:
|
|
16
|
+
// lock-v1-parse the file is not JSON
|
|
17
|
+
// lock-v1-shape the document is not a v1 lock (programs map etc.)
|
|
18
|
+
// lock-v1-fingerprint a pinned fingerprint is not sha256:<64 hex>
|
|
19
|
+
// lock-pins-nothing the lock pins ZERO programs, so it can never detect
|
|
20
|
+
// drift — a green gate over a check that never ran
|
|
21
|
+
// (measured 2026-11-09: `sememe lock` on a store with
|
|
22
|
+
// no receipts wrote `{"lockVersion":1,"programs":{}}`
|
|
23
|
+
// and exited 0, and `sememe verify --mode error` over
|
|
24
|
+
// that lock exited 0 with `pinned: 0, violations: 0`
|
|
25
|
+
// against a real 18-program store)
|
|
26
|
+
// Violations checkLock reports (each with the program named):
|
|
27
|
+
// drifted the store answers the program with a DIFFERENT fingerprint
|
|
28
|
+
// missing the store has no verified receipt for a pinned program
|
|
29
|
+
// unverified an object fails the store reader's own re-hash check. The
|
|
30
|
+
// entry carries a `scope`: `pinned` (the lock names this
|
|
31
|
+
// program) or `extra` (the lock does NOT — the store gained the
|
|
32
|
+
// program later, and its receipt is damaged). The scope is part
|
|
33
|
+
// of the repair, so the two are never reported alike.
|
|
34
|
+
// relabelled a pinned or stored DECLARED origin changed after the fact —
|
|
35
|
+
// the receipt says "declared" about bytes whose label no longer
|
|
36
|
+
// matches what was pinned (see src/receipt-digest.mjs)
|
|
37
|
+
// unbound the store declares an origin the lock does NOT pin, so the
|
|
38
|
+
// gate cannot say whether that label changed. A lock written
|
|
39
|
+
// before origin pinning (every existing user's committed lock)
|
|
40
|
+
// pins no origin; a legacy receipt carries no self-digest, so
|
|
41
|
+
// nothing covers the label at all. Reported instead of passing,
|
|
42
|
+
// because "verified" over a claim that was never checked is the
|
|
43
|
+
// green lie this gate exists to prevent. Repair: `sememe lock`.
|
|
44
|
+
// Extra store programs are reported, never a violation: the lock pins
|
|
45
|
+
// minimums, not store equality — a pull that ADDS a program must not fail
|
|
46
|
+
// someone else's CI. writeLock refuses to write a lock over a store it cannot
|
|
47
|
+
// fully verify: every receipt is re-checked through the store reader first,
|
|
48
|
+
// so a lock is a snapshot of VERIFIED answers, never of filenames. The lock
|
|
49
|
+
// pins each program's DECLARED origin next to its fingerprint: the fingerprint
|
|
50
|
+
// binds the bytes, the origin pins the label the store answered with, and
|
|
51
|
+
// neither may change silently.
|
|
52
|
+
|
|
53
|
+
import { writeFile, readFile, readdir } from 'node:fs/promises';
|
|
54
|
+
import path from 'node:path';
|
|
55
|
+
import { canonicalJson } from './canonical.mjs';
|
|
56
|
+
import { readVerified } from './store-read.mjs';
|
|
57
|
+
import { compareReceiptNames } from './receipt-order.mjs';
|
|
58
|
+
import { classifyReceiptFiles, isReceiptFile } from './receipt-name.mjs';
|
|
59
|
+
|
|
60
|
+
const LOCK_VERSION = 1;
|
|
61
|
+
const HEX64 = /^[a-f0-9]{64}$/;
|
|
62
|
+
|
|
63
|
+
/** The program a receipt filename belongs to. Base58 program ids contain no
|
|
64
|
+
* `-`, so the first dash-terminated run is the program; the source label may
|
|
65
|
+
* contain dashes, the fingerprint never does (it is the last segment). */
|
|
66
|
+
function programOfReceiptName(name) {
|
|
67
|
+
return name.split('-')[0];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/** Newest-selection rule, identical to `findStoredIdl`: sort by the ONE
|
|
71
|
+
* shared ordering (numeric slot, filename tiebreak), take last. A plain
|
|
72
|
+
* string sort here would pin "99999999" over "123456789" — two answers from
|
|
73
|
+
* one store, measured 2026-11-07 (test/slot-order-store.test.mjs O2). */
|
|
74
|
+
async function newestReceipt(storeDir, program) {
|
|
75
|
+
const receipts = path.join(storeDir, 'receipts');
|
|
76
|
+
const names = (await readdir(receipts)).filter((name) => name.startsWith(`${program}-`) && isReceiptFile(name));
|
|
77
|
+
if (!names.length) return null;
|
|
78
|
+
names.sort(compareReceiptNames);
|
|
79
|
+
return readVerified(storeDir, program, names[names.length - 1]);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/** Every receipt of `program`, re-checked through the store reader.
|
|
83
|
+
*
|
|
84
|
+
* Measured 2026-11-07 (this host): a store may hold SEVERAL receipts for one
|
|
85
|
+
* program — a pull plus a published copy, or two published copies. `writeLock`
|
|
86
|
+
* re-checked only the NEWEST of them, so a DAMAGED second copy was skipped in
|
|
87
|
+
* silence: `sememe lock` exited 0 and `sememe verify` reported 0 violations
|
|
88
|
+
* over a store whose union decode refused to decode. The README's claim
|
|
89
|
+
* ("every receipt is re-checked through the store reader first, so a store the
|
|
90
|
+
* reader refuses produces NO lock, never a lock over damage") was therefore
|
|
91
|
+
* true of one receipt out of several.
|
|
92
|
+
*
|
|
93
|
+
* Every receipt is checked here, so a lock is a snapshot of VERIFIED answers
|
|
94
|
+
* over the WHOLE store, not of the receipt that happened to sort last.
|
|
95
|
+
*
|
|
96
|
+
* ONE helper for BOTH lock paths on purpose: `writeLock` (refuse to write) and
|
|
97
|
+
* `checkLock` (name the damage) must agree about what a clean store is. Two
|
|
98
|
+
* separate loops is how the store ended up "locked clean" and "verified
|
|
99
|
+
* damaged" in the same run. */
|
|
100
|
+
async function verifyEveryReceipt(storeDir, program, names) {
|
|
101
|
+
for (const name of names) {
|
|
102
|
+
try {
|
|
103
|
+
await readVerified(storeDir, program, name);
|
|
104
|
+
} catch (error) {
|
|
105
|
+
// The failure KIND decides which bucket checkLock reports it in: a
|
|
106
|
+
// rewritten declared origin is a relabel, not an unreadable object.
|
|
107
|
+
return {
|
|
108
|
+
ok: false,
|
|
109
|
+
receipt: name,
|
|
110
|
+
reason: String(error?.message ?? error).slice(0, 300),
|
|
111
|
+
code: error?.code ?? null,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return { ok: true, receipt: null, reason: null, code: null };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Snapshot every verified receipt of `storeDir` into a lock document.
|
|
119
|
+
* Returns the document; `outPath` receives exactly its canonical bytes. */
|
|
120
|
+
export async function writeLock(storeDir, outPath) {
|
|
121
|
+
const receipts = path.join(storeDir, 'receipts');
|
|
122
|
+
let names;
|
|
123
|
+
try { names = await readdir(receipts); }
|
|
124
|
+
catch (error) {
|
|
125
|
+
if (error?.code === 'ENOENT') {
|
|
126
|
+
throw Error(`no sememe store at ${storeDir}: run "sememe pull" first`);
|
|
127
|
+
}
|
|
128
|
+
throw error;
|
|
129
|
+
}
|
|
130
|
+
const programs = [...new Set(names.filter(isReceiptFile).map(programOfReceiptName))].sort();
|
|
131
|
+
// A lock that pins NOTHING is not a lock: it can never detect a drifted
|
|
132
|
+
// fingerprint, a missing receipt or a rewritten origin. Measured
|
|
133
|
+
// 2026-11-09: `sememe lock` over a store whose receipts/ directory holds
|
|
134
|
+
// no receipt wrote `{"lockVersion":1,"programs":{}}` and exited 0 — and
|
|
135
|
+
// `sememe verify --mode error` over that lock exited 0 with
|
|
136
|
+
// `pinned: 0, violations: 0` against a REAL 18-program store, reporting
|
|
137
|
+
// every program as an `extra` (extras are never violations). The gate was
|
|
138
|
+
// green over a check that never ran, and it LOOKED informative. So a lock
|
|
139
|
+
// over an empty store is REFUSED and no file is written: the repair is to
|
|
140
|
+
// pull a program first, not to commit a lock that certifies nothing.
|
|
141
|
+
if (programs.length === 0) {
|
|
142
|
+
throw Error(
|
|
143
|
+
`the store at ${storeDir} pins nothing: it holds no receipt, so a lock over it could never detect drift `
|
|
144
|
+
+ `(lock-pins-nothing) — run "sememe pull <program-id> --store ${storeDir}" first, then lock again`,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
const doc = { lockVersion: LOCK_VERSION, programs: {} };
|
|
148
|
+
for (const program of programs) {
|
|
149
|
+
// Full verification through the store reader: a tampered object or a
|
|
150
|
+
// dangling receipt must fail the LOCK, not silently enter it — and that
|
|
151
|
+
// holds for EVERY receipt of the program, not only the newest one.
|
|
152
|
+
const candidates = names.filter((name) => name.startsWith(`${program}-`) && isReceiptFile(name));
|
|
153
|
+
const verdict = await verifyEveryReceipt(storeDir, program, candidates);
|
|
154
|
+
if (!verdict.ok) {
|
|
155
|
+
// Refuse the lock: a store the reader refuses must produce NO lock.
|
|
156
|
+
throw Error(verdict.reason);
|
|
157
|
+
}
|
|
158
|
+
const { idl, receipt, receiptName } = await newestReceipt(storeDir, program);
|
|
159
|
+
doc.programs[program] = {
|
|
160
|
+
fingerprint: receipt.fingerprint,
|
|
161
|
+
source: receipt.source ?? 'unknown',
|
|
162
|
+
slot: receipt.slot ?? null,
|
|
163
|
+
receipt: receiptName,
|
|
164
|
+
instructions: Array.isArray(idl.instructions) ? idl.instructions.length : 0,
|
|
165
|
+
// The DECLARED origin, pinned beside the fingerprint. Absent for an
|
|
166
|
+
// on-chain receipt (it names no file) and for a legacy adopted receipt
|
|
167
|
+
// without one; `null` never appears — a pinned origin is a string.
|
|
168
|
+
...(receipt.origin === undefined || receipt.origin === null ? {} : {
|
|
169
|
+
origin: receipt.origin,
|
|
170
|
+
originStatus: receipt.originStatus ?? 'declared',
|
|
171
|
+
}),
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
const bytes = Buffer.from(`${canonicalJson(doc)}\n`);
|
|
175
|
+
if (outPath !== undefined && outPath !== null) {
|
|
176
|
+
await writeFile(outPath, bytes);
|
|
177
|
+
}
|
|
178
|
+
return doc;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/** Parse and shape-check a lock document. Every failure names its reason. */
|
|
182
|
+
export async function readLock(lockPath) {
|
|
183
|
+
let raw;
|
|
184
|
+
try { raw = await readFile(lockPath, 'utf8'); }
|
|
185
|
+
catch (error) {
|
|
186
|
+
throw Error(`sememe lock file ${lockPath} is unreadable: ${String(error?.message ?? error).slice(0, 140)}`);
|
|
187
|
+
}
|
|
188
|
+
let doc;
|
|
189
|
+
try { doc = JSON.parse(raw); }
|
|
190
|
+
catch {
|
|
191
|
+
throw Error(`sememe lock file ${lockPath} is not JSON (lock-v1-parse)`);
|
|
192
|
+
}
|
|
193
|
+
if (!doc || typeof doc !== 'object' || Array.isArray(doc)) {
|
|
194
|
+
throw Error(`sememe lock file ${lockPath} is not a lock document (lock-v1-shape)`);
|
|
195
|
+
}
|
|
196
|
+
if (doc.lockVersion !== LOCK_VERSION) {
|
|
197
|
+
throw Error(`sememe lock file ${lockPath} has unsupported lockVersion ${JSON.stringify(doc.lockVersion)} — expected ${LOCK_VERSION} (lock-v1-shape)`);
|
|
198
|
+
}
|
|
199
|
+
if (!doc.programs || typeof doc.programs !== 'object' || Array.isArray(doc.programs)) {
|
|
200
|
+
throw Error(`sememe lock file ${lockPath} has no programs map (lock-v1-shape)`);
|
|
201
|
+
}
|
|
202
|
+
// A lock that pins ZERO programs certifies nothing: there is no pinned
|
|
203
|
+
// answer a drifted store could contradict, so every store would pass it.
|
|
204
|
+
// Measured 2026-11-09 (this host, published v0.4.13 AND main): an empty
|
|
205
|
+
// `programs` map made `sememe verify --mode error` exit 0 with
|
|
206
|
+
// `pinned: 0, violations: 0` against a real 18-program store — a green CI
|
|
207
|
+
// gate over a check that never ran. The refusal is about the DOCUMENT, not
|
|
208
|
+
// about the store, so it applies in `--mode warn` too: warn mode exists to
|
|
209
|
+
// downgrade STORE violations, not to certify a lock that pins nothing.
|
|
210
|
+
if (Object.keys(doc.programs).length === 0) {
|
|
211
|
+
throw Error(
|
|
212
|
+
`sememe lock file ${lockPath} pins no program (lock-pins-nothing): a lock over zero answers can never `
|
|
213
|
+
+ `detect drift — re-create it with "sememe lock" over a store that holds at least one receipt`,
|
|
214
|
+
);
|
|
215
|
+
}
|
|
216
|
+
for (const [program, entry] of Object.entries(doc.programs)) {
|
|
217
|
+
if (!entry || typeof entry !== 'object') {
|
|
218
|
+
throw Error(`sememe lock file ${lockPath} pins ${program} with a non-object entry (lock-v1-shape)`);
|
|
219
|
+
}
|
|
220
|
+
const fingerprint = typeof entry.fingerprint === 'string' ? entry.fingerprint.replace(/^sha256:/, '') : '';
|
|
221
|
+
if (!HEX64.test(fingerprint)) {
|
|
222
|
+
throw Error(`sememe lock file ${lockPath} pins ${program} with a malformed fingerprint ${JSON.stringify(entry.fingerprint)} (lock-v1-fingerprint)`);
|
|
223
|
+
}
|
|
224
|
+
if (entry.origin !== undefined && typeof entry.origin !== 'string') {
|
|
225
|
+
throw Error(`sememe lock file ${lockPath} pins ${program} with a malformed origin ${JSON.stringify(entry.origin)} (lock-v1-origin)`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return doc;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Re-check a store against a lock document. Returns the full report.
|
|
232
|
+
* `mode: 'error'` (the CI-gate default) throws after the report is complete
|
|
233
|
+
* when any violation exists; `mode: 'warn'` never throws on violations. */
|
|
234
|
+
export async function checkLock(storeDir, lockPath, { mode = 'error' } = {}) {
|
|
235
|
+
const doc = await readLock(lockPath);
|
|
236
|
+
const pinnedPrograms = Object.keys(doc.programs).sort();
|
|
237
|
+
|
|
238
|
+
let receiptNames = [];
|
|
239
|
+
try { receiptNames = (await readdir(path.join(storeDir, 'receipts'))); }
|
|
240
|
+
catch (error) {
|
|
241
|
+
if (error?.code === 'ENOENT') {
|
|
242
|
+
throw Error(`no sememe store at ${storeDir}: run "sememe pull" first`);
|
|
243
|
+
}
|
|
244
|
+
throw error;
|
|
245
|
+
}
|
|
246
|
+
// The same ONE ordering the writer and both readers use, so "newest" means
|
|
247
|
+
// the same receipt on every side of a lock comparison.
|
|
248
|
+
// Stray files (sememe's own uncommitted .tmp- debris from a crash, or a
|
|
249
|
+
// file that carries no slot segment) are partitioned off: they are not
|
|
250
|
+
// receipts and reading them as receipts would fail the gate over a clean
|
|
251
|
+
// store (measured 2026-11-07). Malformed receipts (a slot segment but bad
|
|
252
|
+
// hex/suffix) stay in `receiptNames` and are refused by name — they are
|
|
253
|
+
// damage to the store, not uncommitted writes.
|
|
254
|
+
const { receipts: validReceipts, stray } = classifyReceiptFiles(receiptNames);
|
|
255
|
+
validReceipts.sort(compareReceiptNames);
|
|
256
|
+
const storePrograms = [...new Set(validReceipts.map(programOfReceiptName))].sort();
|
|
257
|
+
|
|
258
|
+
const report = {
|
|
259
|
+
lock: path.resolve(lockPath),
|
|
260
|
+
store: path.resolve(storeDir),
|
|
261
|
+
mode,
|
|
262
|
+
pinned: pinnedPrograms.length,
|
|
263
|
+
verified: [],
|
|
264
|
+
drifted: [],
|
|
265
|
+
missing: [],
|
|
266
|
+
unverified: [],
|
|
267
|
+
// A declared origin that is no longer the one that was pinned, or a
|
|
268
|
+
// receipt whose own declared label no longer matches its digest.
|
|
269
|
+
relabelled: [],
|
|
270
|
+
// A declared origin the lock does not pin at all: the gate cannot
|
|
271
|
+
// certify a label it never recorded.
|
|
272
|
+
unbound: [],
|
|
273
|
+
extra: storePrograms.filter((program) => !doc.programs[program]),
|
|
274
|
+
// Non-receipt files found in receipts/ (sememe's own .tmp- crash debris,
|
|
275
|
+
// foreign files): reported by name and kind, never a violation — an
|
|
276
|
+
// uncommitted write is safe to ignore.
|
|
277
|
+
stray,
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
for (const program of pinnedPrograms) {
|
|
281
|
+
const pinnedFingerprint = doc.programs[program].fingerprint.replace(/^sha256:/, '');
|
|
282
|
+
const candidates = validReceipts.filter((name) => name.startsWith(`${program}-`));
|
|
283
|
+
if (!candidates.length) {
|
|
284
|
+
report.missing.push(program);
|
|
285
|
+
continue;
|
|
286
|
+
}
|
|
287
|
+
const newest = candidates[candidates.length - 1];
|
|
288
|
+
// Every receipt of this program is re-checked, not only the newest: a
|
|
289
|
+
// damaged SECOND copy is a damaged store, and reporting it as clean is the
|
|
290
|
+
// green lie this gate exists to prevent. The SAME helper the writer uses,
|
|
291
|
+
// so "lock" and "verify" cannot disagree about the store.
|
|
292
|
+
const verdict = await verifyEveryReceipt(storeDir, program, candidates);
|
|
293
|
+
if (!verdict.ok) {
|
|
294
|
+
if (verdict.code === 'receipt-relabelled') {
|
|
295
|
+
// The declared origin in the receipt no longer matches its own digest:
|
|
296
|
+
// the label was rewritten after the receipt was written. Reported as
|
|
297
|
+
// its own violation kind so a caller can see that the BYTES are fine
|
|
298
|
+
// and the PROVENANCE claim is not.
|
|
299
|
+
report.relabelled.push({ program, receipt: verdict.receipt, reason: verdict.reason, scope: 'receipt' });
|
|
300
|
+
} else {
|
|
301
|
+
report.unverified.push({ program, receipt: verdict.receipt, reason: verdict.reason });
|
|
302
|
+
}
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
let source;
|
|
306
|
+
try {
|
|
307
|
+
source = await readVerified(storeDir, program, newest);
|
|
308
|
+
} catch (error) {
|
|
309
|
+
if (error?.code === 'receipt-relabelled') {
|
|
310
|
+
report.relabelled.push({
|
|
311
|
+
program,
|
|
312
|
+
receipt: newest,
|
|
313
|
+
reason: String(error?.message ?? error).slice(0, 300),
|
|
314
|
+
scope: 'receipt',
|
|
315
|
+
});
|
|
316
|
+
} else {
|
|
317
|
+
report.unverified.push({ program, receipt: newest, reason: String(error?.message ?? error).slice(0, 200) });
|
|
318
|
+
}
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
const storeFingerprint = source.receipt.fingerprint.replace(/^sha256:/, '');
|
|
322
|
+
if (storeFingerprint !== pinnedFingerprint) {
|
|
323
|
+
report.drifted.push({
|
|
324
|
+
program,
|
|
325
|
+
pinned: `sha256:${pinnedFingerprint}`,
|
|
326
|
+
store: `sha256:${storeFingerprint}`,
|
|
327
|
+
storeReceipt: newest,
|
|
328
|
+
});
|
|
329
|
+
continue;
|
|
330
|
+
}
|
|
331
|
+
// Fingerprint matches: now the DECLARED ORIGIN must still be the pinned
|
|
332
|
+
// one. The receipt's own digests catch an edited receipt; this catches a
|
|
333
|
+
// receipt that was rewritten AND re-digested after the lock was written —
|
|
334
|
+
// the layer a self-digest alone cannot cover, because the attacker holds
|
|
335
|
+
// the same hashing function.
|
|
336
|
+
const pinnedOrigin = doc.programs[program].origin;
|
|
337
|
+
const storeOrigin = source.receipt.origin ?? null;
|
|
338
|
+
if (typeof pinnedOrigin === 'string') {
|
|
339
|
+
if (storeOrigin !== pinnedOrigin) {
|
|
340
|
+
report.relabelled.push({
|
|
341
|
+
program,
|
|
342
|
+
receipt: newest,
|
|
343
|
+
pinned: pinnedOrigin,
|
|
344
|
+
store: storeOrigin,
|
|
345
|
+
reason: `the store no longer declares origin ${JSON.stringify(pinnedOrigin)} for ${program} — the store answers ${storeOrigin === null ? 'no origin' : JSON.stringify(storeOrigin)}`,
|
|
346
|
+
scope: 'lock',
|
|
347
|
+
});
|
|
348
|
+
continue;
|
|
349
|
+
}
|
|
350
|
+
} else if (storeOrigin !== null) {
|
|
351
|
+
// The store answers with a DECLARED origin this lock does not pin. For
|
|
352
|
+
// a legacy receipt there is no self-digest either, so this label is
|
|
353
|
+
// covered by nothing: a rewrite of it would look exactly like the
|
|
354
|
+
// untouched case. Say so, and name the one command that binds it —
|
|
355
|
+
// writeLock already pins `receipt.origin`, so the repair really works.
|
|
356
|
+
report.unbound.push({
|
|
357
|
+
program,
|
|
358
|
+
receipt: newest,
|
|
359
|
+
store: storeOrigin,
|
|
360
|
+
reason: `the store declares origin ${JSON.stringify(storeOrigin)} for ${program}, but sememe.lock does not pin it — this lock predates origin pinning, so a rewritten origin here cannot be detected; re-run "sememe lock" to bind it`,
|
|
361
|
+
scope: 'lock',
|
|
362
|
+
});
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
365
|
+
report.verified.push(program);
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// EXTRA programs are re-checked too. The lock pins MINIMUMS, so a program
|
|
369
|
+
// the store gained is still only reported (presence is never a violation —
|
|
370
|
+
// that contract is G1 in test/extra-program-damage.test.mjs and must not
|
|
371
|
+
// move). But DAMAGE is not presence: measured 2026-11-07 on this tree
|
|
372
|
+
// (pre-fix 61df87b), a store whose LATER-PULLED extra program carried an
|
|
373
|
+
// object that no longer re-hashed to its receipt gave
|
|
374
|
+
//
|
|
375
|
+
// sememe lock -> EXIT 1, no lock written (writeLock re-checks extras)
|
|
376
|
+
// sememe verify -> EXIT 0, violations 0 (checkLock never read them)
|
|
377
|
+
//
|
|
378
|
+
// Two commands, two verdicts about ONE store, and a green CI gate over a
|
|
379
|
+
// program the store reader refuses: `sememe decode` on that same program
|
|
380
|
+
// exits 1 with "object and receipt disagree, refusing to decode". The
|
|
381
|
+
// shared helper existed; only the PINNED loop ever called it — the same
|
|
382
|
+
// "one store, two strengths" class this repo killed for the newest-receipt
|
|
383
|
+
// path on 2026-11-07, one level down.
|
|
384
|
+
//
|
|
385
|
+
// So extras run through the SAME helper, and only their FAILURES are
|
|
386
|
+
// reported (as `unverified`/`relabelled` with scope `extra`, so pin drift is
|
|
387
|
+
// not misread as the cause). What is NOT checked for an extra: binding it
|
|
388
|
+
// against the lock's origin pin — an unpinned program has no pin to bind
|
|
389
|
+
// to, and demanding one would turn "someone pulled a program" into a CI
|
|
390
|
+
// failure, which is exactly the equality rule the lock rejects.
|
|
391
|
+
for (const program of report.extra) {
|
|
392
|
+
const candidates = validReceipts.filter((name) => name.startsWith(`${program}-`));
|
|
393
|
+
if (!candidates.length) continue; // no receipt left to check
|
|
394
|
+
const verdict = await verifyEveryReceipt(storeDir, program, candidates);
|
|
395
|
+
if (verdict.ok) continue;
|
|
396
|
+
const entry = {
|
|
397
|
+
program,
|
|
398
|
+
receipt: verdict.receipt,
|
|
399
|
+
reason: verdict.reason,
|
|
400
|
+
scope: 'extra',
|
|
401
|
+
};
|
|
402
|
+
if (verdict.code === 'receipt-relabelled') {
|
|
403
|
+
report.relabelled.push({ ...entry, scope: 'extra' });
|
|
404
|
+
} else {
|
|
405
|
+
report.unverified.push(entry);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
const violations = report.drifted.length + report.missing.length
|
|
410
|
+
+ report.unverified.length + report.relabelled.length + report.unbound.length;
|
|
411
|
+
report.violations = violations;
|
|
412
|
+
if (mode === 'error' && violations > 0) {
|
|
413
|
+
const parts = [];
|
|
414
|
+
if (report.drifted.length) {
|
|
415
|
+
parts.push(`drifted: ${report.drifted.map((d) => `${d.program} pinned ${d.pinned} vs store ${d.store}`).join('; ')}`);
|
|
416
|
+
}
|
|
417
|
+
if (report.missing.length) parts.push(`missing: ${report.missing.join(', ')}`);
|
|
418
|
+
if (report.unverified.length) {
|
|
419
|
+
// The scope is part of the repair: `extra` means the LOCK is fine and
|
|
420
|
+
// the STORE holds a damaged receipt for a program the lock never pinned
|
|
421
|
+
// (re-pull or drop that receipt) — while a pinned name means the pin no
|
|
422
|
+
// longer describes the store (re-run `sememe lock`). One list, two very
|
|
423
|
+
// different repairs; the refusal must not make them look alike.
|
|
424
|
+
// For an extra, the RECEIPT is named too: it is the file in the store
|
|
425
|
+
// that has to be re-pulled, and its name carries the fingerprint the
|
|
426
|
+
// receipt claims — the number a reader needs to look the damage up.
|
|
427
|
+
parts.push(`unverified: ${report.unverified.map((u) => (u.scope === 'extra'
|
|
428
|
+
? `${u.program} (extra, receipt ${u.receipt})`
|
|
429
|
+
: u.program)).join(', ')}`);
|
|
430
|
+
}
|
|
431
|
+
if (report.relabelled.length) {
|
|
432
|
+
parts.push(`relabelled (declared origin changed): ${report.relabelled.map((r) => (r.scope === 'extra' ? `${r.program} (extra)` : r.program)).join(', ')}`);
|
|
433
|
+
}
|
|
434
|
+
if (report.unbound.length) {
|
|
435
|
+
parts.push(`unbound (declared origin not pinned by the lock): ${report.unbound.map((r) => r.program).join(', ')} — re-run "sememe lock"`);
|
|
436
|
+
}
|
|
437
|
+
throw Error(`store does not match sememe.lock (${report.drifted.length} drifted, ${report.missing.length} missing, ${report.unverified.length} unverified, ${report.relabelled.length} relabelled, ${report.unbound.length} unbound) — ${parts.join(' | ')}`);
|
|
438
|
+
}
|
|
439
|
+
return report;
|
|
440
|
+
}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* native-audit.mjs — THE DERIVATION AUDIT, callable by a consumer.
|
|
3
|
+
*
|
|
4
|
+
* tools/native-interface.mjs --check answers "is the committed interface artifact exactly
|
|
5
|
+
* what the vendored upstream Rust derives to today?" — but as a subprocess in sememe's own
|
|
6
|
+
* checkout. A consumer that installed the package has no checkout and no tools/ directory
|
|
7
|
+
* (deliberately: the qualification harness does not ship), so this module carries the same
|
|
8
|
+
* verdict as an import: re-derive both pinned native interfaces from vendor/ and compare
|
|
9
|
+
* against the shipped interface/ artifacts byte-for-byte.
|
|
10
|
+
*
|
|
11
|
+
* This is the same audit sememe's own suite runs (test/native-interface.test.mjs), moved to
|
|
12
|
+
* a package surface. It exists so a consumer can falsify the table it was handed instead of
|
|
13
|
+
* trusting it: the verdict is derived in the consumer's process from the shipped upstream
|
|
14
|
+
* bytes, not read from a log the producer wrote about itself.
|
|
15
|
+
*/
|
|
16
|
+
import { readFileSync } from 'node:fs';
|
|
17
|
+
import path from 'node:path';
|
|
18
|
+
import { fileURLToPath } from 'node:url';
|
|
19
|
+
import { deriveAll } from './native-interface.mjs';
|
|
20
|
+
|
|
21
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
22
|
+
const INTERFACE_DIR = path.resolve(HERE, '..', 'interface');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Re-derive every pinned native interface from the vendored upstream sources and compare
|
|
26
|
+
* with the shipped artifacts.
|
|
27
|
+
*
|
|
28
|
+
* Returns `{ ok, checked, stale, missing, error }` where `checked` is the number of
|
|
29
|
+
* artifacts compared and `stale` names each artifact whose bytes differ from a fresh
|
|
30
|
+
* derivation. `ok === false` with a non-empty `stale` is the interesting failure: the
|
|
31
|
+
* derivation rules or the vendored bytes moved without the committed artifact following.
|
|
32
|
+
*/
|
|
33
|
+
export function auditNativeInterface() {
|
|
34
|
+
const stale = [];
|
|
35
|
+
const missing = [];
|
|
36
|
+
let checked = 0;
|
|
37
|
+
let docs;
|
|
38
|
+
try {
|
|
39
|
+
docs = deriveAll();
|
|
40
|
+
} catch (error) {
|
|
41
|
+
return { ok: false, checked, stale, missing, error: `derivation failed: ${error.message}` };
|
|
42
|
+
}
|
|
43
|
+
for (const doc of docs) {
|
|
44
|
+
const file = path.join(INTERFACE_DIR, `native-${doc.program}.json`);
|
|
45
|
+
let current;
|
|
46
|
+
try {
|
|
47
|
+
current = readFileSync(file, 'utf8');
|
|
48
|
+
} catch {
|
|
49
|
+
missing.push(path.basename(file));
|
|
50
|
+
continue;
|
|
51
|
+
}
|
|
52
|
+
checked += 1;
|
|
53
|
+
if (current !== `${JSON.stringify(doc, null, 2)}\n`) stale.push(path.basename(file));
|
|
54
|
+
}
|
|
55
|
+
return { ok: stale.length === 0 && missing.length === 0, checked, stale, missing, error: null };
|
|
56
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* native-corpus.mjs — sememe's REAL MAINNET NATIVE-PROGRAM CORPUS, shipped with the package.
|
|
3
|
+
*
|
|
4
|
+
* The two harvests here are the measured traffic the native decoder was qualified against
|
|
5
|
+
* (2026-10-31): every System and ComputeBudget payload pulled from live mainnet blocks,
|
|
6
|
+
* with the bytes the chain actually carried. A decoder can be checked against invented
|
|
7
|
+
* bytes; these are not invented — each carries its own provenance (slot, block, index)
|
|
8
|
+
* inside the harvest document.
|
|
9
|
+
*
|
|
10
|
+
* Why this ships instead of staying a test fixture: sememe's first in-tree consumer
|
|
11
|
+
* (solana-tx-remix) falsifies its own native path against the SAME corpus — "the table I
|
|
12
|
+
* consume names the chain's traffic" is a claim about the consumer, and the bytes it is
|
|
13
|
+
* judged on must be the producer's, not a stale copy. A copy in the consumer's tree is
|
|
14
|
+
* exactly the drift this package exists to end. The corpus is addressed by its sha256, so
|
|
15
|
+
* the consumer can pin and re-hash what it was given.
|
|
16
|
+
*
|
|
17
|
+
* `default` returns the whole document; `payloads(program)` flattens the wire bytes to
|
|
18
|
+
* Buffers. Neither decodes anything: reading is this module's whole job.
|
|
19
|
+
*/
|
|
20
|
+
import { readFileSync } from 'node:fs';
|
|
21
|
+
import { createHash } from 'node:crypto';
|
|
22
|
+
import path from 'node:path';
|
|
23
|
+
import { fileURLToPath } from 'node:url';
|
|
24
|
+
|
|
25
|
+
const HERE = path.dirname(fileURLToPath(import.meta.url));
|
|
26
|
+
|
|
27
|
+
export const SYSTEM_PROGRAM = '11111111111111111111111111111111';
|
|
28
|
+
export const COMPUTE_BUDGET_PROGRAM = 'ComputeBudget111111111111111111111111111111';
|
|
29
|
+
|
|
30
|
+
export const CORPUS = {
|
|
31
|
+
system: { file: 'harvest-system-2026-10-31.json', sha256: 'sha256:75c7052ebc8e1f18a0e2cb3fb1fb70b9963141f86b5994b3f1e64b4d9c64fd7d' },
|
|
32
|
+
computeBudget: { file: 'harvest-compute-budget-2026-10-31.json', sha256: 'sha256:25354bf24977ee52371cf307db2edab9fa7108662d68018db2aba63ba8172295' },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** One harvest document, byte-hashed on every read. */
|
|
36
|
+
export function corpus(which) {
|
|
37
|
+
const entry = CORPUS[which];
|
|
38
|
+
if (!entry) throw Error(`unknown corpus ${String(which)}; expected one of ${Object.keys(CORPUS).join(', ')}`);
|
|
39
|
+
const raw = readFileSync(path.join(HERE, 'corpus', entry.file));
|
|
40
|
+
const actual = `sha256:${createHash('sha256').update(raw).digest('hex')}`;
|
|
41
|
+
if (actual !== entry.sha256) {
|
|
42
|
+
throw Error(`corpus ${which} re-hashes to ${actual}, expected ${entry.sha256} — the shipped bytes are not the qualified ones`);
|
|
43
|
+
}
|
|
44
|
+
return JSON.parse(raw);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Every wire payload in one harvest, as Buffers (payload.dataHex). */
|
|
48
|
+
export function payloads(which) {
|
|
49
|
+
const document = corpus(which);
|
|
50
|
+
const out = [];
|
|
51
|
+
for (const transaction of document.transactions ?? []) {
|
|
52
|
+
for (const payload of transaction.payloads ?? []) {
|
|
53
|
+
if (typeof payload.dataHex === 'string') out.push(Buffer.from(payload.dataHex, 'hex'));
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return out;
|
|
57
|
+
}
|