@finchip.ai/foga 0.0.0-stage → 0.6.0-alpha.4
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 +201 -0
- package/NOTICE +21 -0
- package/README.md +216 -2
- package/artifacts/bundle.json +74250 -0
- package/artifacts/bundle.sha256 +1 -0
- package/bin/foga.js +14 -0
- package/build/known-interface-ids.json +18 -0
- package/build/profiles.json +120 -0
- package/dist/build/bundle.js +417 -0
- package/dist/build/vendor.js +74 -0
- package/dist/bundle/load.js +24 -0
- package/dist/bundle/types.js +1 -0
- package/dist/cli-cell.js +395 -0
- package/dist/cli.js +505 -0
- package/dist/core/acdf.js +162 -0
- package/dist/core/adrf.js +273 -0
- package/dist/core/aid.js +83 -0
- package/dist/core/ard.js +146 -0
- package/dist/core/asf.js +157 -0
- package/dist/core/canonical.js +79 -0
- package/dist/core/erc8004.js +52 -0
- package/dist/core/kya.js +225 -0
- package/dist/core/measure.js +96 -0
- package/dist/core/pack/cbor.js +130 -0
- package/dist/core/pack/skillpack.js +142 -0
- package/dist/core/skills.js +40 -0
- package/dist/core/swarm.js +206 -0
- package/dist/core/tenders.js +121 -0
- package/dist/core/units.js +122 -0
- package/dist/core/world.js +104 -0
- package/dist/demo/demander.js +76 -0
- package/dist/demo/evidence.js +1 -0
- package/dist/demo/loop-d.js +449 -0
- package/dist/doctor/checks.js +51 -0
- package/dist/doctor/doctor.js +385 -0
- package/dist/doctor/loop-d.js +103 -0
- package/dist/doctor/probe.js +97 -0
- package/dist/doctor/semantic.js +83 -0
- package/dist/genesis/extract.js +64 -0
- package/dist/genesis/funding.js +126 -0
- package/dist/genesis/operator-scope.js +90 -0
- package/dist/genesis/preflight.js +54 -0
- package/dist/genesis/probe.js +164 -0
- package/dist/genesis/roles.js +96 -0
- package/dist/genesis/run.js +71 -0
- package/dist/genesis/scenario.js +765 -0
- package/dist/genesis/skill-runner.js +103 -0
- package/dist/genesis/skill-worker.js +41 -0
- package/dist/genesis/t1-zk.js +62 -0
- package/dist/genesis/t3-zk.js +105 -0
- package/dist/genesis/t4.js +259 -0
- package/dist/genesis/t5.js +168 -0
- package/dist/genesis/t6.js +222 -0
- package/dist/genesis/t7.js +198 -0
- package/dist/genesis/t8.js +189 -0
- package/dist/genesis/t9.js +188 -0
- package/dist/genesis/zk.js +128 -0
- package/dist/http/server.js +276 -0
- package/dist/kernel/cell.js +104 -0
- package/dist/kernel/errors.js +78 -0
- package/dist/kernel/kernel.js +251 -0
- package/dist/kernel/provider.js +125 -0
- package/dist/kernel/store.js +470 -0
- package/dist/kernel/tx.js +385 -0
- package/dist/mcp/server.js +170 -0
- package/dist/node/config.js +195 -0
- package/dist/node/init.js +200 -0
- package/dist/node/keys.js +95 -0
- package/dist/node/migrate.js +173 -0
- package/dist/node/model/anthropic.js +97 -0
- package/dist/node/model/stub.js +129 -0
- package/dist/node/model/types.js +9 -0
- package/dist/node/node.js +198 -0
- package/dist/node/policy.js +95 -0
- package/dist/node/runner.js +256 -0
- package/dist/node/watchers.js +88 -0
- package/dist/tools/admin.js +184 -0
- package/dist/tools/catalog.js +261 -0
- package/dist/tools/common.js +143 -0
- package/dist/tools/identity.js +107 -0
- package/dist/tools/index.js +14 -0
- package/dist/tools/read.js +471 -0
- package/dist/tools/tenders.js +400 -0
- package/dist/tools/types.js +4 -0
- package/dist/util/bytecode.js +123 -0
- package/dist/util/fs.js +58 -0
- package/dist/util/hash.js +4 -0
- package/dist/util/log.js +11 -0
- package/dist/util/version.js +12 -0
- package/dist/world/deploy.js +277 -0
- package/dist/world/plan.js +93 -0
- package/dist/world/types.js +1 -0
- package/docs/build.md +45 -0
- package/docs/cell-account.md +58 -0
- package/docs/compatibility-matrix.md +38 -0
- package/docs/design-memo.md +113 -0
- package/docs/doctor.md +53 -0
- package/docs/examples/README.md +11 -0
- package/docs/examples/claude-code-mcp.json +13 -0
- package/docs/examples/claude_desktop_config.json +12 -0
- package/docs/examples/cursor-mcp.json +11 -0
- package/docs/genesis.md +312 -0
- package/docs/kernel.md +101 -0
- package/docs/loop-d.md +90 -0
- package/docs/mcp.md +105 -0
- package/docs/node.md +66 -0
- package/docs/version-lock.md +67 -0
- package/docs/world.md +50 -0
- package/docs/zk.md +48 -0
- package/package.json +82 -4
- package/prover/README.md +26 -0
- package/prover/build-predicate.mjs +77 -0
- package/prover/package-lock.json +1837 -0
- package/prover/package.json +15 -0
- package/prover/run.cjs +199 -0
- package/scenarios/fixtures/sample-filing.txt +16 -0
- package/scenarios/skills/finch-extract-verify/LICENSE +2 -0
- package/scenarios/skills/finch-extract-verify/SKILL.md +34 -0
- package/scenarios/skills/finch-extract-verify/fields.json +56 -0
- package/scenarios/skills/finch-extract-verify/manifest.json +16 -0
- package/scenarios/skills/foga-probe/LICENSE +2 -0
- package/scenarios/skills/foga-probe/SKILL.md +35 -0
- package/scenarios/skills/foga-probe/manifest.json +16 -0
- package/scenarios/skills/foga-probe/probe.schema.json +40 -0
- package/server.json +79 -0
- package/vendor/LOCK.json +107 -0
- package/vendor/README.md +13 -0
- package/worlds/local.json +34 -0
- package/worlds/sepolia.json +314 -0
- package/zk/build/verification_key.json +174 -0
- package/zk/circuits/kya_predicate_count_v1.circom +231 -0
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, join } from "node:path";
|
|
3
|
+
import { getAddress, toUtf8Bytes, hexlify } from "ethers";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
import { accountRef, dataUri, dataUriJcs, keccakJcs, sha256Hex } from "../core/canonical.js";
|
|
6
|
+
import { kindId, taskDescriptor } from "../core/ard.js";
|
|
7
|
+
import { hashlock, nativeTerms } from "../core/tenders.js";
|
|
8
|
+
import { runExtractIsolated, runProbeIsolated } from "../genesis/skill-runner.js";
|
|
9
|
+
import { ok, refused, safetyGate, skillGate, tenderGates } from "../node/policy.js";
|
|
10
|
+
import { GateSchema, TaskDocumentSchema, TenderOutputSchema, derivedJobId, readTender, tenderJobId, tenderOutput, zAs, zBool, zBytes32, zHex, zJobId, zUint } from "./common.js";
|
|
11
|
+
/**
|
|
12
|
+
* The tender path (ERC-8414) as capabilities: what the genesis does in T3 by hand, offered to a caller and judged by
|
|
13
|
+
* the catalog. The fulfiller's side — precheck, run the skill, submit, settle, withdraw — is the Loop D path; the
|
|
14
|
+
* demander's side — mint, fund — is what a counterparty (or a test) uses, in act mode through the cell or in build
|
|
15
|
+
* mode as calldata for its own account.
|
|
16
|
+
*/
|
|
17
|
+
const ZERO32 = "0x" + "00".repeat(32);
|
|
18
|
+
/** The text of a data: URI (base64 or percent-encoded), whatever its media type. */
|
|
19
|
+
function decodeDataUriText(uri) {
|
|
20
|
+
const m = /^data:([^,]*?)(;base64)?,(.*)$/s.exec(uri);
|
|
21
|
+
if (!m)
|
|
22
|
+
throw new Error("not a data: URI");
|
|
23
|
+
return m[2] ? Buffer.from(m[3], "base64").toString("utf8") : decodeURIComponent(m[3]);
|
|
24
|
+
}
|
|
25
|
+
/** The business gates of taking a tender: liveness and safety, skill, the tender policy, the hashlock commitment. */
|
|
26
|
+
async function takingGates(node, view) {
|
|
27
|
+
const k = node.kernel;
|
|
28
|
+
const cell = node.body;
|
|
29
|
+
const gates = [];
|
|
30
|
+
const [aidState, status] = await Promise.all([k.aid.stateName(cell.address), cell.status()]);
|
|
31
|
+
gates.push(safetyGate(aidState, status, "commitment"));
|
|
32
|
+
const held = await node.heldSkills();
|
|
33
|
+
gates.push(skillGate(node.config, { skill: view.facts.skill, packageHash: view.facts.packageHash }, held.map((h) => ({ name: h.name, packageHash: h.packageHash }))));
|
|
34
|
+
gates.push(...tenderGates(node.config, view.facts, { now: await node.now() }, node.acceptance));
|
|
35
|
+
if (getAddress(view.facts.acceptanceAuthority) === getAddress(node.acceptance.hashlock)) {
|
|
36
|
+
const committed = await k.tenders.hashlockVerifier().answerHashOf(await k.tenders.hashlockVerifier().key(k.tenders.address, view.facts.tokenId));
|
|
37
|
+
if (committed === ZERO32)
|
|
38
|
+
gates.push(refused("verifier", "the hashlock verifier holds no committed answer for this tender yet"));
|
|
39
|
+
else if (view.task?.acceptance.answerHash && view.task.acceptance.answerHash.toLowerCase() !== committed.toLowerCase())
|
|
40
|
+
gates.push(refused("verifier", `the task document's answerHash ${view.task.acceptance.answerHash.slice(0, 10)}… differs from the verifier's commitment ${committed.slice(0, 10)}…`));
|
|
41
|
+
else
|
|
42
|
+
gates.push(ok("verifier", `answer committed in the hashlock verifier (${committed.slice(0, 10)}…)`));
|
|
43
|
+
}
|
|
44
|
+
const gas = await k.balance(node.config.identity.operator);
|
|
45
|
+
const floor = BigInt(node.config.policy.budget.dailyGasWei) / 4n;
|
|
46
|
+
if (gas < floor)
|
|
47
|
+
gates.push(refused("budget", `operator gas ${gas} wei is below a quarter of the daily gas budget (${floor} wei)`));
|
|
48
|
+
else
|
|
49
|
+
gates.push(ok("budget", `operator holds ${gas} wei of gas (floor ${floor} wei)`));
|
|
50
|
+
return gates;
|
|
51
|
+
}
|
|
52
|
+
export const tender_precheck = {
|
|
53
|
+
name: "tender_precheck",
|
|
54
|
+
title: "Pre-check a tender",
|
|
55
|
+
description: "Every gate the cell passes before taking a tender: anchor ACTIVE and binding Satisfied, the skill held and offered with the committed package, the policy on reward and deadlines, the acceptance authority, the hashlock commitment, the budget. Writes a decision record; sends nothing.",
|
|
56
|
+
action: "act",
|
|
57
|
+
visibility: "public",
|
|
58
|
+
principal: "cell",
|
|
59
|
+
ercs: [8414, 8434, 8468, 8338],
|
|
60
|
+
input: z.object({ tokenId: zUint }),
|
|
61
|
+
output: z.object({ tokenId: zUint, jobId: z.string(), gates: z.array(GateSchema), tender: TenderOutputSchema }),
|
|
62
|
+
async plan({ node }, { tokenId }) {
|
|
63
|
+
const id = BigInt(tokenId);
|
|
64
|
+
const view = await readTender(node, id, { counterparty: true });
|
|
65
|
+
const gates = await takingGates(node, view);
|
|
66
|
+
return {
|
|
67
|
+
job: { id: `precheck:${tokenId}`, kind: "precheck", input: { tokenId }, commitment: true, ephemeral: true },
|
|
68
|
+
gates,
|
|
69
|
+
valueWei: 0n,
|
|
70
|
+
steps: [],
|
|
71
|
+
output: () => ({ tokenId, jobId: tenderJobId(id, view.facts.completions), gates, tender: tenderOutput(view) }),
|
|
72
|
+
};
|
|
73
|
+
},
|
|
74
|
+
};
|
|
75
|
+
const RunOutput = z.object({
|
|
76
|
+
skill: z.string(),
|
|
77
|
+
packageHash: zBytes32,
|
|
78
|
+
jobId: z.string(),
|
|
79
|
+
answer: z.string(),
|
|
80
|
+
answerHash: zBytes32,
|
|
81
|
+
/** sha256(answer ‖ cell): what a hashlock submission by this cell carries. */
|
|
82
|
+
resultHash: zBytes32,
|
|
83
|
+
report: z.unknown(),
|
|
84
|
+
runReceipt: z.unknown(),
|
|
85
|
+
elapsedMs: z.number(),
|
|
86
|
+
});
|
|
87
|
+
export const skill_run = {
|
|
88
|
+
name: "skill_run",
|
|
89
|
+
title: "Run a held skill",
|
|
90
|
+
description: "Run one of the cell's skills in the isolated worker (foga-probe: view calls at a pinned block; finch-extract-verify: the deterministic extraction over a document inside the input directory or given inline). A job with a persistent result; the same input is the same job.",
|
|
91
|
+
action: "act",
|
|
92
|
+
visibility: "caller",
|
|
93
|
+
principal: "cell",
|
|
94
|
+
ercs: [8338],
|
|
95
|
+
input: z.object({ skill: z.string(), input: z.unknown(), tokenId: zUint.optional(), jobId: zJobId }),
|
|
96
|
+
output: RunOutput,
|
|
97
|
+
async plan({ node }, input) {
|
|
98
|
+
const held = await node.heldSkills();
|
|
99
|
+
const gates = [skillGate(node.config, { skill: input.skill, packageHash: null }, held.map((h) => ({ name: h.name, packageHash: h.packageHash })))];
|
|
100
|
+
const skill = held.find((h) => h.name === input.skill);
|
|
101
|
+
const jobId = input.jobId ?? derivedJobId(`skill-run:${input.skill}`, { input: input.input, tokenId: input.tokenId ?? null });
|
|
102
|
+
const cell = node.body.address;
|
|
103
|
+
return {
|
|
104
|
+
job: { id: jobId, kind: "skill-run", input: { skill: input.skill, input: input.input, tokenId: input.tokenId ?? null } },
|
|
105
|
+
gates,
|
|
106
|
+
valueWei: 0n,
|
|
107
|
+
steps: [],
|
|
108
|
+
work: async () => {
|
|
109
|
+
if (!skill)
|
|
110
|
+
throw new Error(`skill ${input.skill} is not held`);
|
|
111
|
+
const started = Date.now();
|
|
112
|
+
let answer;
|
|
113
|
+
let report;
|
|
114
|
+
if (input.skill === "foga-probe") {
|
|
115
|
+
const probe = input.input;
|
|
116
|
+
const run = await runProbeIsolated({ worldPath: node.worldPath, rpc: node.rpc, bundlePath: node.bundlePath, input: probe });
|
|
117
|
+
answer = run.answer;
|
|
118
|
+
report = run.report;
|
|
119
|
+
}
|
|
120
|
+
else if (input.skill === "finch-extract-verify") {
|
|
121
|
+
const doc = input.input?.document;
|
|
122
|
+
if (!doc)
|
|
123
|
+
throw new Error("finch-extract-verify needs input.document: a file name inside the input directory or a data: URI holding the text");
|
|
124
|
+
// an inline document is written beside the cell's store and read from there; a named one must be a bare file name in the input directory
|
|
125
|
+
let documentPath;
|
|
126
|
+
let inputDir = node.inputDir;
|
|
127
|
+
if (doc.startsWith("data:")) {
|
|
128
|
+
const text = decodeDataUriText(doc);
|
|
129
|
+
inputDir = join(dirname(node.storePath), "inline");
|
|
130
|
+
mkdirSync(inputDir, { recursive: true });
|
|
131
|
+
documentPath = join(inputDir, `${sha256Hex(text).slice(2, 34)}.txt`);
|
|
132
|
+
writeFileSync(documentPath, text);
|
|
133
|
+
}
|
|
134
|
+
else {
|
|
135
|
+
if (/[\\/]|\.\./.test(doc))
|
|
136
|
+
throw new Error("input.document must be a bare file name inside the input directory");
|
|
137
|
+
documentPath = join(node.inputDir, doc);
|
|
138
|
+
}
|
|
139
|
+
const run = await runExtractIsolated({ documentPath, fieldsPath: join(node.skillsDir, "finch-extract-verify", "fields.json"), packageHash: skill.packageHash, inputDir, skillsDir: node.skillsDir });
|
|
140
|
+
answer = run.canonical;
|
|
141
|
+
report = run.report;
|
|
142
|
+
}
|
|
143
|
+
else
|
|
144
|
+
throw new Error(`no executor for skill ${input.skill}; the node runs foga-probe and finch-extract-verify`);
|
|
145
|
+
const answerHash = hashlock.answerHash(answer);
|
|
146
|
+
const runReceipt = { schema: "foga.run-receipt/1", skill: input.skill, packageHash: skill.packageHash, tokenId: input.tokenId ?? null, input: input.input, answerHash, fulfiller: accountRef(node.chainId, cell), worker: "isolated child process (skill-worker), view calls only" };
|
|
147
|
+
return { answer, answerHash, resultHash: hashlock.resultHash(answer, cell), report, runReceipt, elapsedMs: Date.now() - started };
|
|
148
|
+
},
|
|
149
|
+
output: (_c, work) => ({ skill: input.skill, packageHash: skill?.packageHash ?? ZERO32, jobId, ...work }),
|
|
150
|
+
};
|
|
151
|
+
},
|
|
152
|
+
};
|
|
153
|
+
export const tender_submit = {
|
|
154
|
+
name: "tender_submit",
|
|
155
|
+
title: "Submit a fulfillment",
|
|
156
|
+
description: "Submit the result of a run to a tender through the cell's account (ERC-8414 submitFulfillment): a commitment-class call that passes every taking gate again at the moment of sending. The job is the tender's n-th completion by this cell.",
|
|
157
|
+
action: "act",
|
|
158
|
+
visibility: "caller",
|
|
159
|
+
principal: "cell",
|
|
160
|
+
ercs: [8414],
|
|
161
|
+
input: z.object({ tokenId: zUint, resultHash: zBytes32, resultURI: z.string().optional(), runReceipt: z.unknown().optional(), jobId: zJobId }),
|
|
162
|
+
output: z.object({ tokenId: zUint, submissionId: zUint, txHash: z.string(), operationId: z.string(), jobId: z.string() }),
|
|
163
|
+
async plan({ node }, input) {
|
|
164
|
+
const k = node.kernel;
|
|
165
|
+
const id = BigInt(input.tokenId);
|
|
166
|
+
const view = await readTender(node, id, { counterparty: true });
|
|
167
|
+
const gates = await takingGates(node, view);
|
|
168
|
+
const jobId = input.jobId ?? tenderJobId(id, view.facts.completions);
|
|
169
|
+
const resultURI = input.resultURI ?? (input.runReceipt ? dataUriJcs(input.runReceipt) : dataUriJcs({ schema: "foga.run-receipt/1", tokenId: input.tokenId, resultHash: input.resultHash, fulfiller: accountRef(node.chainId, node.body.address) }));
|
|
170
|
+
const intent = { target: k.tenders.address, value: 0n, data: k.tenders.encodeSubmit(id, input.resultHash, resultURI), label: `submitFulfillment(${input.tokenId}, ${input.resultHash.slice(0, 10)}…)` };
|
|
171
|
+
return {
|
|
172
|
+
job: { id: jobId, kind: "tender", input: { tokenId: input.tokenId, resultHash: input.resultHash }, commitment: true },
|
|
173
|
+
gates,
|
|
174
|
+
valueWei: 0n,
|
|
175
|
+
steps: [{ name: "submit", kind: "commitment", build: () => intent }],
|
|
176
|
+
output: (confirmed) => {
|
|
177
|
+
const c = confirmed[0];
|
|
178
|
+
return { tokenId: input.tokenId, submissionId: k.tenders.submissionIdFromReceipt(c.receipt).toString(), txHash: c.txHash, operationId: `job:${jobId}:submit`, jobId };
|
|
179
|
+
},
|
|
180
|
+
};
|
|
181
|
+
},
|
|
182
|
+
};
|
|
183
|
+
export const tender_settle = {
|
|
184
|
+
name: "tender_settle",
|
|
185
|
+
title: "Settle a submission",
|
|
186
|
+
description: "Reveal the answer and settle the cell's submission (ERC-8414 settleFulfillment with the hashlock proof); the reward is paid to the account or credited for withdrawal.",
|
|
187
|
+
action: "act",
|
|
188
|
+
visibility: "caller",
|
|
189
|
+
principal: "cell",
|
|
190
|
+
ercs: [8414],
|
|
191
|
+
input: z.object({ tokenId: zUint, submissionId: zUint, answer: z.string().optional(), proof: zHex.optional(), jobId: zJobId }),
|
|
192
|
+
output: z.object({ tokenId: zUint, submissionId: zUint, txHash: z.string(), operationId: z.string(), jobId: z.string(), paidWei: zUint, creditWei: zUint, status: z.number() }),
|
|
193
|
+
async plan({ node }, input) {
|
|
194
|
+
const k = node.kernel;
|
|
195
|
+
const id = BigInt(input.tokenId);
|
|
196
|
+
const sid = BigInt(input.submissionId);
|
|
197
|
+
if (!input.answer && !input.proof)
|
|
198
|
+
throw new Error("pass the answer (text) or the proof (hex)");
|
|
199
|
+
const proof = input.proof ?? hexlify(toUtf8Bytes(input.answer));
|
|
200
|
+
const view = await readTender(node, id);
|
|
201
|
+
const sub = view.submissions.find((s) => s.id === input.submissionId);
|
|
202
|
+
const gates = [];
|
|
203
|
+
const [aidState, status] = await Promise.all([k.aid.stateName(node.body.address), node.body.status()]);
|
|
204
|
+
gates.push(safetyGate(aidState, status, "plain"));
|
|
205
|
+
if (!sub)
|
|
206
|
+
gates.push(refused("input", `tender ${input.tokenId} has no submission ${input.submissionId}`));
|
|
207
|
+
else if (getAddress(sub.fulfiller) !== getAddress(node.body.address))
|
|
208
|
+
gates.push(refused("policy", `submission ${input.submissionId} is ${sub.fulfiller}'s, not the cell's`));
|
|
209
|
+
else if (sub.status !== 0)
|
|
210
|
+
gates.push(refused("policy", `submission ${input.submissionId} is not pending (status ${sub.status})`));
|
|
211
|
+
else
|
|
212
|
+
gates.push(ok("policy", `submission ${input.submissionId} by the cell is pending`));
|
|
213
|
+
const now = await node.now();
|
|
214
|
+
if (view.facts.settleBy !== 0n && Number(view.facts.settleBy) <= now)
|
|
215
|
+
gates.push(refused("policy", "past settleBy"));
|
|
216
|
+
const jobId = input.jobId ?? `${tenderJobId(id, view.facts.completions)}:settle`;
|
|
217
|
+
const intent = { target: k.tenders.address, value: 0n, data: k.tenders.encodeSettle(id, sid, proof), label: `settleFulfillment(${input.tokenId}, ${input.submissionId})` };
|
|
218
|
+
const before = await node.body.balance();
|
|
219
|
+
return {
|
|
220
|
+
job: { id: jobId, kind: "tender-settle", input: { tokenId: input.tokenId, submissionId: input.submissionId, proofHash: sha256Hex(proof) } },
|
|
221
|
+
gates,
|
|
222
|
+
valueWei: 0n,
|
|
223
|
+
steps: [{ name: "settle", kind: "plain", build: () => intent }],
|
|
224
|
+
output: async (confirmed) => {
|
|
225
|
+
const c = confirmed[0];
|
|
226
|
+
const [after, credit, s] = await Promise.all([node.body.balance(), k.tenders.creditOf(id, node.body.address), k.tenders.submissionOf(id, sid)]);
|
|
227
|
+
return { tokenId: input.tokenId, submissionId: input.submissionId, txHash: c.txHash, operationId: `job:${jobId}:settle`, jobId, paidWei: (after > before ? after - before : 0n).toString(), creditWei: credit.toString(), status: s.status };
|
|
228
|
+
},
|
|
229
|
+
};
|
|
230
|
+
},
|
|
231
|
+
};
|
|
232
|
+
export const credit_withdraw = {
|
|
233
|
+
name: "credit_withdraw",
|
|
234
|
+
title: "Withdraw credited reward",
|
|
235
|
+
description: "Withdraw the cell's credited reward of a tender (ERC-8414 withdrawCredit), a de-risk action the account allows under a derisk-only suspension.",
|
|
236
|
+
action: "act",
|
|
237
|
+
visibility: "caller",
|
|
238
|
+
principal: "cell",
|
|
239
|
+
ercs: [8414],
|
|
240
|
+
input: z.object({ tokenId: zUint, jobId: zJobId }),
|
|
241
|
+
output: z.object({ tokenId: zUint, withdrawnWei: zUint, txHash: z.string(), operationId: z.string(), jobId: z.string() }),
|
|
242
|
+
async plan({ node }, input) {
|
|
243
|
+
const k = node.kernel;
|
|
244
|
+
const id = BigInt(input.tokenId);
|
|
245
|
+
const credit = await k.tenders.creditOf(id, node.body.address);
|
|
246
|
+
const [aidState, status] = await Promise.all([k.aid.stateName(node.body.address), node.body.status()]);
|
|
247
|
+
const gates = [safetyGate(aidState, status, "derisk")];
|
|
248
|
+
if (credit === 0n)
|
|
249
|
+
gates.push(refused("policy", `no credit of tender ${input.tokenId} is due to the cell`));
|
|
250
|
+
else
|
|
251
|
+
gates.push(ok("policy", `${credit} wei credited`));
|
|
252
|
+
const jobId = input.jobId ?? derivedJobId(`credit-withdraw:${input.tokenId}`, { credit: credit.toString() });
|
|
253
|
+
return {
|
|
254
|
+
job: { id: jobId, kind: "withdraw", input: { tokenId: input.tokenId } },
|
|
255
|
+
gates,
|
|
256
|
+
valueWei: 0n,
|
|
257
|
+
steps: [{ name: "withdraw", kind: "derisk", build: () => ({ target: k.tenders.address, value: 0n, data: k.tenders.encodeWithdrawCredit(id, node.body.address), label: `withdrawCredit(${input.tokenId}, cell)` }) }],
|
|
258
|
+
output: (confirmed) => ({ tokenId: input.tokenId, withdrawnWei: credit.toString(), txHash: confirmed[0].txHash, operationId: `job:${jobId}:withdraw`, jobId }),
|
|
259
|
+
};
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
const MintInput = z.object({
|
|
263
|
+
skill: z.string(),
|
|
264
|
+
packageHash: zBytes32,
|
|
265
|
+
input: z.unknown(),
|
|
266
|
+
answerHash: zBytes32,
|
|
267
|
+
rewardWei: zUint,
|
|
268
|
+
/** Seconds from now. */
|
|
269
|
+
submitIn: z.coerce.number().int().positive(),
|
|
270
|
+
settleIn: z.coerce.number().int().positive(),
|
|
271
|
+
judgmentWindowSeconds: z.coerce.number().int().positive().default(7 * 86400),
|
|
272
|
+
title: z.string().max(200).optional(),
|
|
273
|
+
deliverable: z.string().max(500).optional(),
|
|
274
|
+
/** Whether to fund and commit the answer in the same job (default) or mint only. */
|
|
275
|
+
fundAndCommit: zBool.default(true),
|
|
276
|
+
/** Also register an ERC-8444 task record for discovery (act mode). */
|
|
277
|
+
record: zBool.default(false),
|
|
278
|
+
as: zAs,
|
|
279
|
+
jobId: zJobId,
|
|
280
|
+
});
|
|
281
|
+
export const tender_mint = {
|
|
282
|
+
name: "tender_mint",
|
|
283
|
+
title: "Mint a hashlock tender",
|
|
284
|
+
description: "The demander's side: mint an ERC-8414 tender for a skill run with a foga.task/1 document, fund it with the reward and commit the answer hash in the hashlock verifier (one job, three operations), optionally with an ERC-8444 task record. In build mode the three calls are returned for the caller's own account.",
|
|
285
|
+
action: "act",
|
|
286
|
+
visibility: "caller",
|
|
287
|
+
principal: "cell",
|
|
288
|
+
ercs: [8414, 8444],
|
|
289
|
+
input: MintInput,
|
|
290
|
+
output: z.object({ tokenId: zUint, taskHash: zBytes32, answerHash: zBytes32, rewardWei: zUint, submitBy: z.number(), settleBy: z.number(), operations: z.array(z.string()), jobId: z.string(), recordId: zBytes32.nullable() }),
|
|
291
|
+
async plan(ctx, input) {
|
|
292
|
+
const { node } = ctx;
|
|
293
|
+
const k = node.kernel;
|
|
294
|
+
const now = await node.now();
|
|
295
|
+
const reward = BigInt(input.rewardWei);
|
|
296
|
+
const submitBy = now + input.submitIn;
|
|
297
|
+
const settleBy = now + input.settleIn;
|
|
298
|
+
if (settleBy <= submitBy)
|
|
299
|
+
throw new Error("settleIn must be later than submitIn");
|
|
300
|
+
const task = { schema: "foga.task/1", skill: input.skill, packageHash: input.packageHash, input: input.input, acceptance: { mode: "verifier", profile: "x-hashlock-v1", verifier: k.tenders.hashlockAddress, answerHash: input.answerHash }, deliverable: input.deliverable ?? "the canonical answer; resultHash = sha256(answer || fulfiller)" };
|
|
301
|
+
TaskDocumentSchema.parse(task);
|
|
302
|
+
const taskURI = dataUri(task);
|
|
303
|
+
const taskHash = sha256Hex(JSON.stringify(task));
|
|
304
|
+
const tdHash = keccakJcs({ td: "foga.task/1", skill: input.skill });
|
|
305
|
+
const terms = nativeTerms(reward, { submitBy: BigInt(submitBy), settleBy: BigInt(settleBy), judgmentWindow: BigInt(input.judgmentWindowSeconds) });
|
|
306
|
+
const owner = ctx.mode === "build" ? ctx.as : node.body.address;
|
|
307
|
+
const jobId = input.jobId ?? derivedJobId("tender-mint", { skill: input.skill, input: input.input, answerHash: input.answerHash, rewardWei: input.rewardWei, submitBy, settleBy, owner });
|
|
308
|
+
const gates = [];
|
|
309
|
+
if (ctx.mode !== "build") {
|
|
310
|
+
const [aidState, status] = await Promise.all([k.aid.stateName(node.body.address), node.body.status()]);
|
|
311
|
+
gates.push(safetyGate(aidState, status, "plain"));
|
|
312
|
+
const balance = await node.body.balance();
|
|
313
|
+
if (input.fundAndCommit && balance < reward)
|
|
314
|
+
gates.push(refused("budget", `the account holds ${balance} wei; the reward is ${reward} wei`));
|
|
315
|
+
}
|
|
316
|
+
const mintIntent = () => ({ target: k.tenders.address, value: 0n, data: k.tenders.encodeMint({ to: owner, updateAuthority: owner, acceptanceAuthority: k.tenders.hashlockAddress, tdHash, taskHash, taskURI, terms }), label: `mintTask(${input.skill}, reward ${reward})` });
|
|
317
|
+
const tokenOf = (prior) => k.tenders.tokenIdFromReceipt(prior[0].receipt);
|
|
318
|
+
const steps = [
|
|
319
|
+
{ name: "mint", kind: "plain", build: mintIntent },
|
|
320
|
+
...(input.fundAndCommit
|
|
321
|
+
? [
|
|
322
|
+
{ name: "fund", kind: "plain", build: (prior) => ({ target: k.tenders.address, value: reward, data: k.tenders.encodeFund(tokenOf(prior), reward), label: `fundTask(reward ${reward})` }) },
|
|
323
|
+
{ name: "commit", kind: "plain", build: (prior) => ({ target: k.tenders.hashlockAddress, value: 0n, data: k.tenders.encodeCommitAnswer(tokenOf(prior), input.answerHash), label: `commitAnswer(${input.answerHash.slice(0, 10)}…)` }) },
|
|
324
|
+
]
|
|
325
|
+
: []),
|
|
326
|
+
...(input.record && ctx.mode !== "build"
|
|
327
|
+
? [
|
|
328
|
+
{
|
|
329
|
+
name: "record",
|
|
330
|
+
kind: "plain",
|
|
331
|
+
build: (prior) => {
|
|
332
|
+
const tokenId = tokenOf(prior);
|
|
333
|
+
const desc = taskDescriptor({ chainId: node.chainId, demander: owner, slug: `tender-${tokenId}`, name: input.title ?? `${input.skill} tender ${tokenId}`, wants: input.deliverable ?? `a run of ${input.skill}`, taskHash, taskURI, bounty: { amount: (Number(reward) / 1e18).toString(), currency: "ETH", asset: `eip155:${node.chainId}/slip44:60` }, acceptance: { mode: "verifier", ref: accountRef(node.chainId, k.tenders.hashlockAddress) }, settleBy, judgmentWindow: `PT${input.judgmentWindowSeconds}S` });
|
|
334
|
+
return { target: k.ard.address, value: 0n, data: k.ard.encodeRegister(kindId("task"), k.tenders.address, tokenId, desc), label: `ARD register(task ${tokenId})` };
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
]
|
|
338
|
+
: []),
|
|
339
|
+
];
|
|
340
|
+
// build mode: the calls for the caller's own account, the token id taken from nextId()
|
|
341
|
+
let calls;
|
|
342
|
+
if (ctx.mode === "build") {
|
|
343
|
+
const next = await k.tenders.nextId();
|
|
344
|
+
calls = [mintIntent()];
|
|
345
|
+
if (input.fundAndCommit)
|
|
346
|
+
calls.push({ target: k.tenders.address, value: reward, data: k.tenders.encodeFund(next, reward), label: `fundTask(${next}, ${reward}) — token id assumed from nextId()` }, { target: k.tenders.hashlockAddress, value: 0n, data: k.tenders.encodeCommitAnswer(next, input.answerHash), label: `commitAnswer(${next}) — token id assumed from nextId()` });
|
|
347
|
+
}
|
|
348
|
+
return {
|
|
349
|
+
job: { id: jobId, kind: "tender-mint", input: { task, rewardWei: input.rewardWei, submitBy, settleBy, owner } },
|
|
350
|
+
gates,
|
|
351
|
+
valueWei: input.fundAndCommit ? reward : 0n,
|
|
352
|
+
steps,
|
|
353
|
+
calls,
|
|
354
|
+
output: (confirmed) => {
|
|
355
|
+
const tokenId = confirmed.length ? k.tenders.tokenIdFromReceipt(confirmed[0].receipt) : 0n;
|
|
356
|
+
const recordStep = steps.findIndex((s) => s.name === "record");
|
|
357
|
+
const recordId = recordStep >= 0 && confirmed[recordStep] ? k.ard.recordIdFromReceipt(confirmed[recordStep].receipt) : null;
|
|
358
|
+
return { tokenId: tokenId.toString(), taskHash, answerHash: input.answerHash, rewardWei: input.rewardWei, submitBy, settleBy, operations: steps.slice(0, confirmed.length).map((s) => `job:${jobId}:${s.name}`), jobId, recordId };
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
},
|
|
362
|
+
};
|
|
363
|
+
export const tender_fund = {
|
|
364
|
+
name: "tender_fund",
|
|
365
|
+
title: "Fund a tender",
|
|
366
|
+
description: "Add native value to a tender's escrow (ERC-8414 fundTask) from the cell, or as calldata for another funder.",
|
|
367
|
+
action: "act",
|
|
368
|
+
visibility: "caller",
|
|
369
|
+
principal: "cell",
|
|
370
|
+
ercs: [8414],
|
|
371
|
+
input: z.object({ tokenId: zUint, amountWei: zUint, as: zAs, jobId: zJobId }),
|
|
372
|
+
output: z.object({ tokenId: zUint, amountWei: zUint, escrowWei: zUint, txHash: z.string().nullable(), operationId: z.string().nullable(), jobId: z.string() }),
|
|
373
|
+
async plan(ctx, input) {
|
|
374
|
+
const { node } = ctx;
|
|
375
|
+
const k = node.kernel;
|
|
376
|
+
const id = BigInt(input.tokenId);
|
|
377
|
+
const amount = BigInt(input.amountWei);
|
|
378
|
+
const gates = [];
|
|
379
|
+
if (ctx.mode !== "build") {
|
|
380
|
+
const [aidState, status] = await Promise.all([k.aid.stateName(node.body.address), node.body.status()]);
|
|
381
|
+
gates.push(safetyGate(aidState, status, "plain"));
|
|
382
|
+
const balance = await node.body.balance();
|
|
383
|
+
if (balance < amount)
|
|
384
|
+
gates.push(refused("budget", `the account holds ${balance} wei; ${amount} wei asked`));
|
|
385
|
+
}
|
|
386
|
+
const jobId = input.jobId ?? derivedJobId(`tender-fund:${input.tokenId}`, { amount: input.amountWei, escrowBefore: (await k.tenders.escrowBalanceOf(id)).toString() });
|
|
387
|
+
const intent = { target: k.tenders.address, value: amount, data: k.tenders.encodeFund(id, amount), label: `fundTask(${input.tokenId}, ${amount})` };
|
|
388
|
+
return {
|
|
389
|
+
job: { id: jobId, kind: "tender-fund", input: { tokenId: input.tokenId, amountWei: input.amountWei } },
|
|
390
|
+
gates,
|
|
391
|
+
valueWei: amount,
|
|
392
|
+
steps: [{ name: "fund", kind: "plain", build: () => intent }],
|
|
393
|
+
calls: [intent],
|
|
394
|
+
output: async (confirmed) => ({ tokenId: input.tokenId, amountWei: input.amountWei, escrowWei: (await k.tenders.escrowBalanceOf(id)).toString(), txHash: confirmed[0]?.txHash ?? null, operationId: confirmed.length ? `job:${jobId}:fund` : null, jobId }),
|
|
395
|
+
};
|
|
396
|
+
},
|
|
397
|
+
};
|
|
398
|
+
export const TENDER_CAPABILITIES = [tender_precheck, skill_run, tender_submit, tender_settle, credit_withdraw, tender_mint, tender_fund];
|
|
399
|
+
/** For other modules: the gates of taking a tender, as the precheck computes them. */
|
|
400
|
+
export { takingGates };
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export const ACTION_RANK = { read: 0, build: 1, act: 2, admin: 3 };
|
|
2
|
+
export const VISIBILITY_RANK = { public: 0, caller: 1, operator: 2 };
|
|
3
|
+
export const OPERATOR_CALLER = { id: "operator", entrance: "cli", level: "admin", visibility: "operator" };
|
|
4
|
+
export const PUBLIC_CALLER = { id: "anonymous", entrance: "http", level: "build", visibility: "public" };
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { keccak256 } from "ethers";
|
|
2
|
+
function hexBody(hex) {
|
|
3
|
+
return hex.startsWith("0x") ? hex.slice(2) : hex;
|
|
4
|
+
}
|
|
5
|
+
/**
|
|
6
|
+
* Remove the CBOR metadata trailer solc appends to runtime and creation code.
|
|
7
|
+
* The last two bytes encode the trailer length (big-endian). We only strip when the
|
|
8
|
+
* trailer parses as a CBOR map (0xa1..0xa5), which is what solc emits.
|
|
9
|
+
*/
|
|
10
|
+
export function stripMetadata(hex) {
|
|
11
|
+
const body = hexBody(hex);
|
|
12
|
+
if (body.length < 4)
|
|
13
|
+
return "0x" + body;
|
|
14
|
+
const len = parseInt(body.slice(-4), 16);
|
|
15
|
+
const total = (len + 2) * 2;
|
|
16
|
+
if (!Number.isFinite(len) || total > body.length)
|
|
17
|
+
return "0x" + body;
|
|
18
|
+
const trailer = body.slice(body.length - total, body.length - 4);
|
|
19
|
+
const first = trailer.slice(0, 2).toLowerCase();
|
|
20
|
+
if (!["a1", "a2", "a3", "a4", "a5"].includes(first))
|
|
21
|
+
return "0x" + body;
|
|
22
|
+
return "0x" + body.slice(0, body.length - total);
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Zero the content hash inside every CBOR metadata block found anywhere in the code, not only the
|
|
26
|
+
* trailer. A contract that creates other contracts (TaskToken deploys a TaskVault per token) embeds
|
|
27
|
+
* their creation code, including their metadata trailer; that nested hash depends on source names and
|
|
28
|
+
* build paths, so two builds of identical code can differ there. Handles the `ipfs` form
|
|
29
|
+
* (a2 64 "ipfs" 58 22 <34-byte multihash> ... ) and the legacy `bzzr0`/`bzzr1` form (a1/a2 65 "bzzr?" 58 20 <32 bytes>).
|
|
30
|
+
* Metadata written with `bytecode_hash = none` carries no hash and is left untouched.
|
|
31
|
+
*/
|
|
32
|
+
export function maskEmbeddedMetadataHashes(hex) {
|
|
33
|
+
let body = hexBody(hex).toLowerCase();
|
|
34
|
+
const patterns = [
|
|
35
|
+
{ marker: "a2646970667358" + "22", hashHex: 68 }, // a2 64 'ipfs' 58 22 + 34 bytes
|
|
36
|
+
{ marker: "a165627a7a72305820", hashHex: 64 }, // a1 65 'bzzr0' 58 20 + 32 bytes
|
|
37
|
+
{ marker: "a265627a7a72305820", hashHex: 64 },
|
|
38
|
+
{ marker: "a265627a7a72315820", hashHex: 64 }, // bzzr1
|
|
39
|
+
];
|
|
40
|
+
for (const { marker, hashHex } of patterns) {
|
|
41
|
+
let from = 0;
|
|
42
|
+
for (;;) {
|
|
43
|
+
const at = body.indexOf(marker, from);
|
|
44
|
+
if (at < 0)
|
|
45
|
+
break;
|
|
46
|
+
if (at % 2 === 0 && at + marker.length + hashHex <= body.length) {
|
|
47
|
+
const start = at + marker.length;
|
|
48
|
+
body = body.slice(0, start) + "0".repeat(hashHex) + body.slice(start + hashHex);
|
|
49
|
+
}
|
|
50
|
+
from = at + 2;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return "0x" + body;
|
|
54
|
+
}
|
|
55
|
+
/** Zero out every immutable value slot (constructor-set immutables and a library's own deploy address). */
|
|
56
|
+
export function maskImmutables(hex, refs) {
|
|
57
|
+
let body = hexBody(hex);
|
|
58
|
+
if (!refs)
|
|
59
|
+
return "0x" + body;
|
|
60
|
+
for (const slots of Object.values(refs)) {
|
|
61
|
+
for (const { start, length } of slots) {
|
|
62
|
+
const a = start * 2;
|
|
63
|
+
const b = a + length * 2;
|
|
64
|
+
if (b <= body.length)
|
|
65
|
+
body = body.slice(0, a) + "0".repeat(length * 2) + body.slice(b);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return "0x" + body;
|
|
69
|
+
}
|
|
70
|
+
/** Zero out every linked-library address slot so linked and unlinked code compare equal. */
|
|
71
|
+
export function maskLinks(hex, refs) {
|
|
72
|
+
if (!refs)
|
|
73
|
+
return hex.startsWith("0x") ? hex : "0x" + hex;
|
|
74
|
+
let body = hexBody(hex);
|
|
75
|
+
for (const libs of Object.values(refs)) {
|
|
76
|
+
for (const slots of Object.values(libs)) {
|
|
77
|
+
for (const { start, length } of slots) {
|
|
78
|
+
const a = start * 2;
|
|
79
|
+
const b = a + length * 2;
|
|
80
|
+
if (b <= body.length)
|
|
81
|
+
body = body.slice(0, a) + "0".repeat(length * 2) + body.slice(b);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return "0x" + body;
|
|
86
|
+
}
|
|
87
|
+
/** Replace library placeholders with the deployed addresses. `resolve` returns a 0x address or undefined. */
|
|
88
|
+
export function linkBytecode(hex, refs, resolve) {
|
|
89
|
+
let body = hexBody(hex);
|
|
90
|
+
if (!refs)
|
|
91
|
+
return "0x" + body;
|
|
92
|
+
for (const [sourcePath, libs] of Object.entries(refs)) {
|
|
93
|
+
for (const [libName, slots] of Object.entries(libs)) {
|
|
94
|
+
const address = resolve(sourcePath, libName);
|
|
95
|
+
if (!address)
|
|
96
|
+
throw new Error(`unlinked library ${sourcePath}:${libName}`);
|
|
97
|
+
const addr = hexBody(address).toLowerCase().padStart(40, "0");
|
|
98
|
+
if (addr.length !== 40)
|
|
99
|
+
throw new Error(`bad library address for ${libName}: ${address}`);
|
|
100
|
+
for (const { start, length } of slots) {
|
|
101
|
+
if (length !== 20)
|
|
102
|
+
throw new Error(`unexpected link slot length ${length} for ${libName}`);
|
|
103
|
+
const a = start * 2;
|
|
104
|
+
body = body.slice(0, a) + addr + body.slice(a + 40);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
if (body.includes("__$"))
|
|
109
|
+
throw new Error("bytecode still contains unresolved link placeholders");
|
|
110
|
+
return "0x" + body;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* keccak256 of runtime code with the metadata trailer stripped, every embedded metadata hash zeroed and
|
|
114
|
+
* every deployment-dependent slot (linked library addresses, immutables, a library's own address) masked:
|
|
115
|
+
* the "code identity" the doctor compares between a build and a chain. Two builds of the same source with
|
|
116
|
+
* the same compiler and settings share it whatever their source names or build paths were.
|
|
117
|
+
*/
|
|
118
|
+
export function runtimeIdentity(deployedBytecode, links, immutables) {
|
|
119
|
+
return keccak256(stripMetadata(maskEmbeddedMetadataHashes(maskImmutables(maskLinks(deployedBytecode, links), immutables))));
|
|
120
|
+
}
|
|
121
|
+
export function codeSize(hex) {
|
|
122
|
+
return hexBody(hex).length / 2;
|
|
123
|
+
}
|
package/dist/util/fs.js
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, readdirSync, statSync, writeFileSync, rmSync, cpSync } from "node:fs";
|
|
2
|
+
import { dirname, join, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
/** Repository root (the directory that holds package.json). */
|
|
5
|
+
export function repoRoot() {
|
|
6
|
+
// src/util/fs.ts -> repo root is two levels up; dist/util/fs.js -> same shape.
|
|
7
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
8
|
+
return resolve(here, "..", "..");
|
|
9
|
+
}
|
|
10
|
+
export function readJson(path) {
|
|
11
|
+
return JSON.parse(readFileSync(path, "utf8"));
|
|
12
|
+
}
|
|
13
|
+
export function writeJson(path, value) {
|
|
14
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
15
|
+
writeFileSync(path, JSON.stringify(value, null, 2) + "\n");
|
|
16
|
+
}
|
|
17
|
+
export function ensureDir(path) {
|
|
18
|
+
mkdirSync(path, { recursive: true });
|
|
19
|
+
}
|
|
20
|
+
export function rmrf(path) {
|
|
21
|
+
rmSync(path, { recursive: true, force: true });
|
|
22
|
+
}
|
|
23
|
+
export function copyTree(from, to) {
|
|
24
|
+
mkdirSync(dirname(to), { recursive: true });
|
|
25
|
+
cpSync(from, to, { recursive: true });
|
|
26
|
+
}
|
|
27
|
+
export function exists(path) {
|
|
28
|
+
return existsSync(path);
|
|
29
|
+
}
|
|
30
|
+
/** Recursively list files under `dir` (absolute paths). */
|
|
31
|
+
export function walk(dir, out = []) {
|
|
32
|
+
if (!existsSync(dir))
|
|
33
|
+
return out;
|
|
34
|
+
for (const entry of readdirSync(dir)) {
|
|
35
|
+
const p = join(dir, entry);
|
|
36
|
+
const st = statSync(p);
|
|
37
|
+
if (st.isDirectory())
|
|
38
|
+
walk(p, out);
|
|
39
|
+
else
|
|
40
|
+
out.push(p);
|
|
41
|
+
}
|
|
42
|
+
return out;
|
|
43
|
+
}
|
|
44
|
+
/** Deterministic JSON: keys sorted at every level, no whitespace. */
|
|
45
|
+
export function canonicalJson(value) {
|
|
46
|
+
return JSON.stringify(sortKeys(value));
|
|
47
|
+
}
|
|
48
|
+
function sortKeys(value) {
|
|
49
|
+
if (Array.isArray(value))
|
|
50
|
+
return value.map(sortKeys);
|
|
51
|
+
if (value && typeof value === "object") {
|
|
52
|
+
const out = {};
|
|
53
|
+
for (const k of Object.keys(value).sort())
|
|
54
|
+
out[k] = sortKeys(value[k]);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
return value;
|
|
58
|
+
}
|
package/dist/util/log.js
ADDED
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
const prefix = "foga";
|
|
2
|
+
export const log = {
|
|
3
|
+
info: (msg) => console.log(`${prefix}: ${msg}`),
|
|
4
|
+
warn: (msg) => console.warn(`${prefix}: warning: ${msg}`),
|
|
5
|
+
error: (msg) => console.error(`${prefix}: error: ${msg}`),
|
|
6
|
+
step: (msg) => console.log(` - ${msg}`),
|
|
7
|
+
};
|
|
8
|
+
export function fail(msg) {
|
|
9
|
+
log.error(msg);
|
|
10
|
+
process.exit(1);
|
|
11
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { repoRoot } from "./fs.js";
|
|
4
|
+
/** The package version, read once from package.json (the same file the npm package ships). */
|
|
5
|
+
export const version = (() => {
|
|
6
|
+
try {
|
|
7
|
+
return String(JSON.parse(readFileSync(join(repoRoot(), "package.json"), "utf8")).version);
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
return "0.0.0";
|
|
11
|
+
}
|
|
12
|
+
})();
|