@toon-protocol/git 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +52 -0
- package/dist/chunk-4WFGAICZ.js +707 -0
- package/dist/chunk-4WFGAICZ.js.map +1 -0
- package/dist/chunk-KXXHAUXL.js +109 -0
- package/dist/chunk-KXXHAUXL.js.map +1 -0
- package/dist/chunk-LJA7PPZI.js +144 -0
- package/dist/chunk-LJA7PPZI.js.map +1 -0
- package/dist/chunk-M7O4SEVW.js +56 -0
- package/dist/chunk-M7O4SEVW.js.map +1 -0
- package/dist/chunk-R3JVS6SX.js +345 -0
- package/dist/chunk-R3JVS6SX.js.map +1 -0
- package/dist/chunk-SBMFWVCP.js +265 -0
- package/dist/chunk-SBMFWVCP.js.map +1 -0
- package/dist/cli/rig.d.ts +1 -0
- package/dist/cli/rig.js +1430 -0
- package/dist/cli/rig.js.map +1 -0
- package/dist/index.d.ts +742 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/publisher-VEIEQHl6.d.ts +254 -0
- package/dist/standalone/index.d.ts +272 -0
- package/dist/standalone/index.js +30 -0
- package/dist/standalone/index.js.map +1 -0
- package/dist/standalone-mode-UFMHGUOM.js +132 -0
- package/dist/standalone-mode-UFMHGUOM.js.map +1 -0
- package/package.json +71 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/repo-reader.ts","../src/routes.ts","../src/push.ts"],"sourcesContent":["/**\n * GitRepoReader — read a local repository via `execFile` git plumbing.\n *\n * Real git gives perfect fidelity (packfiles, delta chains, exotic history)\n * with zero new deps — no isomorphic-git. Everything here is read-only and\n * injection-safe:\n *\n * - child processes are spawned with `execFile`/`spawn` and argument\n * ARRAYS — never a shell, never string interpolation;\n * - every caller-supplied revision/range is validated against strict\n * regexes that (among other things) reject a leading `-`, so a value\n * like `--upload-pack=…` can never be parsed as an option;\n * - `--` terminators are appended where git supports them so nothing\n * user-supplied can be re-interpreted as a pathspec.\n *\n * Push planning/publishing live in follow-up tickets of epic\n * toon-client#222 — this module only reads.\n */\n\nimport { execFile, spawn } from 'node:child_process';\nimport { promisify } from 'node:util';\nimport type { GitObjectType } from './objects.js';\n\nconst execFileAsync = promisify(execFile);\n\n/** Generous cap for plumbing stdout (rev-list on big repos, format-patch). */\nconst MAX_BUFFER = 256 * 1024 * 1024;\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** A single ref from `git for-each-ref` (branches + tags). */\nexport interface GitRef {\n /** Full refname, e.g. `refs/heads/main` or `refs/tags/v1.0.0`. */\n refname: string;\n /**\n * SHA the ref points at. For annotated tags this is the TAG object's SHA\n * (the peeled commit is in {@link peeledSha}); for branches and\n * lightweight tags it is the commit SHA.\n */\n sha: string;\n /** Type of the referenced object: `commit`, or `tag` for annotated tags. */\n type: GitObjectType;\n /** For annotated tags: the peeled (target) object SHA. */\n peeledSha?: string;\n}\n\n/** Result of {@link GitRepoReader.listRefs}. */\nexport interface RepoRefs {\n /**\n * Full refname HEAD points at (e.g. `refs/heads/main`), or `undefined`\n * when HEAD is detached.\n */\n head?: string;\n refs: GitRef[];\n}\n\n/** One object streamed out of `git cat-file --batch`. */\nexport interface ReadGitObject {\n /** Full 40-hex SHA-1. */\n sha: string;\n type: GitObjectType;\n /** Raw object body (content only, no envelope header). May be binary. */\n body: Buffer;\n}\n\n/** Result of {@link GitRepoReader.readObjects}. */\nexport interface ReadObjectsResult {\n /** Objects found, in input order (minus missing ones). */\n objects: ReadGitObject[];\n /** Requested SHAs not present in the repository. */\n missing: string[];\n}\n\n/** One object from `rev-list --objects`: SHA plus the path it was reached by. */\nexport interface ObjectWithPath {\n /** Full 40-hex SHA-1. */\n sha: string;\n /**\n * Path the object was first reached by (blobs and non-root trees);\n * `undefined` for commits, root trees, and tag objects.\n */\n path?: string;\n}\n\n/** One object's metadata from `cat-file --batch-check`. */\nexport interface ObjectStat {\n sha: string;\n type: GitObjectType;\n /** Object body size in bytes (content only, no envelope header). */\n size: number;\n}\n\n/** Result of {@link GitRepoReader.statObjects}. */\nexport interface StatObjectsResult {\n /** Stats found, in input order (minus missing ones). */\n objects: ObjectStat[];\n /** Requested SHAs not present in the repository. */\n missing: string[];\n}\n\n/** Error from a git child process, carrying exit code and stderr. */\nexport class GitError extends Error {\n constructor(\n message: string,\n /** Process exit code (undefined when the process failed to spawn). */\n public readonly exitCode: number | undefined,\n /** Captured stderr, trimmed. */\n public readonly stderr: string\n ) {\n super(message);\n this.name = 'GitError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Argument validation (injection defense)\n// ---------------------------------------------------------------------------\n\n/** Full 40-hex SHA-1. */\nconst FULL_SHA_RE = /^[0-9a-f]{40}$/;\n\n/**\n * One revision token: a SHA prefix (4–40 hex) or a refname-ish word with an\n * optional `^`/`~<n>` ancestry suffix. Must start with an alphanumeric, so a\n * leading `-` (option injection) is impossible; `@{…}`, whitespace, and other\n * revspec exotica are deliberately rejected.\n */\nconst REV_TOKEN_RE = /^[A-Za-z0-9][A-Za-z0-9._/-]*(?:[~^][0-9]*)*$/;\n\nfunction isValidRevision(rev: string): boolean {\n if (rev.length === 0 || rev.length > 1024) return false;\n if (!REV_TOKEN_RE.test(rev)) return false;\n // Refname rules git enforces that our charset alone doesn't:\n if (rev.includes('..')) return false; // range separator / invalid in refnames\n if (rev.endsWith('.lock') || rev.endsWith('/') || rev.endsWith('.')) return false;\n return true;\n}\n\nfunction assertRevision(rev: string, what: string): void {\n if (!isValidRevision(rev)) {\n throw new Error(\n `${what} is not a valid git revision (got ${JSON.stringify(rev)}); ` +\n 'expected a SHA or simple refname — options/ranges are rejected'\n );\n }\n}\n\nfunction assertFullSha(sha: string, what: string): void {\n if (!FULL_SHA_RE.test(sha)) {\n throw new Error(\n `${what} is not a full 40-hex SHA-1 (got ${JSON.stringify(sha)})`\n );\n }\n}\n\n/**\n * A revision range for format-patch: `<rev>`, `<rev>..<rev>`, or\n * `<rev>...<rev>` where each side passes {@link isValidRevision}.\n */\nfunction assertRange(range: string, what: string): void {\n const parts = range.split(/\\.{2,3}/);\n const separators = range.match(/\\.{2,3}/g) ?? [];\n const ok =\n parts.length <= 2 &&\n separators.length === parts.length - 1 &&\n parts.every((p) => isValidRevision(p));\n if (!ok) {\n throw new Error(\n `${what} is not a valid revision range (got ${JSON.stringify(range)}); ` +\n 'expected <rev>, <rev>..<rev>, or <rev>...<rev>'\n );\n }\n}\n\n// ---------------------------------------------------------------------------\n// cat-file --batch incremental parser\n// ---------------------------------------------------------------------------\n\nconst OBJECT_TYPES: ReadonlySet<string> = new Set(['blob', 'tree', 'commit', 'tag']);\n\n/**\n * Incremental parser for `git cat-file --batch` output:\n * `<sha> <type> <size>\\n<body>\\n` per found object, `<name> missing\\n` for\n * absent ones. Bodies are raw bytes (possibly binary) and may be split\n * across arbitrary chunk boundaries, so parsing is strictly size-driven.\n */\nclass BatchParser {\n private buf: Buffer = Buffer.alloc(0);\n private pending: { sha: string; type: GitObjectType; size: number } | null = null;\n\n readonly objects: ReadGitObject[] = [];\n readonly missing: string[] = [];\n\n push(chunk: Buffer): void {\n this.buf = this.buf.length === 0 ? chunk : Buffer.concat([this.buf, chunk]);\n this.drain();\n }\n\n /** True when no partially-parsed record remains. */\n isComplete(): boolean {\n return this.pending === null && this.buf.length === 0;\n }\n\n private drain(): void {\n for (;;) {\n if (this.pending) {\n // Need body + trailing LF before the record is complete.\n const needed = this.pending.size + 1;\n if (this.buf.length < needed) return;\n const body = Buffer.from(this.buf.subarray(0, this.pending.size));\n this.objects.push({ sha: this.pending.sha, type: this.pending.type, body });\n this.buf = this.buf.subarray(needed);\n this.pending = null;\n continue;\n }\n\n const nl = this.buf.indexOf(0x0a);\n if (nl === -1) return;\n const header = this.buf.subarray(0, nl).toString('utf-8');\n this.buf = this.buf.subarray(nl + 1);\n\n const [name, second, third] = header.split(' ');\n if (name && second === 'missing' && third === undefined) {\n this.missing.push(name);\n continue;\n }\n if (name && second && third !== undefined && OBJECT_TYPES.has(second)) {\n const size = Number.parseInt(third, 10);\n if (Number.isSafeInteger(size) && size >= 0) {\n this.pending = { sha: name, type: second as GitObjectType, size };\n continue;\n }\n }\n throw new GitError(\n `unexpected cat-file --batch header: ${JSON.stringify(header)}`,\n undefined,\n ''\n );\n }\n }\n}\n\n// ---------------------------------------------------------------------------\n// GitRepoReader\n// ---------------------------------------------------------------------------\n\n/**\n * Read-only view of a local git repository via git plumbing commands.\n *\n * All methods throw {@link GitError} when the underlying git process fails\n * unexpectedly, and plain `Error` when a caller-supplied argument fails\n * validation (before any process is spawned).\n */\nexport class GitRepoReader {\n constructor(\n /** Absolute or relative path to the repository worktree (or .git dir). */\n public readonly repoPath: string\n ) {}\n\n /** Run git with argument-array safety; resolves stdout as UTF-8. */\n private async git(\n args: string[],\n opts: { allowExitCodes?: number[] } = {}\n ): Promise<{ stdout: string; exitCode: number }> {\n try {\n const { stdout } = await execFileAsync('git', args, {\n cwd: this.repoPath,\n maxBuffer: MAX_BUFFER,\n encoding: 'utf-8',\n });\n return { stdout, exitCode: 0 };\n } catch (err) {\n const e = err as NodeJS.ErrnoException & {\n code?: number | string;\n stdout?: string;\n stderr?: string;\n };\n const exitCode = typeof e.code === 'number' ? e.code : undefined;\n if (exitCode !== undefined && opts.allowExitCodes?.includes(exitCode)) {\n return { stdout: e.stdout ?? '', exitCode };\n }\n throw new GitError(\n `git ${args[0]} failed${exitCode !== undefined ? ` (exit ${exitCode})` : ''}: ` +\n `${(e.stderr ?? e.message ?? '').trim()}`,\n exitCode,\n (e.stderr ?? '').trim()\n );\n }\n }\n\n /**\n * List all branches and tags plus the symbolic HEAD.\n *\n * Annotated tags report the tag object's SHA/type with the peeled target\n * in `peeledSha`. A detached HEAD is tolerated (`head` is `undefined`).\n */\n async listRefs(): Promise<RepoRefs> {\n const format = '%(refname)%00%(objectname)%00%(objecttype)%00%(*objectname)';\n const [refsRes, headRes] = await Promise.all([\n this.git(['for-each-ref', `--format=${format}`, 'refs/heads', 'refs/tags']),\n // Exit 1 = detached HEAD (or unborn branch pointer oddities) — tolerated.\n this.git(['symbolic-ref', '--quiet', 'HEAD'], { allowExitCodes: [1] }),\n ]);\n\n const refs: GitRef[] = [];\n for (const line of refsRes.stdout.split('\\n')) {\n if (!line) continue;\n const [refname, sha, objecttype, peeled] = line.split('\\0');\n if (!refname || !sha || !objecttype || !OBJECT_TYPES.has(objecttype)) {\n throw new GitError(`unexpected for-each-ref line: ${JSON.stringify(line)}`, undefined, '');\n }\n refs.push({\n refname,\n sha,\n type: objecttype as GitObjectType,\n ...(peeled ? { peeledSha: peeled } : {}),\n });\n }\n\n const head = headRes.exitCode === 0 ? headRes.stdout.trim() || undefined : undefined;\n return { head, refs };\n }\n\n /**\n * SHAs of every object reachable from `want` but not from `have`\n * (`git rev-list --objects <want…> --not <have…>`), i.e. the push delta.\n *\n * Haves that don't exist locally (e.g. remote tips we never fetched) are\n * filtered out first via one `cat-file --batch-check` pass — rev-list\n * would otherwise die on them.\n */\n async objectsBetween(want: string[], have: string[]): Promise<string[]> {\n const objects = await this.objectsBetweenWithPaths(want, have);\n return objects.map((o) => o.sha);\n }\n\n /**\n * Like {@link objectsBetween} but keeps the path each object was reached\n * by (`rev-list --objects` emits `<sha> <path>` for blobs and non-root\n * trees) — used by push planning to report actionable oversize errors.\n */\n async objectsBetweenWithPaths(\n want: string[],\n have: string[]\n ): Promise<ObjectWithPath[]> {\n for (const w of want) assertRevision(w, 'want');\n for (const h of have) assertRevision(h, 'have');\n if (want.length === 0) return [];\n\n const knownHaves = await this.filterExisting(have);\n\n const args = ['rev-list', '--objects', ...want];\n if (knownHaves.length > 0) args.push('--not', ...knownHaves);\n args.push('--'); // nothing user-supplied can become a pathspec\n const { stdout } = await this.git(args);\n\n const objects: ObjectWithPath[] = [];\n for (const line of stdout.split('\\n')) {\n if (!line) continue;\n // `--objects` lines are `<sha>` or `<sha> <path>`.\n const spaceIdx = line.indexOf(' ');\n if (spaceIdx === -1) {\n objects.push({ sha: line });\n } else {\n const path = line.slice(spaceIdx + 1);\n objects.push({\n sha: line.slice(0, spaceIdx),\n ...(path ? { path } : {}),\n });\n }\n }\n return objects;\n }\n\n /** Run git feeding `input` on stdin; resolves collected stdout bytes. */\n private runWithStdin(args: string[], input: string): Promise<Buffer> {\n return new Promise<Buffer>((resolve, reject) => {\n const child = spawn('git', args, {\n cwd: this.repoPath,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n const out: Buffer[] = [];\n let stderr = '';\n child.stdout.on('data', (chunk: Buffer) => out.push(chunk));\n child.stderr.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf-8');\n });\n child.on('error', (err) => {\n reject(new GitError(`failed to spawn git ${args[0]}: ${err.message}`, undefined, ''));\n });\n child.on('close', (code) => {\n if (code !== 0) {\n return reject(\n new GitError(`git ${args[0]} failed (exit ${code}): ${stderr.trim()}`, code ?? undefined, stderr.trim())\n );\n }\n resolve(Buffer.concat(out));\n });\n child.stdin.on('error', () => {\n // Child died before consuming stdin; 'close' surfaces the failure.\n });\n child.stdin.write(input);\n child.stdin.end();\n });\n }\n\n /** Of the given revisions, keep only those resolvable locally. */\n private async filterExisting(revs: string[]): Promise<string[]> {\n if (revs.length === 0) return [];\n const { missing } = await this.batchCheck(revs);\n const missingSet = new Set(missing);\n return revs.filter((r) => !missingSet.has(r));\n }\n\n /** One `cat-file --batch-check` pass; returns names reported missing. */\n private async batchCheck(names: string[]): Promise<{ missing: string[] }> {\n const stdout = await this.runWithStdin(\n ['cat-file', '--batch-check'],\n names.join('\\n') + '\\n'\n );\n const missing: string[] = [];\n for (const line of stdout.toString('utf-8').split('\\n')) {\n if (line.endsWith(' missing')) missing.push(line.slice(0, -' missing'.length));\n }\n return { missing };\n }\n\n /**\n * Object metadata (type + body size) for a batch of SHAs via one\n * `cat-file --batch-check` pass — no bodies are read. Missing objects are\n * reported, not thrown.\n */\n async statObjects(shas: string[]): Promise<StatObjectsResult> {\n for (const sha of shas) assertFullSha(sha, 'sha');\n if (shas.length === 0) return { objects: [], missing: [] };\n\n const stdout = await this.runWithStdin(\n ['cat-file', '--batch-check'],\n shas.join('\\n') + '\\n'\n );\n\n const objects: ObjectStat[] = [];\n const missing: string[] = [];\n for (const line of stdout.toString('utf-8').split('\\n')) {\n if (!line) continue;\n if (line.endsWith(' missing')) {\n missing.push(line.slice(0, -' missing'.length));\n continue;\n }\n const [sha, type, sizeStr] = line.split(' ');\n const size = Number.parseInt(sizeStr ?? '', 10);\n if (\n !sha ||\n !type ||\n !OBJECT_TYPES.has(type) ||\n !Number.isSafeInteger(size) ||\n size < 0\n ) {\n throw new GitError(\n `unexpected cat-file --batch-check line: ${JSON.stringify(line)}`,\n undefined,\n ''\n );\n }\n objects.push({ sha, type: type as GitObjectType, size });\n }\n return { objects, missing };\n }\n\n /**\n * Read raw object bodies via a single streaming `git cat-file --batch`\n * child process. Bodies may be binary and are parsed size-driven across\n * chunk boundaries. Missing objects are reported, not thrown.\n */\n async readObjects(shas: string[]): Promise<ReadObjectsResult> {\n for (const sha of shas) assertFullSha(sha, 'sha');\n if (shas.length === 0) return { objects: [], missing: [] };\n\n const parser = new BatchParser();\n await new Promise<void>((resolve, reject) => {\n const child = spawn('git', ['cat-file', '--batch'], {\n cwd: this.repoPath,\n stdio: ['pipe', 'pipe', 'pipe'],\n });\n\n let stderr = '';\n let parseError: Error | null = null;\n\n child.stderr.on('data', (chunk: Buffer) => {\n stderr += chunk.toString('utf-8');\n });\n child.stdout.on('data', (chunk: Buffer) => {\n if (parseError) return;\n try {\n parser.push(chunk);\n } catch (err) {\n parseError = err as Error;\n child.kill();\n }\n });\n child.on('error', (err) => {\n reject(new GitError(`failed to spawn git cat-file: ${err.message}`, undefined, ''));\n });\n child.on('close', (code) => {\n if (parseError) return reject(parseError);\n if (code !== 0) {\n return reject(\n new GitError(`git cat-file --batch failed (exit ${code}): ${stderr.trim()}`, code ?? undefined, stderr.trim())\n );\n }\n if (!parser.isComplete()) {\n return reject(\n new GitError('git cat-file --batch output ended mid-record', code ?? undefined, stderr.trim())\n );\n }\n resolve();\n });\n\n child.stdin.on('error', () => {\n // Child died before consuming stdin; 'close' will surface the error.\n });\n child.stdin.write(shas.join('\\n') + '\\n');\n child.stdin.end();\n });\n\n return { objects: parser.objects, missing: parser.missing };\n }\n\n /**\n * `git merge-base --is-ancestor <a> <b>` — true when `a` is an ancestor\n * of `b` (fast-forward check / force detection). Exit codes other than\n * 0/1 (e.g. unknown revisions) throw.\n */\n async isAncestor(a: string, b: string): Promise<boolean> {\n assertRevision(a, 'ancestor candidate');\n assertRevision(b, 'descendant candidate');\n const { exitCode } = await this.git(\n ['merge-base', '--is-ancestor', a, b],\n { allowExitCodes: [1] }\n );\n return exitCode === 0;\n }\n\n /**\n * `git format-patch --stdout <range>` — the full mbox-formatted patch\n * series text (empty string when the range selects no commits).\n */\n async formatPatch(range: string): Promise<string> {\n assertRange(range, 'range');\n const { stdout } = await this.git(['format-patch', '--stdout', range, '--']);\n return stdout;\n }\n\n /**\n * Parent SHAs for a batch of commit SHAs via one\n * `git rev-list --no-walk=unsorted --parents` pass. Root commits map to an\n * empty array. Used to derive the kind:1617 `commit`/`parent-commit` tag\n * pairs for exactly the commits a format-patch series carries.\n */\n async commitParents(shas: string[]): Promise<Map<string, string[]>> {\n for (const sha of shas) assertFullSha(sha, 'sha');\n if (shas.length === 0) return new Map();\n const { stdout } = await this.git([\n 'rev-list',\n '--no-walk=unsorted',\n '--parents',\n ...shas,\n '--',\n ]);\n const parents = new Map<string, string[]>();\n for (const line of stdout.split('\\n')) {\n if (!line) continue;\n const [sha, ...rest] = line.split(' ');\n if (!sha || !FULL_SHA_RE.test(sha) || rest.some((p) => !FULL_SHA_RE.test(p))) {\n throw new GitError(\n `unexpected rev-list --parents line: ${JSON.stringify(line)}`,\n undefined,\n ''\n );\n }\n parents.set(sha, rest);\n }\n return parents;\n }\n\n /**\n * Resolve a ref/revision to a full SHA via `git rev-parse --verify`.\n * Throws {@link GitError} when the name doesn't resolve.\n */\n async resolveRef(name: string): Promise<string> {\n assertRevision(name, 'ref name');\n const { stdout } = await this.git(['rev-parse', '--verify', '--quiet', name]);\n const sha = stdout.trim();\n if (!FULL_SHA_RE.test(sha)) {\n throw new GitError(\n `rev-parse --verify returned unexpected output for ${JSON.stringify(name)}: ${JSON.stringify(sha)}`,\n undefined,\n ''\n );\n }\n return sha;\n }\n}\n","/**\n * Wire shapes of the toon-clientd `/git/*` control routes (epic #222).\n *\n * These are the JSON request/response types the daemon serves (bigints as\n * decimal strings, Maps as plain records) — defined HERE, in the dependency\n * root, so both sides of the route can share them: the `rig` CLI (#229)\n * consumes them as a plain-fetch client, and `@toon-protocol/client-mcp`\n * (which depends on this package for the planner — the reverse import would\n * be circular) can adopt them for its `control-api.ts` declarations. TYPES\n * ONLY plus two pure serializers; no transport code lives here.\n *\n * Keep in byte-for-byte sync with\n * `packages/client-mcp/src/control-api.ts` (`Git*` shapes) and\n * `packages/client-mcp/src/daemon/routes.ts` (error envelopes: 409\n * `non_fast_forward` carries `refs`, 413 `oversize_objects` carries\n * `objects`, 503 `bootstrapping` / 402 `insufficient_gas` are retryable).\n */\n\nimport type { PublishReceipt } from './publisher.js';\nimport type { PlannedObject, PushPlan, PushResult, RefUpdate } from './push.js';\n\n/** One planned ref update (JSON-safe as-is). */\nexport type GitRefUpdate = RefUpdate;\n\n/** One object scheduled for upload (JSON-safe as-is). */\nexport type GitPlannedObject = PlannedObject;\n\n/**\n * `POST /git/estimate` — plan a push (local git plumbing + remote-state read)\n * and price it WITHOUT paying anything. The same body (plus `confirm`) drives\n * `POST /git/push`.\n */\nexport interface GitEstimateRequest {\n /** Path to the local git repository (worktree or .git dir). Must exist. */\n repoPath: string;\n /** Repository identifier (NIP-34 `d` tag). The daemon identity is the owner. */\n repoId: string;\n /**\n * Full refnames to push (e.g. `[\"refs/heads/main\"]`). Default: every local\n * branch and tag.\n */\n refspecs?: string[];\n /** Allow non-fast-forward updates (default false → 409 `non_fast_forward`). */\n force?: boolean;\n /**\n * Relay URLs to read remote state from and publish to. Plural from day one\n * (forward-compat); defaults to the daemon's config-seeded relay.\n */\n relayUrls?: string[];\n /** Repo name/description for the first-push kind:30617 announcement. */\n announcement?: { name?: string; description?: string };\n}\n\n/** Pre-push fee table (all fees in base/micro units, decimal strings). */\nexport interface GitFeeEstimate {\n objectCount: number;\n totalObjectBytes: number;\n /** Σ size × uploadFeePerByte. */\n uploadFee: string;\n /** Events to publish (refs event + announcement on first push). */\n eventCount: number;\n /** eventCount × per-event fee. */\n eventFees: string;\n /** uploadFee + eventFees. */\n totalFee: string;\n}\n\n/** Serialized `PushPlan` — everything a confirm UI needs. */\nexport interface GitEstimateResponse {\n repoId: string;\n refUpdates: GitRefUpdate[];\n /** Full new ref state to publish (HEAD target first). */\n newRefs: Record<string, string>;\n headSymref: string | null;\n objects: GitPlannedObject[];\n /** sha→txId hints known WITHOUT uploading (remote tags + resolver finds). */\n knownShaToTxId: Record<string, string>;\n /** True when no kind:30617 exists yet — the push announces first. */\n announceNeeded: boolean;\n announcement: { name: string; description: string };\n estimate: GitFeeEstimate;\n}\n\n/**\n * `POST /git/push` — plan + execute: upload the delta to Arweave and publish\n * the cumulative kind:30618 (+ kind:30617 on first push). PERMANENT + PAID.\n */\nexport interface GitPushRequest extends GitEstimateRequest {\n /** Must be literally `true` — a push spends channel funds irreversibly. */\n confirm: boolean;\n}\n\n/** One object-upload step result. */\nexport interface GitUploadStep {\n sha: string;\n txId: string;\n /** '0' when skipped (already on Arweave — content-addressed resume). */\n feePaid: string;\n skipped: boolean;\n}\n\n/** Receipt for one published event. */\nexport interface GitPublishReceipt {\n eventId: string;\n feePaid: string;\n}\n\n/** Serialized `PushResult` — per-step receipts + total fees actually paid. */\nexport interface GitPushResponse {\n repoId: string;\n refUpdates: GitRefUpdate[];\n /** Per-object results, in plan order. */\n uploads: GitUploadStep[];\n /** kind:30617 receipt, or null when the repo was already announced. */\n announceReceipt: GitPublishReceipt | null;\n /** kind:30618 (cumulative refs + arweave map) receipt. */\n refsReceipt: GitPublishReceipt;\n /** Full sha→txId map published in the refs event. */\n arweaveMap: Record<string, string>;\n /** Total fees actually paid (uploads + events), base units, decimal. */\n totalFeePaid: string;\n /** The pre-push estimate the push ran under (compare against totalFeePaid). */\n estimate: GitFeeEstimate;\n}\n\n// ---------------------------------------------------------------------------\n// Single-event git publishes (`/git/issue|comment|patch|status`, #231)\n// ---------------------------------------------------------------------------\n\n/** NIP-34 repository address: the owner+id pair behind `a` tags. */\nexport interface GitRepoAddr {\n /** Repository owner's Nostr pubkey (64-char hex) — author of kind:30617/30618. */\n ownerPubkey: string;\n /** Repository identifier (NIP-34 `d` tag). */\n repoId: string;\n}\n\n/** `POST /git/issue` — publish a kind:1621 issue against a repo. PAID. */\nexport interface GitIssueRequest {\n repoAddr: GitRepoAddr;\n /** Issue title (`subject` tag). */\n title: string;\n /** Issue body (Markdown content). */\n body: string;\n /** Labels (`t` tags). */\n labels?: string[];\n}\n\n/** `POST /git/comment` — publish a kind:1622 comment on an issue/patch. PAID. */\nexport interface GitCommentRequest {\n repoAddr: GitRepoAddr;\n /** Event id of the issue or patch being commented on. */\n rootEventId: string;\n /** Comment body (Markdown content). */\n body: string;\n /**\n * Pubkey of the TARGET event's author (NIP-34 `p` threading tag — not the\n * comment author). Defaults to the repo owner.\n */\n parentAuthorPubkey?: string;\n /** `e`-tag marker (default 'root': commenting directly on the issue/patch). */\n marker?: 'root' | 'reply';\n}\n\n/**\n * `POST /git/patch` — publish a kind:1617 patch. Supply EXACTLY ONE of\n * `patchText` (literal `git format-patch` output) or `repoPath`+`range`\n * (the daemon runs `git format-patch --stdout <range>` locally). PAID.\n */\nexport interface GitPatchRequest {\n repoAddr: GitRepoAddr;\n /** Patch/PR title (`subject` tag). */\n title: string;\n /** Literal patch text. Mutually exclusive with `repoPath`+`range`. */\n patchText?: string;\n /** Local repository to run format-patch in. Requires `range`. */\n repoPath?: string;\n /** Revision range for format-patch (`<rev>`, `<rev>..<rev>`, `<rev>...<rev>`). */\n range?: string;\n /** Commit/parent pairs for `commit`/`parent-commit` tags. */\n commits?: { sha: string; parentSha: string }[];\n /** Branch name for the `t` tag. */\n branch?: string;\n}\n\nexport type GitStatusValue = 'open' | 'applied' | 'closed' | 'draft';\n\n/** `POST /git/status` — publish a kind:1630-1633 status event. PAID. */\nexport interface GitStatusRequest {\n repoAddr: GitRepoAddr;\n /** Event id of the issue/patch whose status is being set. */\n targetEventId: string;\n /** open → 1630, applied → 1631, closed → 1632, draft → 1633. */\n status: GitStatusValue;\n /** Pubkey of the target event's author (`p` tag), when known. */\n targetPubkey?: string;\n}\n\n/**\n * Response of the single-event git publishes (issue/comment/patch/status):\n * a publish receipt plus the NIP-34 kind that was published. Daemon\n * responses extend the full `POST /publish` receipt, so the channel fields\n * (`channelId`/`nonce`/…) are present there; they are optional here because\n * the CLI's standalone path publishes through the embedded client and has\n * no channel wire shape to report.\n */\nexport interface GitEventResponse {\n /** Event ID as accepted by the relay. */\n eventId: string;\n /** Fee actually paid for this publish, base units, decimal string. */\n feePaid: string;\n /** The NIP-34 kind that was published. */\n kind: number;\n /** Channel the claim was signed against (daemon responses). */\n channelId?: string;\n /** Channel nonce after this publish (daemon responses). */\n nonce?: number;\n /** FULFILL response data (base64), when the backend returned any. */\n data?: string;\n /** Spendable channel balance after this write, when known. */\n channelBalanceAfter?: string;\n}\n\n/**\n * Uniform error envelope of non-2xx control-route responses. Structured\n * errors put extra fields at the top level: `non_fast_forward` (409) adds\n * `refs`, `oversize_objects` (413) adds `objects`.\n */\nexport interface GitErrorEnvelope {\n error: string;\n detail?: string;\n /** True when the caller should retry (e.g. daemon still bootstrapping). */\n retryable?: boolean;\n [extra: string]: unknown;\n}\n\n// ---------------------------------------------------------------------------\n// Serializers (pure) — the exact mapping the daemon routes apply; used by the\n// CLI's standalone mode so both surfaces emit identical wire JSON.\n// ---------------------------------------------------------------------------\n\n/** Serialize a plan's fee estimate onto the wire (bigints → strings). */\nexport function serializeFeeEstimate(plan: PushPlan): GitFeeEstimate {\n return {\n objectCount: plan.estimate.objectCount,\n totalObjectBytes: plan.estimate.totalObjectBytes,\n uploadFee: plan.estimate.uploadFee.toString(),\n eventCount: plan.estimate.eventCount,\n eventFees: plan.estimate.eventFees.toString(),\n totalFee: plan.estimate.totalFee.toString(),\n };\n}\n\n/** Serialize a PushPlan onto the wire (bigints → strings, Maps → records). */\nexport function serializePushPlan(plan: PushPlan): GitEstimateResponse {\n return {\n repoId: plan.repoId,\n refUpdates: plan.refUpdates,\n newRefs: plan.newRefs,\n headSymref: plan.headSymref,\n objects: plan.objects,\n knownShaToTxId: Object.fromEntries(plan.knownShaToTxId),\n announceNeeded: plan.announceNeeded,\n announcement: plan.announcement,\n estimate: serializeFeeEstimate(plan),\n };\n}\n\n/**\n * Serialize a standalone {@link PublishReceipt} into the wire shape the\n * daemon's single-event `/git/*` routes answer with, so `--json` consumers\n * see one `GitEventResponse` shape regardless of publisher mode.\n */\nexport function serializeEventReceipt(\n kind: number,\n receipt: PublishReceipt\n): GitEventResponse {\n return {\n eventId: receipt.eventId,\n feePaid: receipt.feePaid.toString(),\n kind,\n };\n}\n\n/** Serialize a PushResult onto the wire (bigints → strings, Maps → records). */\nexport function serializePushResult(\n plan: PushPlan,\n result: PushResult\n): GitPushResponse {\n return {\n repoId: plan.repoId,\n refUpdates: plan.refUpdates,\n uploads: result.uploads.map((u) => ({\n sha: u.sha,\n txId: u.txId,\n feePaid: u.feePaid.toString(),\n skipped: u.skipped,\n })),\n announceReceipt: result.announceReceipt\n ? {\n eventId: result.announceReceipt.eventId,\n feePaid: result.announceReceipt.feePaid.toString(),\n }\n : null,\n refsReceipt: {\n eventId: result.refsReceipt.eventId,\n feePaid: result.refsReceipt.feePaid.toString(),\n },\n arweaveMap: Object.fromEntries(result.arweaveMap),\n totalFeePaid: result.totalFeePaid.toString(),\n estimate: serializeFeeEstimate(plan),\n };\n}\n","/**\n * Push planner/executor — the core of `rig push` (epic #222, ticket #226).\n *\n * `planPush` is network-free (relay/Arweave-wise — it only runs local git\n * plumbing through GitRepoReader plus one injectable async resolver step):\n * it classifies every ref update, computes the object delta against what the\n * remote already stores, hard-errors on oversize objects, and prices the\n * push. The returned {@link PushPlan} carries everything a confirm UI needs.\n *\n * `executePush` spends money: it uploads the planned objects through a\n * {@link Publisher} (ref-tip objects last, so a crashed push never leads to\n * a state where a discoverable tip's history is missing), then publishes ONE\n * cumulative kind:30618 whose `arweave` tags are the MERGE of the remote's\n * existing sha→txId map and the new uploads — kind:30618 is NIP-33\n * replaceable, so dropping prior tags would orphan earlier hints — and whose\n * `r` tags are the full new ref state. On a first push it publishes the\n * kind:30617 announcement before the refs event.\n *\n * Resume safety: uploads are content-addressed (Git-SHA-tagged), so re-running\n * `executePush` after a crash is safe — it consults the merged\n * remote + planned sha→txId map before paying for any upload, and a re-plan\n * with fresh remote state (whose `resolveMissing` finds the already-uploaded\n * objects via GraphQL) skips them entirely.\n */\n\nimport { buildRepoAnnouncement, buildRepoRefs } from './nip34-events.js';\nimport { MAX_OBJECT_SIZE, type GitObjectType } from './objects.js';\nimport type {\n FeeRates,\n PublishReceipt,\n Publisher,\n} from './publisher.js';\nimport { GitError, type GitRef, type GitRepoReader } from './repo-reader.js';\nimport type { RemoteState } from './remote-state.js';\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** A ref update rejected because it is not a fast-forward. */\nexport interface RejectedRefUpdate {\n refname: string;\n localSha: string;\n remoteSha: string;\n}\n\n/** Thrown by {@link planPush} when a non-fast-forward update lacks `force`. */\nexport class NonFastForwardError extends Error {\n constructor(\n /** The refs that would need `--force` to update. */\n public readonly refs: RejectedRefUpdate[]\n ) {\n super(\n `non-fast-forward update rejected for ${refs\n .map((r) => r.refname)\n .join(', ')} — re-run with force to overwrite the remote ref(s)`\n );\n this.name = 'NonFastForwardError';\n }\n}\n\n/** One object exceeding {@link MAX_OBJECT_SIZE}. */\nexport interface OversizeObject {\n sha: string;\n type: GitObjectType;\n /** Body size in bytes. */\n size: number;\n /** Path the object was reached by (blobs / non-root trees), if known. */\n path?: string;\n}\n\n/**\n * Thrown by {@link planPush} when any object in the delta exceeds the 95KB\n * upload limit (hard error in v1 — the paid blob path is a follow-up spike).\n */\nexport class OversizeObjectsError extends Error {\n constructor(\n /** The offending objects with paths and sizes. */\n public readonly objects: OversizeObject[]\n ) {\n super(\n `${objects.length} object(s) exceed the ${MAX_OBJECT_SIZE} byte upload limit: ` +\n objects\n .map((o) => `${o.path ?? o.sha} (${o.size} bytes)`)\n .join(', ')\n );\n this.name = 'OversizeObjectsError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Plan types\n// ---------------------------------------------------------------------------\n\n/** How a ref moves relative to the remote. */\nexport type RefUpdateKind =\n /** Ref does not exist on the remote yet. */\n | 'new'\n /** Remote tip is an ancestor of the local tip. */\n | 'fast-forward'\n /** Non-fast-forward, allowed because `force` was set. */\n | 'forced'\n /** Local and remote tips already match — nothing to push. */\n | 'up-to-date';\n\n/** One planned ref update (deletions are out of scope in v1). */\nexport interface RefUpdate {\n /** Full refname, e.g. `refs/heads/main`. */\n refname: string;\n /** Local tip SHA (tag object SHA for annotated tags). */\n localSha: string;\n /** Remote tip SHA, or null when the ref is new. */\n remoteSha: string | null;\n kind: RefUpdateKind;\n}\n\n/** One object scheduled for upload. */\nexport interface PlannedObject {\n sha: string;\n type: GitObjectType;\n /** Body size in bytes (what the upload fee is charged on). */\n size: number;\n /** Path the object was reached by, if any (blobs / non-root trees). */\n path?: string;\n /** True when this SHA is the tip of a planned ref update (uploaded last). */\n isRefTip: boolean;\n}\n\n/** Pre-push fee estimate — render this in the confirm table. */\nexport interface PushFeeEstimate {\n /** Number of objects to upload. */\n objectCount: number;\n /** Total bytes across all planned object bodies. */\n totalObjectBytes: number;\n /** Σ size × uploadFeePerByte (smallest asset unit). */\n uploadFee: bigint;\n /** Number of events to publish (refs event + announcement on first push). */\n eventCount: number;\n /** eventCount × eventFee (smallest asset unit). */\n eventFees: bigint;\n /** uploadFee + eventFees. */\n totalFee: bigint;\n}\n\n/** Everything `executePush` (and a confirm UI) needs. */\nexport interface PushPlan {\n repoId: string;\n /** Every considered ref with its classification (incl. up-to-date). */\n refUpdates: RefUpdate[];\n /**\n * Full new ref state to publish as `r` tags: the remote's refs overlaid\n * with the planned updates (refs not being pushed are preserved — v1\n * never deletes). Ordered with the HEAD target first, which is what\n * `buildRepoRefs` derives the HEAD symref tag from.\n */\n newRefs: Record<string, string>;\n /** HEAD symref target for the new state (first key of {@link newRefs}). */\n headSymref: string | null;\n /**\n * Objects to upload, dependency-safe order: ref-tip objects last so a\n * crashed push never uploads a tip whose history is missing.\n */\n objects: PlannedObject[];\n /**\n * sha→txId hints known WITHOUT uploading: the remote's `arweave` tags\n * plus anything `resolveMissing` found. Merged into the published\n * kind:30618 so prior hints are never dropped.\n */\n knownShaToTxId: Map<string, string>;\n /** True when no kind:30617 exists yet — executePush announces first. */\n announceNeeded: boolean;\n /** Announcement metadata used when {@link announceNeeded}. */\n announcement: { name: string; description: string };\n estimate: PushFeeEstimate;\n}\n\nexport interface PlanPushOptions {\n repoReader: GitRepoReader;\n remoteState: RemoteState;\n /** Fee rates from `Publisher.getFeeRates()`. */\n feeRates: FeeRates;\n /** Repository identifier (NIP-34 `d` tag). */\n repoId: string;\n /**\n * Full refnames to push (e.g. `['refs/heads/main']`). Defaults to every\n * local branch and tag. Refs that don't exist locally are an error\n * (deletions are out of scope in v1).\n */\n refs?: string[];\n /** Allow non-fast-forward updates (default false → hard error). */\n force?: boolean;\n /** Repo name/description for the first-push announcement. */\n announcement?: { name?: string; description?: string };\n /**\n * Async resolver for SHAs the remote's `arweave` tags don't cover —\n * consulted before deciding to re-upload. Defaults to\n * `remoteState.resolveMissing` (GraphQL fallback); injectable so the\n * planner core stays testable without network.\n */\n resolveMissing?: (shas: string[]) => Promise<Map<string, string>>;\n}\n\n// ---------------------------------------------------------------------------\n// planPush\n// ---------------------------------------------------------------------------\n\n/**\n * Classify ref updates, compute the object delta, enforce the size limit,\n * and price the push. Throws {@link NonFastForwardError} /\n * {@link OversizeObjectsError} (both carry structured data for UIs).\n */\nexport async function planPush(options: PlanPushOptions): Promise<PushPlan> {\n const { repoReader, remoteState, feeRates, repoId, force = false } = options;\n const resolveMissing =\n options.resolveMissing ?? remoteState.resolveMissing.bind(remoteState);\n\n // 1. Select and classify refs. -------------------------------------------\n const { head, refs: localRefs } = await repoReader.listRefs();\n const localByName = new Map(localRefs.map((r) => [r.refname, r]));\n\n let selected: GitRef[];\n if (options.refs !== undefined) {\n selected = options.refs.map((name) => {\n const ref = localByName.get(name);\n if (!ref) {\n throw new Error(\n `ref ${JSON.stringify(name)} does not exist locally ` +\n '(ref deletion is out of scope in v1)'\n );\n }\n return ref;\n });\n } else {\n selected = localRefs;\n }\n\n const refUpdates: RefUpdate[] = [];\n const rejected: RejectedRefUpdate[] = [];\n for (const ref of selected) {\n const remoteSha = remoteState.refs.get(ref.refname) ?? null;\n if (remoteSha === null) {\n refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: 'new' });\n continue;\n }\n if (remoteSha === ref.sha) {\n refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: 'up-to-date' });\n continue;\n }\n let fastForward = false;\n try {\n fastForward = await repoReader.isAncestor(remoteSha, ref.sha);\n } catch (err) {\n // Remote tip unknown locally (never fetched) or not a commit-ish —\n // we can't prove ancestry, so treat it as non-fast-forward.\n if (!(err instanceof GitError)) throw err;\n fastForward = false;\n }\n if (fastForward) {\n refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: 'fast-forward' });\n } else if (force) {\n refUpdates.push({ refname: ref.refname, localSha: ref.sha, remoteSha, kind: 'forced' });\n } else {\n rejected.push({ refname: ref.refname, localSha: ref.sha, remoteSha });\n }\n }\n if (rejected.length > 0) throw new NonFastForwardError(rejected);\n\n const updates = refUpdates.filter((u) => u.kind !== 'up-to-date');\n\n // 2. Object delta: reachable from the new tips, minus what the remote has.\n const wantTips = [...new Set(updates.map((u) => u.localSha))];\n const haveTips = [...new Set(remoteState.refs.values())];\n const delta =\n wantTips.length > 0\n ? await repoReader.objectsBetweenWithPaths(wantTips, haveTips)\n : [];\n\n const knownShaToTxId = new Map(remoteState.shaToTxId);\n let candidates = delta.filter((o) => !knownShaToTxId.has(o.sha));\n if (candidates.length > 0) {\n // The remote's `arweave` tags may lag reality (e.g. a crashed push\n // uploaded objects but never published the refs event) — resolve the\n // gaps before paying to re-upload content-addressed data.\n const resolved = await resolveMissing(candidates.map((o) => o.sha));\n for (const [sha, txId] of resolved) knownShaToTxId.set(sha, txId);\n candidates = candidates.filter((o) => !knownShaToTxId.has(o.sha));\n }\n\n // 3. Sizes + oversize hard error. -----------------------------------------\n const pathBySha = new Map(candidates.map((c) => [c.sha, c.path]));\n const { objects: stats, missing } = await repoReader.statObjects(\n candidates.map((c) => c.sha)\n );\n if (missing.length > 0) {\n throw new Error(\n `objects vanished from the local repository during planning: ${missing.join(', ')}`\n );\n }\n\n const oversize: OversizeObject[] = [];\n for (const stat of stats) {\n if (stat.size > MAX_OBJECT_SIZE) {\n const path = pathBySha.get(stat.sha);\n oversize.push({ ...stat, ...(path ? { path } : {}) });\n }\n }\n if (oversize.length > 0) throw new OversizeObjectsError(oversize);\n\n // 4. Upload order: ref tips last. -----------------------------------------\n const tipShas = new Set(updates.map((u) => u.localSha));\n const planned: PlannedObject[] = stats.map((stat) => {\n const path = pathBySha.get(stat.sha);\n return { ...stat, ...(path ? { path } : {}), isRefTip: tipShas.has(stat.sha) };\n });\n const objects = [\n ...planned.filter((o) => !o.isRefTip),\n ...planned.filter((o) => o.isRefTip),\n ];\n\n // 5. Full new ref state (remote refs overlaid with updates), HEAD first. --\n const newRefsMap = new Map(remoteState.refs);\n for (const update of updates) newRefsMap.set(update.refname, update.localSha);\n\n const headSymref =\n head && newRefsMap.has(head)\n ? head\n : remoteState.headSymref && newRefsMap.has(remoteState.headSymref)\n ? remoteState.headSymref\n : ([...newRefsMap.keys()][0] ?? null);\n\n const newRefs: Record<string, string> = {};\n const headSha = headSymref ? newRefsMap.get(headSymref) : undefined;\n if (headSymref && headSha) newRefs[headSymref] = headSha;\n for (const [refname, sha] of newRefsMap) {\n if (refname !== headSymref) newRefs[refname] = sha;\n }\n\n // 6. Fee estimate. ---------------------------------------------------------\n const announceNeeded = !remoteState.announced;\n const totalObjectBytes = objects.reduce((sum, o) => sum + o.size, 0);\n const uploadFee = BigInt(totalObjectBytes) * feeRates.uploadFeePerByte;\n const eventCount = 1 + (announceNeeded ? 1 : 0);\n const eventFees = BigInt(eventCount) * feeRates.eventFee;\n\n return {\n repoId,\n refUpdates,\n newRefs,\n headSymref,\n objects,\n knownShaToTxId,\n announceNeeded,\n announcement: {\n name: options.announcement?.name ?? repoId,\n description: options.announcement?.description ?? '',\n },\n estimate: {\n objectCount: objects.length,\n totalObjectBytes,\n uploadFee,\n eventCount,\n eventFees,\n totalFee: uploadFee + eventFees,\n },\n };\n}\n\n// ---------------------------------------------------------------------------\n// executePush\n// ---------------------------------------------------------------------------\n\n/** Result of one object-upload step. */\nexport interface UploadStepResult {\n sha: string;\n txId: string;\n /** 0n when skipped (already uploaded — content-addressed resume). */\n feePaid: bigint;\n /** True when the object was already on Arweave and nothing was paid. */\n skipped: boolean;\n}\n\n/** Result of the whole push. */\nexport interface PushResult {\n /** Per-object results, in plan order. */\n uploads: UploadStepResult[];\n /** kind:30617 receipt, or null when the repo was already announced. */\n announceReceipt: PublishReceipt | null;\n /** kind:30618 (cumulative refs + arweave map) receipt. */\n refsReceipt: PublishReceipt;\n /**\n * The full sha→txId map published in the refs event: remote hints +\n * resolver finds + this push's uploads.\n */\n arweaveMap: Map<string, string>;\n /** Total fees actually paid (uploads + events), smallest asset unit. */\n totalFeePaid: bigint;\n}\n\nexport interface ExecutePushOptions {\n plan: PushPlan;\n publisher: Publisher;\n /**\n * Remote state — pass a FRESH fetch when resuming after a crash so its\n * `shaToTxId` (and `announced`) reflect what the previous attempt already\n * paid for.\n */\n remoteState: RemoteState;\n repoReader: GitRepoReader;\n /** Relay URLs to publish events to (plural from day one; size 1 today). */\n relayUrls: string[];\n}\n\n/** How many object bodies to hold in memory at once between read and upload. */\nconst READ_BATCH_SIZE = 100;\n\n/**\n * Execute a {@link PushPlan}: upload objects (ref tips last), then publish\n * the kind:30617 announcement (first push only) and ONE cumulative\n * kind:30618 whose `arweave` tags merge every known sha→txId hint with the\n * new uploads and whose `r` tags carry the full new ref state.\n *\n * Safe to re-run after a crash: SHAs already present in the merged\n * remote + plan map are skipped without paying.\n */\nexport async function executePush(\n options: ExecutePushOptions\n): Promise<PushResult> {\n const { plan, publisher, remoteState, repoReader, relayUrls } = options;\n\n // Merged sha→txId map: remote hints (fresh on resume) + plan-time finds.\n // Consulted before every upload — this is the resume-safety check.\n const merged = new Map([...remoteState.shaToTxId, ...plan.knownShaToTxId]);\n\n const resultBySha = new Map<string, UploadStepResult>();\n let totalFeePaid = 0n;\n\n const pending: PlannedObject[] = [];\n for (const object of plan.objects) {\n const knownTxId = merged.get(object.sha);\n if (knownTxId !== undefined) {\n resultBySha.set(object.sha, {\n sha: object.sha,\n txId: knownTxId,\n feePaid: 0n,\n skipped: true,\n });\n } else {\n pending.push(object);\n }\n }\n\n // Upload in plan order (ref tips are already last), reading bodies in\n // batches so memory stays bounded on large pushes.\n for (let i = 0; i < pending.length; i += READ_BATCH_SIZE) {\n const batch = pending.slice(i, i + READ_BATCH_SIZE);\n const { objects: read, missing } = await repoReader.readObjects(\n batch.map((o) => o.sha)\n );\n if (missing.length > 0) {\n throw new Error(\n `objects vanished from the local repository during push: ${missing.join(', ')}`\n );\n }\n const bodyBySha = new Map(read.map((r) => [r.sha, r.body]));\n for (const object of batch) {\n const body = bodyBySha.get(object.sha);\n if (!body) {\n throw new Error(\n `internal: cat-file returned no body for ${object.sha}`\n );\n }\n const receipt = await publisher.uploadGitObject({\n sha: object.sha,\n type: object.type,\n body,\n repoId: plan.repoId,\n });\n merged.set(object.sha, receipt.txId);\n totalFeePaid += receipt.feePaid;\n resultBySha.set(object.sha, {\n sha: object.sha,\n txId: receipt.txId,\n feePaid: receipt.feePaid,\n skipped: false,\n });\n }\n }\n\n // Announcement (first push only) goes before the refs event so a repo is\n // never referenced by an `a` tag before its kind:30617 exists. Re-check\n // the (fresh-on-resume) remote state so a crashed push doesn't announce\n // twice.\n let announceReceipt: PublishReceipt | null = null;\n if (plan.announceNeeded && !remoteState.announced) {\n const announceEvent = buildRepoAnnouncement(\n plan.repoId,\n plan.announcement.name,\n plan.announcement.description\n );\n announceReceipt = await publisher.publishEvent(announceEvent, relayUrls);\n totalFeePaid += announceReceipt.feePaid;\n }\n\n // ONE cumulative kind:30618: full ref state + MERGED arweave map (NIP-33\n // replaceable — dropping prior tags would orphan earlier sha→txId hints).\n const refsEvent = buildRepoRefs(\n plan.repoId,\n plan.newRefs,\n Object.fromEntries(merged)\n );\n const refsReceipt = await publisher.publishEvent(refsEvent, relayUrls);\n totalFeePaid += refsReceipt.feePaid;\n\n const uploads = plan.objects.map((o) => {\n const step = resultBySha.get(o.sha);\n if (!step) {\n throw new Error(`internal: no upload result recorded for ${o.sha}`);\n }\n return step;\n });\n\n return {\n uploads,\n announceReceipt,\n refsReceipt,\n arweaveMap: merged,\n totalFeePaid,\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
// src/nip34-events.ts
|
|
2
|
+
import {
|
|
3
|
+
ISSUE_KIND,
|
|
4
|
+
PATCH_KIND,
|
|
5
|
+
REPOSITORY_ANNOUNCEMENT_KIND
|
|
6
|
+
} from "@toon-protocol/core/nip34";
|
|
7
|
+
var REPOSITORY_STATE_KIND = 30618;
|
|
8
|
+
var COMMENT_KIND = 1622;
|
|
9
|
+
function buildRepoAnnouncement(repoId, name, description) {
|
|
10
|
+
return {
|
|
11
|
+
kind: REPOSITORY_ANNOUNCEMENT_KIND,
|
|
12
|
+
content: "",
|
|
13
|
+
tags: [
|
|
14
|
+
["d", repoId],
|
|
15
|
+
["name", name],
|
|
16
|
+
["description", description]
|
|
17
|
+
],
|
|
18
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
function buildRepoRefs(repoId, refs, arweaveMap = {}) {
|
|
22
|
+
const tags = [["d", repoId]];
|
|
23
|
+
for (const [refPath, commitSha] of Object.entries(refs)) {
|
|
24
|
+
tags.push(["r", refPath, commitSha]);
|
|
25
|
+
}
|
|
26
|
+
const firstRef = Object.keys(refs)[0];
|
|
27
|
+
if (firstRef) {
|
|
28
|
+
tags.push(["HEAD", `ref: ${firstRef}`]);
|
|
29
|
+
}
|
|
30
|
+
for (const [sha, txId] of Object.entries(arweaveMap)) {
|
|
31
|
+
tags.push(["arweave", sha, txId]);
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
kind: REPOSITORY_STATE_KIND,
|
|
35
|
+
content: "",
|
|
36
|
+
tags,
|
|
37
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
function buildIssue(repoOwnerPubkey, repoId, title, body, labels = []) {
|
|
41
|
+
const tags = [
|
|
42
|
+
["a", `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],
|
|
43
|
+
["p", repoOwnerPubkey],
|
|
44
|
+
["subject", title],
|
|
45
|
+
...labels.map((label) => ["t", label])
|
|
46
|
+
];
|
|
47
|
+
return {
|
|
48
|
+
kind: ISSUE_KIND,
|
|
49
|
+
content: body,
|
|
50
|
+
tags,
|
|
51
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function buildComment(repoOwnerPubkey, repoId, issueOrPrEventId, authorPubkey, body, marker = "reply") {
|
|
55
|
+
return {
|
|
56
|
+
kind: COMMENT_KIND,
|
|
57
|
+
content: body,
|
|
58
|
+
tags: [
|
|
59
|
+
["a", `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],
|
|
60
|
+
["e", issueOrPrEventId, "", marker],
|
|
61
|
+
["p", authorPubkey]
|
|
62
|
+
],
|
|
63
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
function buildPatch(repoOwnerPubkey, repoId, title, commits, branchTag, content = "") {
|
|
67
|
+
const tags = [
|
|
68
|
+
["a", `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],
|
|
69
|
+
["p", repoOwnerPubkey],
|
|
70
|
+
["subject", title]
|
|
71
|
+
];
|
|
72
|
+
for (const commit of commits) {
|
|
73
|
+
tags.push(["commit", commit.sha]);
|
|
74
|
+
tags.push(["parent-commit", commit.parentSha]);
|
|
75
|
+
}
|
|
76
|
+
if (branchTag) {
|
|
77
|
+
tags.push(["t", branchTag]);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
kind: PATCH_KIND,
|
|
81
|
+
content,
|
|
82
|
+
tags,
|
|
83
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
function buildStatus(targetEventId, statusKind, targetPubkey) {
|
|
87
|
+
const tags = [["e", targetEventId]];
|
|
88
|
+
if (targetPubkey) {
|
|
89
|
+
tags.push(["p", targetPubkey]);
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
kind: statusKind,
|
|
93
|
+
content: "",
|
|
94
|
+
tags,
|
|
95
|
+
created_at: Math.floor(Date.now() / 1e3)
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export {
|
|
100
|
+
REPOSITORY_STATE_KIND,
|
|
101
|
+
COMMENT_KIND,
|
|
102
|
+
buildRepoAnnouncement,
|
|
103
|
+
buildRepoRefs,
|
|
104
|
+
buildIssue,
|
|
105
|
+
buildComment,
|
|
106
|
+
buildPatch,
|
|
107
|
+
buildStatus
|
|
108
|
+
};
|
|
109
|
+
//# sourceMappingURL=chunk-KXXHAUXL.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/nip34-events.ts"],"sourcesContent":["/**\n * Pure NIP-34 event builders for the Git-to-TOON write path.\n *\n * Promoted from `packages/rig/tests/e2e/seed/lib/event-builders.ts` (#223).\n * All builders return UnsignedEvent — the caller signs with their keypair\n * via finalizeEvent() and publishes through a Publisher (#226). Tag\n * structures follow the NIP-34 spec and `@toon-protocol/core/nip34`.\n */\n\nimport {\n ISSUE_KIND,\n PATCH_KIND,\n REPOSITORY_ANNOUNCEMENT_KIND,\n} from '@toon-protocol/core/nip34';\nimport type {\n STATUS_APPLIED_KIND,\n STATUS_CLOSED_KIND,\n STATUS_DRAFT_KIND,\n STATUS_OPEN_KIND,\n} from '@toon-protocol/core/nip34';\n\n// Kinds not (yet) exported by @toon-protocol/core/nip34:\n/** Repository State (refs) — replaceable, pairs with kind:30617 via `d` tag. */\nexport const REPOSITORY_STATE_KIND = 30618;\n/** Comment on an issue or patch (NIP-22 style threading within NIP-34). */\nexport const COMMENT_KIND = 1622;\n\n// ---------------------------------------------------------------------------\n// UnsignedEvent type (subset of nostr-tools — no id, sig, or pubkey)\n// ---------------------------------------------------------------------------\n\nexport interface UnsignedEvent {\n kind: number;\n content: string;\n tags: string[][];\n created_at: number;\n}\n\n// ---------------------------------------------------------------------------\n// kind:30617 — Repository Announcement\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:30617 repository announcement event.\n *\n * @param repoId - Repository identifier (d tag)\n * @param name - Human-readable repository name\n * @param description - Repository description\n */\nexport function buildRepoAnnouncement(\n repoId: string,\n name: string,\n description: string\n): UnsignedEvent {\n return {\n kind: REPOSITORY_ANNOUNCEMENT_KIND,\n content: '',\n tags: [\n ['d', repoId],\n ['name', name],\n ['description', description],\n ],\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:30618 — Repository Refs/State\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:30618 repository refs/state event.\n *\n * @param repoId - Repository identifier (d tag, matches kind:30617)\n * @param refs - Map of ref paths to commit SHAs (e.g., { 'refs/heads/main': 'abc123' })\n * @param arweaveMap - Map of git SHAs to Arweave transaction IDs\n */\nexport function buildRepoRefs(\n repoId: string,\n refs: Record<string, string>,\n arweaveMap: Record<string, string> = {}\n): UnsignedEvent {\n const tags: string[][] = [['d', repoId]];\n\n // Add ref tags\n for (const [refPath, commitSha] of Object.entries(refs)) {\n tags.push(['r', refPath, commitSha]);\n }\n\n // Default HEAD to first ref (typically refs/heads/main)\n const firstRef = Object.keys(refs)[0];\n if (firstRef) {\n tags.push(['HEAD', `ref: ${firstRef}`]);\n }\n\n // Add arweave SHA-to-txId mapping tags\n for (const [sha, txId] of Object.entries(arweaveMap)) {\n tags.push(['arweave', sha, txId]);\n }\n\n return {\n kind: REPOSITORY_STATE_KIND,\n content: '',\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1621 — Issue\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1621 issue event.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param title - Issue title (subject tag)\n * @param body - Issue body (Markdown content)\n * @param labels - Optional labels (t tags)\n */\nexport function buildIssue(\n repoOwnerPubkey: string,\n repoId: string,\n title: string,\n body: string,\n labels: string[] = []\n): UnsignedEvent {\n const tags: string[][] = [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['p', repoOwnerPubkey],\n ['subject', title],\n ...labels.map((label) => ['t', label]),\n ];\n\n return {\n kind: ISSUE_KIND,\n content: body,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1622 — Comment (on issue or PR)\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1622 comment event.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param issueOrPrEventId - Event ID of the issue or PR being commented on\n * @param authorPubkey - Pubkey of the issue/PR author (NIP-34 `p` tag for threading), NOT the comment author\n * @param body - Comment body (Markdown content)\n * @param marker - Event reference marker: 'root' or 'reply' (default: 'reply')\n */\nexport function buildComment(\n repoOwnerPubkey: string,\n repoId: string,\n issueOrPrEventId: string,\n authorPubkey: string,\n body: string,\n marker: 'root' | 'reply' = 'reply'\n): UnsignedEvent {\n return {\n kind: COMMENT_KIND,\n content: body,\n tags: [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['e', issueOrPrEventId, '', marker],\n ['p', authorPubkey],\n ],\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1617 — Patch / PR\n// ---------------------------------------------------------------------------\n\n/**\n * Build a kind:1617 patch event.\n *\n * @param repoOwnerPubkey - Pubkey of the repository owner\n * @param repoId - Repository identifier\n * @param title - Patch/PR title (subject tag)\n * @param commits - Array of { sha, parentSha } for commit and parent-commit tags\n * @param branchTag - Branch name for the t tag\n * @param content - Real `git format-patch` text (NIP-34 patch body); defaults\n * to '' for callers that only reference commits by tag\n */\nexport function buildPatch(\n repoOwnerPubkey: string,\n repoId: string,\n title: string,\n commits: { sha: string; parentSha: string }[],\n branchTag?: string,\n content = ''\n): UnsignedEvent {\n const tags: string[][] = [\n ['a', `${REPOSITORY_ANNOUNCEMENT_KIND}:${repoOwnerPubkey}:${repoId}`],\n ['p', repoOwnerPubkey],\n ['subject', title],\n ];\n\n for (const commit of commits) {\n tags.push(['commit', commit.sha]);\n tags.push(['parent-commit', commit.parentSha]);\n }\n\n if (branchTag) {\n tags.push(['t', branchTag]);\n }\n\n return {\n kind: PATCH_KIND,\n content,\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n\n// ---------------------------------------------------------------------------\n// kind:1630-1633 — Status\n// ---------------------------------------------------------------------------\n\n/** Status kinds: 1630 open, 1631 applied/merged, 1632 closed, 1633 draft. */\nexport type StatusKind =\n | typeof STATUS_OPEN_KIND\n | typeof STATUS_APPLIED_KIND\n | typeof STATUS_CLOSED_KIND\n | typeof STATUS_DRAFT_KIND;\n\n/**\n * Build a status event (kind 1630-1633).\n *\n * @param targetEventId - Event ID of the patch, PR, or issue being updated\n * @param statusKind - One of 1630 (open), 1631 (applied), 1632 (closed), 1633 (draft)\n * @param targetPubkey - Optional pubkey of the target event author (p tag per NIP-34 StatusEvent)\n */\nexport function buildStatus(\n targetEventId: string,\n statusKind: StatusKind,\n targetPubkey?: string\n): UnsignedEvent {\n const tags: string[][] = [['e', targetEventId]];\n if (targetPubkey) {\n tags.push(['p', targetPubkey]);\n }\n return {\n kind: statusKind,\n content: '',\n tags,\n created_at: Math.floor(Date.now() / 1000),\n };\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// src/standalone/nonce-guard.ts
|
|
2
|
+
import { mkdirSync, readFileSync, unlinkSync, writeFileSync } from "fs";
|
|
3
|
+
import { homedir } from "os";
|
|
4
|
+
import { join } from "path";
|
|
5
|
+
var DEFAULT_DAEMON_PORT = 8787;
|
|
6
|
+
function defaultDaemonPort() {
|
|
7
|
+
const env = process.env["TOON_CLIENT_HTTP_PORT"];
|
|
8
|
+
const parsed = env ? Number(env) : NaN;
|
|
9
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;
|
|
10
|
+
}
|
|
11
|
+
function defaultLockDir() {
|
|
12
|
+
return process.env["TOON_CLIENT_HOME"] ?? join(homedir(), ".toon-client");
|
|
13
|
+
}
|
|
14
|
+
var DaemonIdentityConflictError = class extends Error {
|
|
15
|
+
constructor(pubkey, daemonUrl) {
|
|
16
|
+
super(
|
|
17
|
+
`toon-clientd is running with this identity (${pubkey.slice(0, 8)}\u2026) at ${daemonUrl} \u2014 use daemon mode or stop the daemon. Two writers on one identity would race the payment channel's cumulative-claim watermark (double-charge hazard).`
|
|
18
|
+
);
|
|
19
|
+
this.pubkey = pubkey;
|
|
20
|
+
this.daemonUrl = daemonUrl;
|
|
21
|
+
this.name = "DaemonIdentityConflictError";
|
|
22
|
+
}
|
|
23
|
+
pubkey;
|
|
24
|
+
daemonUrl;
|
|
25
|
+
};
|
|
26
|
+
var StandaloneLockError = class extends Error {
|
|
27
|
+
constructor(pubkey, lockPath, holderPid) {
|
|
28
|
+
super(
|
|
29
|
+
`another standalone process (pid ${holderPid}) already holds the payment-channel lock for identity ${pubkey.slice(0, 8)}\u2026 (${lockPath}) \u2014 wait for it to finish or stop it. Two writers on one identity would race the cumulative-claim watermark.`
|
|
30
|
+
);
|
|
31
|
+
this.pubkey = pubkey;
|
|
32
|
+
this.lockPath = lockPath;
|
|
33
|
+
this.holderPid = holderPid;
|
|
34
|
+
this.name = "StandaloneLockError";
|
|
35
|
+
}
|
|
36
|
+
pubkey;
|
|
37
|
+
lockPath;
|
|
38
|
+
holderPid;
|
|
39
|
+
};
|
|
40
|
+
async function checkDaemonIdentity(pubkey, options = {}) {
|
|
41
|
+
const port = options.port ?? defaultDaemonPort();
|
|
42
|
+
const fetchImpl = options.fetchImpl ?? fetch;
|
|
43
|
+
const url = `http://127.0.0.1:${port}/status`;
|
|
44
|
+
let daemonPubkey;
|
|
45
|
+
try {
|
|
46
|
+
const res = await fetchImpl(url, {
|
|
47
|
+
signal: AbortSignal.timeout(options.timeoutMs ?? 1500)
|
|
48
|
+
});
|
|
49
|
+
if (!res.ok) return;
|
|
50
|
+
const body = await res.json();
|
|
51
|
+
const candidate = body?.identity?.nostrPubkey;
|
|
52
|
+
if (typeof candidate === "string") daemonPubkey = candidate;
|
|
53
|
+
} catch {
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
if (daemonPubkey !== void 0 && daemonPubkey === pubkey) {
|
|
57
|
+
throw new DaemonIdentityConflictError(pubkey, url);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
function pidAlive(pid) {
|
|
61
|
+
try {
|
|
62
|
+
process.kill(pid, 0);
|
|
63
|
+
return true;
|
|
64
|
+
} catch (err) {
|
|
65
|
+
return err.code === "EPERM";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
var NonceLock = class _NonceLock {
|
|
69
|
+
constructor(pubkey, lockPath) {
|
|
70
|
+
this.pubkey = pubkey;
|
|
71
|
+
this.lockPath = lockPath;
|
|
72
|
+
this.exitHandler = () => {
|
|
73
|
+
try {
|
|
74
|
+
unlinkSync(this.lockPath);
|
|
75
|
+
} catch {
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
process.once("exit", this.exitHandler);
|
|
79
|
+
}
|
|
80
|
+
pubkey;
|
|
81
|
+
lockPath;
|
|
82
|
+
released = false;
|
|
83
|
+
exitHandler;
|
|
84
|
+
static async acquire(pubkey, options = {}) {
|
|
85
|
+
const dir = options.dir ?? defaultLockDir();
|
|
86
|
+
const pid = options.pid ?? process.pid;
|
|
87
|
+
const lockPath = join(dir, `standalone-${pubkey}.lock`);
|
|
88
|
+
mkdirSync(dir, { recursive: true });
|
|
89
|
+
const payload = JSON.stringify(
|
|
90
|
+
{
|
|
91
|
+
pid,
|
|
92
|
+
pubkey,
|
|
93
|
+
createdAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
94
|
+
},
|
|
95
|
+
null,
|
|
96
|
+
2
|
|
97
|
+
);
|
|
98
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
99
|
+
try {
|
|
100
|
+
writeFileSync(lockPath, payload, { flag: "wx" });
|
|
101
|
+
return new _NonceLock(pubkey, lockPath);
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.code !== "EEXIST") throw err;
|
|
104
|
+
let holderPid;
|
|
105
|
+
try {
|
|
106
|
+
const parsed = JSON.parse(
|
|
107
|
+
readFileSync(lockPath, "utf8")
|
|
108
|
+
);
|
|
109
|
+
if (typeof parsed.pid === "number") holderPid = parsed.pid;
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
if (holderPid !== void 0 && holderPid !== pid && pidAlive(holderPid)) {
|
|
113
|
+
throw new StandaloneLockError(pubkey, lockPath, holderPid);
|
|
114
|
+
}
|
|
115
|
+
try {
|
|
116
|
+
unlinkSync(lockPath);
|
|
117
|
+
} catch {
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
throw new StandaloneLockError(pubkey, lockPath, -1);
|
|
122
|
+
}
|
|
123
|
+
/** Remove the lockfile and detach the exit hook. Idempotent. */
|
|
124
|
+
release() {
|
|
125
|
+
if (this.released) return;
|
|
126
|
+
this.released = true;
|
|
127
|
+
process.removeListener("exit", this.exitHandler);
|
|
128
|
+
try {
|
|
129
|
+
unlinkSync(this.lockPath);
|
|
130
|
+
} catch {
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
export {
|
|
136
|
+
DEFAULT_DAEMON_PORT,
|
|
137
|
+
defaultDaemonPort,
|
|
138
|
+
defaultLockDir,
|
|
139
|
+
DaemonIdentityConflictError,
|
|
140
|
+
StandaloneLockError,
|
|
141
|
+
checkDaemonIdentity,
|
|
142
|
+
NonceLock
|
|
143
|
+
};
|
|
144
|
+
//# sourceMappingURL=chunk-LJA7PPZI.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/standalone/nonce-guard.ts"],"sourcesContent":["/**\n * Nonce-ownership guard for the STANDALONE embedded Publisher (#228).\n *\n * Why this exists: a payment channel's balance proof is a CUMULATIVE\n * watermark — the ChannelManager auto-increments the nonce and cumulative\n * amount on every `signBalanceProof`. Two writers signing claims on the same\n * channel from separate processes (a running `toon-clientd` daemon plus a\n * standalone embedded client, or two standalone processes) each keep their\n * own cumulative counter, so their claims race: the connector sees\n * non-monotonic watermarks and a re-signed claim can double-charge (the\n * hazard documented in packages/rig/tests/e2e/seed/lib/publish.ts).\n *\n * Two independent defenses, both keyed by the Nostr pubkey (one identity =\n * one channel set):\n *\n * 1. Daemon detection — probe the toon-clientd loopback control API\n * (`GET /status`) and REFUSE when it reports the SAME identity. A daemon\n * on a different identity holds different channels and is harmless.\n * 2. Advisory lockfile — an exclusive per-pubkey lockfile under the shared\n * `~/.toon-client` state dir so two STANDALONE processes can't race each\n * other (the daemon check only covers the daemon). Stale locks (dead pid)\n * are reclaimed.\n *\n * The daemon port and state-dir conventions are DUPLICATED from\n * `packages/client-mcp/src/daemon/config.ts` (default port 8787 /\n * `TOON_CLIENT_HTTP_PORT`; `~/.toon-client` / `TOON_CLIENT_HOME`).\n * `@toon-protocol/git` must not depend on `@toon-protocol/client-mcp`\n * (the daemon package depends on this one for the #227 Publisher — the\n * import would be circular), so the constants live here with this note.\n * Keep them in sync.\n */\n\nimport { mkdirSync, readFileSync, unlinkSync, writeFileSync } from 'node:fs';\nimport { homedir } from 'node:os';\nimport { join } from 'node:path';\n\n// ---------------------------------------------------------------------------\n// Shared conventions (duplicated from client-mcp — see module doc)\n// ---------------------------------------------------------------------------\n\n/** Default toon-clientd loopback control API port (client-mcp `httpPort`). */\nexport const DEFAULT_DAEMON_PORT = 8787;\n\n/** Daemon control API port: `TOON_CLIENT_HTTP_PORT` env, else 8787. */\nexport function defaultDaemonPort(): number {\n const env = process.env['TOON_CLIENT_HTTP_PORT'];\n const parsed = env ? Number(env) : NaN;\n return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_DAEMON_PORT;\n}\n\n/**\n * Shared client state dir: `TOON_CLIENT_HOME` env, else `~/.toon-client` —\n * the same dir the daemon keeps its config/channel stores in, so daemon and\n * standalone processes agree on where the advisory locks live.\n */\nexport function defaultLockDir(): string {\n return process.env['TOON_CLIENT_HOME'] ?? join(homedir(), '.toon-client');\n}\n\n// ---------------------------------------------------------------------------\n// Errors\n// ---------------------------------------------------------------------------\n\n/** A running toon-clientd holds the same identity (channel watermark owner). */\nexport class DaemonIdentityConflictError extends Error {\n constructor(\n /** The shared Nostr pubkey (hex). */\n public readonly pubkey: string,\n /** The daemon control API URL that answered. */\n public readonly daemonUrl: string\n ) {\n super(\n `toon-clientd is running with this identity (${pubkey.slice(0, 8)}…) at ` +\n `${daemonUrl} — use daemon mode or stop the daemon. Two writers on one ` +\n `identity would race the payment channel's cumulative-claim watermark ` +\n `(double-charge hazard).`\n );\n this.name = 'DaemonIdentityConflictError';\n }\n}\n\n/** Another standalone process already holds the per-identity lock. */\nexport class StandaloneLockError extends Error {\n constructor(\n public readonly pubkey: string,\n public readonly lockPath: string,\n public readonly holderPid: number\n ) {\n super(\n `another standalone process (pid ${holderPid}) already holds the ` +\n `payment-channel lock for identity ${pubkey.slice(0, 8)}… ` +\n `(${lockPath}) — wait for it to finish or stop it. Two writers on one ` +\n `identity would race the cumulative-claim watermark.`\n );\n this.name = 'StandaloneLockError';\n }\n}\n\n// ---------------------------------------------------------------------------\n// Daemon detection\n// ---------------------------------------------------------------------------\n\nexport interface CheckDaemonOptions {\n /** Control API port (default: `TOON_CLIENT_HTTP_PORT` env, else 8787). */\n port?: number;\n /** Probe timeout, ms (default 1500 — loopback, so fast). */\n timeoutMs?: number;\n /** Inject a fetch implementation (tests). Defaults to global `fetch`. */\n fetchImpl?: typeof fetch;\n}\n\n/**\n * Probe the toon-clientd loopback control API and throw\n * {@link DaemonIdentityConflictError} when a daemon responds on `/status`\n * with `identity.nostrPubkey === pubkey`.\n *\n * Anything short of a positive identity match lets the caller proceed: no\n * listener, a timeout, a non-JSON response (some other local service on the\n * port), or a daemon on a DIFFERENT identity (its channels are keyed to its\n * own pubkey — no shared watermark).\n */\nexport async function checkDaemonIdentity(\n pubkey: string,\n options: CheckDaemonOptions = {}\n): Promise<void> {\n const port = options.port ?? defaultDaemonPort();\n const fetchImpl = options.fetchImpl ?? fetch;\n const url = `http://127.0.0.1:${port}/status`;\n\n let daemonPubkey: string | undefined;\n try {\n const res = await fetchImpl(url, {\n signal: AbortSignal.timeout(options.timeoutMs ?? 1500),\n });\n if (!res.ok) return; // listening, but not a healthy daemon status\n const body = (await res.json()) as {\n identity?: { nostrPubkey?: unknown };\n };\n const candidate = body?.identity?.nostrPubkey;\n if (typeof candidate === 'string') daemonPubkey = candidate;\n } catch {\n // Unreachable / timed out / not JSON → no same-identity daemon detected.\n return;\n }\n\n if (daemonPubkey !== undefined && daemonPubkey === pubkey) {\n throw new DaemonIdentityConflictError(pubkey, url);\n }\n}\n\n// ---------------------------------------------------------------------------\n// Advisory per-identity lockfile\n// ---------------------------------------------------------------------------\n\ninterface LockFileContents {\n pid: number;\n pubkey: string;\n createdAt: string;\n}\n\nexport interface AcquireLockOptions {\n /** Directory the lockfile lives in (default: {@link defaultLockDir}). */\n dir?: string;\n /** Override the recorded pid (tests). Defaults to `process.pid`. */\n pid?: number;\n}\n\n/** True when `pid` refers to a live process we can see. */\nfunction pidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err) {\n // EPERM = alive but not ours; ESRCH (and anything else) = not running.\n return (err as NodeJS.ErrnoException).code === 'EPERM';\n }\n}\n\n/**\n * Exclusive advisory lock for one identity's payment-channel watermark.\n *\n * Acquired with an atomic `wx` create of `standalone-<pubkey>.lock` (JSON:\n * pid + pubkey + timestamp) under the shared state dir. A pre-existing lock\n * whose pid is dead (or whose contents are unreadable) is STALE and gets\n * reclaimed; a live holder throws {@link StandaloneLockError}. Released\n * explicitly via {@link release} and best-effort on process exit.\n */\nexport class NonceLock {\n private released = false;\n private readonly exitHandler: () => void;\n\n private constructor(\n public readonly pubkey: string,\n public readonly lockPath: string\n ) {\n this.exitHandler = () => {\n try {\n unlinkSync(this.lockPath);\n } catch {\n // best-effort — the pid check makes a leftover lock reclaimable anyway\n }\n };\n process.once('exit', this.exitHandler);\n }\n\n static async acquire(\n pubkey: string,\n options: AcquireLockOptions = {}\n ): Promise<NonceLock> {\n const dir = options.dir ?? defaultLockDir();\n const pid = options.pid ?? process.pid;\n const lockPath = join(dir, `standalone-${pubkey}.lock`);\n mkdirSync(dir, { recursive: true });\n\n const payload = JSON.stringify(\n {\n pid,\n pubkey,\n createdAt: new Date().toISOString(),\n } satisfies LockFileContents,\n null,\n 2\n );\n\n // Two attempts: initial exclusive create, then one retry after reclaiming\n // a stale lock. A live holder on either attempt is a hard refusal.\n for (let attempt = 0; attempt < 2; attempt++) {\n try {\n writeFileSync(lockPath, payload, { flag: 'wx' });\n return new NonceLock(pubkey, lockPath);\n } catch (err) {\n if ((err as NodeJS.ErrnoException).code !== 'EEXIST') throw err;\n\n let holderPid: number | undefined;\n try {\n const parsed = JSON.parse(\n readFileSync(lockPath, 'utf8')\n ) as Partial<LockFileContents>;\n if (typeof parsed.pid === 'number') holderPid = parsed.pid;\n } catch {\n // Unreadable/corrupt lock → treat as stale.\n }\n\n // Same process re-acquiring (e.g. a retried push in one CLI run) is\n // not a race — the ChannelManager watermark is shared in-process.\n if (\n holderPid !== undefined &&\n holderPid !== pid &&\n pidAlive(holderPid)\n ) {\n throw new StandaloneLockError(pubkey, lockPath, holderPid);\n }\n\n // Stale (dead pid / corrupt / our own pid): reclaim and retry.\n try {\n unlinkSync(lockPath);\n } catch {\n // Lost a reclaim race with another process — the retry's exclusive\n // create settles the winner.\n }\n }\n }\n // Both attempts hit EEXIST → another process is actively (re)creating it.\n throw new StandaloneLockError(pubkey, lockPath, -1);\n }\n\n /** Remove the lockfile and detach the exit hook. Idempotent. */\n release(): void {\n if (this.released) return;\n this.released = true;\n process.removeListener('exit', this.exitHandler);\n try {\n unlinkSync(this.lockPath);\n } catch {\n // already gone — fine\n }\n }\n}\n"],"mappings":"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","names":[]}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
// src/objects.ts
|
|
2
|
+
import { createHash } from "crypto";
|
|
3
|
+
var MAX_OBJECT_SIZE = 95 * 1024;
|
|
4
|
+
function hashGitObject(type, body) {
|
|
5
|
+
const header = Buffer.from(`${type} ${body.length}\0`);
|
|
6
|
+
const fullObject = Buffer.concat([header, body]);
|
|
7
|
+
const sha = createHash("sha1").update(fullObject).digest("hex");
|
|
8
|
+
return { sha, buffer: fullObject, body };
|
|
9
|
+
}
|
|
10
|
+
function createGitBlob(content) {
|
|
11
|
+
return hashGitObject("blob", Buffer.from(content, "utf-8"));
|
|
12
|
+
}
|
|
13
|
+
function createGitTree(entries) {
|
|
14
|
+
const sorted = [...entries].sort(
|
|
15
|
+
(a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0
|
|
16
|
+
);
|
|
17
|
+
const entryBuffers = [];
|
|
18
|
+
for (const entry of sorted) {
|
|
19
|
+
const modeAndName = Buffer.from(`${entry.mode} ${entry.name}\0`);
|
|
20
|
+
const rawSha = Buffer.from(entry.sha, "hex");
|
|
21
|
+
entryBuffers.push(Buffer.concat([modeAndName, rawSha]));
|
|
22
|
+
}
|
|
23
|
+
return hashGitObject("tree", Buffer.concat(entryBuffers));
|
|
24
|
+
}
|
|
25
|
+
function createGitCommit(opts) {
|
|
26
|
+
const lines = [
|
|
27
|
+
`tree ${opts.treeSha}`,
|
|
28
|
+
...opts.parentSha ? [`parent ${opts.parentSha}`] : [],
|
|
29
|
+
`author ${opts.authorName} <${opts.authorPubkey}@nostr> ${opts.timestamp} +0000`,
|
|
30
|
+
`committer ${opts.authorName} <${opts.authorPubkey}@nostr> ${opts.timestamp} +0000`,
|
|
31
|
+
"",
|
|
32
|
+
opts.message
|
|
33
|
+
];
|
|
34
|
+
return hashGitObject("commit", Buffer.from(lines.join("\n"), "utf-8"));
|
|
35
|
+
}
|
|
36
|
+
function createGitTag(opts) {
|
|
37
|
+
const lines = [
|
|
38
|
+
`object ${opts.objectSha}`,
|
|
39
|
+
`type ${opts.objectType}`,
|
|
40
|
+
`tag ${opts.tagName}`,
|
|
41
|
+
`tagger ${opts.taggerName} <${opts.taggerPubkey}@nostr> ${opts.timestamp} +0000`,
|
|
42
|
+
"",
|
|
43
|
+
opts.message
|
|
44
|
+
];
|
|
45
|
+
return hashGitObject("tag", Buffer.from(lines.join("\n"), "utf-8"));
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export {
|
|
49
|
+
MAX_OBJECT_SIZE,
|
|
50
|
+
hashGitObject,
|
|
51
|
+
createGitBlob,
|
|
52
|
+
createGitTree,
|
|
53
|
+
createGitCommit,
|
|
54
|
+
createGitTag
|
|
55
|
+
};
|
|
56
|
+
//# sourceMappingURL=chunk-M7O4SEVW.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/objects.ts"],"sourcesContent":["/**\n * Pure git object construction and SHA-1 envelope hashing.\n *\n * Promoted from `packages/rig/tests/e2e/seed/lib/git-builder.ts` (#223) —\n * the proven seed pipeline builders, now the core of the Git-to-TOON write\n * path. Everything here is pure: no network, no signing, no payments.\n * Upload/publish lives with the Publisher (#226).\n *\n * Git object format: `<type> <size>\\0<content>`. The SHA-1 is computed over\n * the full envelope (header + NUL + content); the `body` (content only) is\n * what gets uploaded to Arweave.\n */\n\nimport { createHash } from 'node:crypto';\n\n// ---------------------------------------------------------------------------\n// Types\n// ---------------------------------------------------------------------------\n\n/** All git object types TOON can carry. */\nexport type GitObjectType = 'blob' | 'tree' | 'commit' | 'tag';\n\nexport interface GitObject {\n /** SHA-1 hex digest computed over full git envelope */\n sha: string;\n /** Full git object (header + null + content) */\n buffer: Buffer;\n /** Body only (content after the null byte) — this is what gets uploaded */\n body: Buffer;\n}\n\n/**\n * Maximum uploadable git object body size: 95KB safety margin under the\n * 100KB free tier (R10-005). Larger objects are a hard error in v1.\n */\nexport const MAX_OBJECT_SIZE = 95 * 1024;\n\n// ---------------------------------------------------------------------------\n// Envelope hashing\n// ---------------------------------------------------------------------------\n\n/**\n * Wrap a raw object body in the git envelope (`<type> <size>\\0`) and compute\n * its SHA-1. This is exactly what `git hash-object -t <type>` does.\n */\nexport function hashGitObject(type: GitObjectType, body: Buffer): GitObject {\n const header = Buffer.from(`${type} ${body.length}\\0`);\n const fullObject = Buffer.concat([header, body]);\n const sha = createHash('sha1').update(fullObject).digest('hex');\n return { sha, buffer: fullObject, body };\n}\n\n// ---------------------------------------------------------------------------\n// Git object construction\n// ---------------------------------------------------------------------------\n\n/**\n * Construct a git blob object and compute its SHA-1.\n *\n * Format: blob <size>\\0<content>\n * SHA is over the full envelope; body is content only (for upload).\n */\nexport function createGitBlob(content: string): GitObject {\n return hashGitObject('blob', Buffer.from(content, 'utf-8'));\n}\n\n/**\n * Construct a git tree object from sorted entries.\n *\n * Format: tree <size>\\0<entries>\n * Each entry: <mode> <name>\\0<20-byte-raw-sha1>\n * Entries MUST be sorted by name (byte-wise).\n */\nexport function createGitTree(\n entries: { mode: string; name: string; sha: string }[]\n): GitObject {\n // Git sorts tree entries by raw byte order (NOT locale-aware)\n const sorted = [...entries].sort((a, b) =>\n a.name < b.name ? -1 : a.name > b.name ? 1 : 0\n );\n\n const entryBuffers: Buffer[] = [];\n for (const entry of sorted) {\n const modeAndName = Buffer.from(`${entry.mode} ${entry.name}\\0`);\n // Raw 20-byte SHA-1 (NOT hex)\n const rawSha = Buffer.from(entry.sha, 'hex');\n entryBuffers.push(Buffer.concat([modeAndName, rawSha]));\n }\n\n return hashGitObject('tree', Buffer.concat(entryBuffers));\n}\n\n/**\n * Construct a git commit object.\n *\n * Format: commit <size>\\0tree <tree-sha>\\n[parent ...]\\nauthor ...\\ncommitter ...\\n\\n<message>\n * Tree/parent SHAs are hex-encoded (40 chars) in commits, unlike tree entries.\n */\nexport function createGitCommit(opts: {\n treeSha: string;\n parentSha?: string;\n authorName: string;\n authorPubkey: string;\n message: string;\n timestamp: number;\n}): GitObject {\n const lines = [\n `tree ${opts.treeSha}`,\n ...(opts.parentSha ? [`parent ${opts.parentSha}`] : []),\n `author ${opts.authorName} <${opts.authorPubkey}@nostr> ${opts.timestamp} +0000`,\n `committer ${opts.authorName} <${opts.authorPubkey}@nostr> ${opts.timestamp} +0000`,\n '',\n opts.message,\n ];\n return hashGitObject('commit', Buffer.from(lines.join('\\n'), 'utf-8'));\n}\n\n/**\n * Construct an annotated git tag object.\n *\n * Format: tag <size>\\0object <sha>\\ntype <type>\\ntag <name>\\ntagger ...\\n\\n<message>\n * The tagged object is usually a commit, but git allows tagging any object\n * type (including another tag).\n */\nexport function createGitTag(opts: {\n /** SHA-1 of the object being tagged (hex, 40 chars) */\n objectSha: string;\n /** Type of the tagged object (usually 'commit') */\n objectType: GitObjectType;\n /** Tag name, e.g. 'v1.0.0' */\n tagName: string;\n taggerName: string;\n taggerPubkey: string;\n message: string;\n timestamp: number;\n}): GitObject {\n const lines = [\n `object ${opts.objectSha}`,\n `type ${opts.objectType}`,\n `tag ${opts.tagName}`,\n `tagger ${opts.taggerName} <${opts.taggerPubkey}@nostr> ${opts.timestamp} +0000`,\n '',\n opts.message,\n ];\n return hashGitObject('tag', Buffer.from(lines.join('\\n'), 'utf-8'));\n}\n"],"mappings":";AAaA,SAAS,kBAAkB;AAsBpB,IAAM,kBAAkB,KAAK;AAU7B,SAAS,cAAc,MAAqB,MAAyB;AAC1E,QAAM,SAAS,OAAO,KAAK,GAAG,IAAI,IAAI,KAAK,MAAM,IAAI;AACrD,QAAM,aAAa,OAAO,OAAO,CAAC,QAAQ,IAAI,CAAC;AAC/C,QAAM,MAAM,WAAW,MAAM,EAAE,OAAO,UAAU,EAAE,OAAO,KAAK;AAC9D,SAAO,EAAE,KAAK,QAAQ,YAAY,KAAK;AACzC;AAYO,SAAS,cAAc,SAA4B;AACxD,SAAO,cAAc,QAAQ,OAAO,KAAK,SAAS,OAAO,CAAC;AAC5D;AASO,SAAS,cACd,SACW;AAEX,QAAM,SAAS,CAAC,GAAG,OAAO,EAAE;AAAA,IAAK,CAAC,GAAG,MACnC,EAAE,OAAO,EAAE,OAAO,KAAK,EAAE,OAAO,EAAE,OAAO,IAAI;AAAA,EAC/C;AAEA,QAAM,eAAyB,CAAC;AAChC,aAAW,SAAS,QAAQ;AAC1B,UAAM,cAAc,OAAO,KAAK,GAAG,MAAM,IAAI,IAAI,MAAM,IAAI,IAAI;AAE/D,UAAM,SAAS,OAAO,KAAK,MAAM,KAAK,KAAK;AAC3C,iBAAa,KAAK,OAAO,OAAO,CAAC,aAAa,MAAM,CAAC,CAAC;AAAA,EACxD;AAEA,SAAO,cAAc,QAAQ,OAAO,OAAO,YAAY,CAAC;AAC1D;AAQO,SAAS,gBAAgB,MAOlB;AACZ,QAAM,QAAQ;AAAA,IACZ,QAAQ,KAAK,OAAO;AAAA,IACpB,GAAI,KAAK,YAAY,CAAC,UAAU,KAAK,SAAS,EAAE,IAAI,CAAC;AAAA,IACrD,UAAU,KAAK,UAAU,KAAK,KAAK,YAAY,WAAW,KAAK,SAAS;AAAA,IACxE,aAAa,KAAK,UAAU,KAAK,KAAK,YAAY,WAAW,KAAK,SAAS;AAAA,IAC3E;AAAA,IACA,KAAK;AAAA,EACP;AACA,SAAO,cAAc,UAAU,OAAO,KAAK,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;AACvE;AASO,SAAS,aAAa,MAWf;AACZ,QAAM,QAAQ;AAAA,IACZ,UAAU,KAAK,SAAS;AAAA,IACxB,QAAQ,KAAK,UAAU;AAAA,IACvB,OAAO,KAAK,OAAO;AAAA,IACnB,UAAU,KAAK,UAAU,KAAK,KAAK,YAAY,WAAW,KAAK,SAAS;AAAA,IACxE;AAAA,IACA,KAAK;AAAA,EACP;AACA,SAAO,cAAc,OAAO,OAAO,KAAK,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC;AACpE;","names":[]}
|