@orangecheck/stamp-cli 0.1.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/LICENSE +21 -0
- package/README.md +113 -0
- package/dist/cli.js +452 -0
- package/dist/cli.js.map +1 -0
- package/package.json +58 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 OrangeCheck
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
# @orangecheck/stamp-cli
|
|
2
|
+
|
|
3
|
+
Shell interface to [OC Stamp](https://github.com/orangecheck/oc-stamp-protocol). Two binaries from one package:
|
|
4
|
+
|
|
5
|
+
- **`stamp`** — sign files, verify envelopes, anchor to Bitcoin, dry-run canonical messages.
|
|
6
|
+
- **`git-stamp`** — stamp git tags. Replaces the GPG dance for release signing.
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
```sh
|
|
11
|
+
npm i -g @orangecheck/stamp-cli
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
## `stamp` — file stamping
|
|
15
|
+
|
|
16
|
+
```sh
|
|
17
|
+
# Sign a file. Prints the canonical message, prompts for a BIP-322 signature
|
|
18
|
+
# from your wallet, writes <path>.stamp alongside, submits to OTS calendars.
|
|
19
|
+
$ stamp file blogpost.md --addr bc1qalice...
|
|
20
|
+
|
|
21
|
+
# Non-interactive (sig pre-computed elsewhere):
|
|
22
|
+
$ stamp file blogpost.md --addr bc1qalice... --sig '...'
|
|
23
|
+
|
|
24
|
+
# Skip anchoring (sign only):
|
|
25
|
+
$ stamp file blogpost.md --addr bc1qalice... --no-anchor
|
|
26
|
+
|
|
27
|
+
# Dry run — print what would be signed, don't sign:
|
|
28
|
+
$ stamp canonical blogpost.md --addr bc1qalice...
|
|
29
|
+
|
|
30
|
+
# Verify a stamp:
|
|
31
|
+
$ stamp verify blogpost.md.stamp blogpost.md --require-anchor
|
|
32
|
+
|
|
33
|
+
# Retry anchoring on an existing stamp:
|
|
34
|
+
$ stamp anchor blogpost.md.stamp
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## `git-stamp` — release/tag signing
|
|
38
|
+
|
|
39
|
+
```sh
|
|
40
|
+
# Tag + stamp:
|
|
41
|
+
$ git tag v2.1.0 -m 'release'
|
|
42
|
+
$ git-stamp tag v2.1.0 --addr bc1qalice...
|
|
43
|
+
|
|
44
|
+
# Later, anyone verifies:
|
|
45
|
+
$ git-stamp verify v2.1.0 --require-anchor
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The stamp is stored at `.git/stamps/<tag>.stamp` — co-located with the repo, discoverable by `git-stamp verify` without a separate config.
|
|
49
|
+
|
|
50
|
+
The signing domain covers the tag's tree hash, the tag object id, and the tag name. Changing any of them invalidates the stamp.
|
|
51
|
+
|
|
52
|
+
## `stamp verify` — full SPEC §8 checks
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
$ stamp verify blogpost.md.stamp blogpost.md --json
|
|
56
|
+
{
|
|
57
|
+
"ok": true,
|
|
58
|
+
"id": "f0dd79a528ab2c75...",
|
|
59
|
+
"signer": "bc1qalice...",
|
|
60
|
+
"signed_at": "2026-04-24T18:30:00Z",
|
|
61
|
+
"content_hash": "sha256:a4c8f7d2...",
|
|
62
|
+
"content_mime": "text/markdown",
|
|
63
|
+
"content_length": 12843,
|
|
64
|
+
"content_checked": true,
|
|
65
|
+
"anchor": "confirmed at block 890123",
|
|
66
|
+
"signature_checked": true,
|
|
67
|
+
"stake": null
|
|
68
|
+
}
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
On failure, exits `2` with an error code:
|
|
72
|
+
|
|
73
|
+
```
|
|
74
|
+
$ stamp verify blogpost.md.stamp other-content.md --json
|
|
75
|
+
{ "ok": false, "code": "E_BAD_CONTENT", "message": "..." }
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
## `--json` output
|
|
79
|
+
|
|
80
|
+
All commands accept `--json` for machine-readable output. Exit codes: `0` success, `1` user error (bad input), `2` verification failure.
|
|
81
|
+
|
|
82
|
+
## How the signing flow works
|
|
83
|
+
|
|
84
|
+
BIP-322 signing in a CLI can't drive every wallet the way the browser can. `stamp-cli` takes the explicit approach: print the canonical message, let you sign it with whatever wallet you have (UniSat, Xverse, Leather, Sparrow, Electrum, `bitcoind signmessage`, etc.), paste the signature back.
|
|
85
|
+
|
|
86
|
+
```
|
|
87
|
+
$ stamp file blogpost.md --addr bc1qalice...
|
|
88
|
+
|
|
89
|
+
Sign this message with your Bitcoin wallet (BIP-322):
|
|
90
|
+
|
|
91
|
+
────────────────────────────────────────────────────────────
|
|
92
|
+
oc-stamp:v1
|
|
93
|
+
address: bc1qalice...
|
|
94
|
+
content_hash: sha256:a4c8f7d2...
|
|
95
|
+
content_length: 12843
|
|
96
|
+
content_mime: text/markdown
|
|
97
|
+
signed_at: 2026-04-24T18:30:00Z
|
|
98
|
+
────────────────────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
paste signature (base64): _
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
For automation, pass `--sig <base64>` to skip the prompt entirely.
|
|
104
|
+
|
|
105
|
+
## Spec
|
|
106
|
+
|
|
107
|
+
- [SPEC.md](https://github.com/orangecheck/oc-stamp-protocol/blob/main/SPEC.md)
|
|
108
|
+
- [Canonical message format (SPEC §3)](https://github.com/orangecheck/oc-stamp-protocol/blob/main/SPEC.md#3-canonical-message)
|
|
109
|
+
- [Verification algorithm (SPEC §8)](https://github.com/orangecheck/oc-stamp-protocol/blob/main/SPEC.md#8-verification-algorithm)
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
|
|
113
|
+
MIT.
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { createHash } from 'crypto';
|
|
3
|
+
import { mkdir, writeFile, stat, readFile } from 'fs/promises';
|
|
4
|
+
import { basename } from 'path';
|
|
5
|
+
import { canonicalMessage, stamp, verify } from '@orangecheck/stamp-core';
|
|
6
|
+
import { Command } from 'commander';
|
|
7
|
+
import { submitToCalendars, toStampOts } from '@orangecheck/stamp-ots';
|
|
8
|
+
import { execFile } from 'child_process';
|
|
9
|
+
import { promisify } from 'util';
|
|
10
|
+
import { createInterface } from 'readline/promises';
|
|
11
|
+
|
|
12
|
+
var __defProp = Object.defineProperty;
|
|
13
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
14
|
+
var __esm = (fn, res) => function __init() {
|
|
15
|
+
return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
|
|
16
|
+
};
|
|
17
|
+
var __export = (target, all) => {
|
|
18
|
+
for (var name in all)
|
|
19
|
+
__defProp(target, name, { get: all[name], enumerable: true });
|
|
20
|
+
};
|
|
21
|
+
async function readBytes(path) {
|
|
22
|
+
const buf = await readFile(path);
|
|
23
|
+
return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);
|
|
24
|
+
}
|
|
25
|
+
async function hashFile(path) {
|
|
26
|
+
const bytes = await readBytes(path);
|
|
27
|
+
const h = createHash("sha256").update(bytes).digest("hex");
|
|
28
|
+
return { hash: "sha256:" + h, length: bytes.byteLength };
|
|
29
|
+
}
|
|
30
|
+
function mimeFromPath(path) {
|
|
31
|
+
const name = basename(path).toLowerCase();
|
|
32
|
+
const ext = name.split(".").pop();
|
|
33
|
+
switch (ext) {
|
|
34
|
+
case "md":
|
|
35
|
+
case "markdown":
|
|
36
|
+
return "text/markdown";
|
|
37
|
+
case "txt":
|
|
38
|
+
return "text/plain";
|
|
39
|
+
case "html":
|
|
40
|
+
case "htm":
|
|
41
|
+
return "text/html";
|
|
42
|
+
case "json":
|
|
43
|
+
return "application/json";
|
|
44
|
+
case "pdf":
|
|
45
|
+
return "application/pdf";
|
|
46
|
+
case "png":
|
|
47
|
+
return "image/png";
|
|
48
|
+
case "jpg":
|
|
49
|
+
case "jpeg":
|
|
50
|
+
return "image/jpeg";
|
|
51
|
+
case "gif":
|
|
52
|
+
return "image/gif";
|
|
53
|
+
case "webp":
|
|
54
|
+
return "image/webp";
|
|
55
|
+
case "mp3":
|
|
56
|
+
return "audio/mpeg";
|
|
57
|
+
case "mp4":
|
|
58
|
+
return "video/mp4";
|
|
59
|
+
case "wav":
|
|
60
|
+
return "audio/wav";
|
|
61
|
+
case "zip":
|
|
62
|
+
return "application/zip";
|
|
63
|
+
case "tar":
|
|
64
|
+
return "application/x-tar";
|
|
65
|
+
case "gz":
|
|
66
|
+
return "application/gzip";
|
|
67
|
+
default:
|
|
68
|
+
return "application/octet-stream";
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
async function pathExists(path) {
|
|
72
|
+
try {
|
|
73
|
+
await stat(path);
|
|
74
|
+
return true;
|
|
75
|
+
} catch {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
async function readJson(pathOrDash) {
|
|
80
|
+
let text;
|
|
81
|
+
if (pathOrDash === "-" || pathOrDash === "/dev/stdin") {
|
|
82
|
+
text = await readStdin();
|
|
83
|
+
} else {
|
|
84
|
+
text = await readFile(pathOrDash, "utf8");
|
|
85
|
+
}
|
|
86
|
+
return JSON.parse(text);
|
|
87
|
+
}
|
|
88
|
+
function readStdin() {
|
|
89
|
+
return new Promise((resolve, reject) => {
|
|
90
|
+
let data = "";
|
|
91
|
+
process.stdin.setEncoding("utf8");
|
|
92
|
+
process.stdin.on("data", (chunk) => {
|
|
93
|
+
data += chunk;
|
|
94
|
+
});
|
|
95
|
+
process.stdin.on("end", () => resolve(data));
|
|
96
|
+
process.stdin.on("error", reject);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
function emit(json, result) {
|
|
100
|
+
if (json) {
|
|
101
|
+
console.log(JSON.stringify(result, null, 2));
|
|
102
|
+
} else {
|
|
103
|
+
for (const [k, v] of Object.entries(result)) {
|
|
104
|
+
console.log(`${k}: ${typeof v === "object" ? JSON.stringify(v) : v}`);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
function die(msg, code = 1) {
|
|
109
|
+
console.error(`error: ${msg}`);
|
|
110
|
+
process.exit(code);
|
|
111
|
+
}
|
|
112
|
+
var init_util = __esm({
|
|
113
|
+
"src/util.ts"() {
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
// src/commands/verify.ts
|
|
118
|
+
var verify_exports = {};
|
|
119
|
+
__export(verify_exports, {
|
|
120
|
+
runVerify: () => runVerify
|
|
121
|
+
});
|
|
122
|
+
async function runVerify(opts) {
|
|
123
|
+
if (!await pathExists(opts.stampPath)) die(`no such file: ${opts.stampPath}`);
|
|
124
|
+
const envelope = await readJson(opts.stampPath);
|
|
125
|
+
let contentBytes;
|
|
126
|
+
if (opts.contentPath) {
|
|
127
|
+
if (!await pathExists(opts.contentPath)) die(`no such file: ${opts.contentPath}`);
|
|
128
|
+
contentBytes = await readBytes(opts.contentPath);
|
|
129
|
+
}
|
|
130
|
+
let verifyBip322;
|
|
131
|
+
if (!opts.skipSignature) {
|
|
132
|
+
const mod = await import('bip322-js');
|
|
133
|
+
const Verifier = mod.Verifier ?? mod.default?.Verifier;
|
|
134
|
+
if (!Verifier) die("bip322-js Verifier export not found");
|
|
135
|
+
verifyBip322 = async (m, s, a) => {
|
|
136
|
+
try {
|
|
137
|
+
return Verifier.verifySignature(a, m, s);
|
|
138
|
+
} catch {
|
|
139
|
+
return false;
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
const r = await verify({
|
|
144
|
+
envelope,
|
|
145
|
+
content: contentBytes,
|
|
146
|
+
verifyBip322,
|
|
147
|
+
skipSignatureVerification: opts.skipSignature
|
|
148
|
+
});
|
|
149
|
+
if (!r.ok) {
|
|
150
|
+
emit(opts.json, {
|
|
151
|
+
ok: false,
|
|
152
|
+
code: r.code,
|
|
153
|
+
message: r.message
|
|
154
|
+
});
|
|
155
|
+
process.exit(2);
|
|
156
|
+
}
|
|
157
|
+
const anchorLabel = r.anchor.status === "none" ? "none" : r.anchor.status === "pending" ? "pending" : `confirmed at block ${r.anchor.blockHeight}`;
|
|
158
|
+
if (opts.requireAnchor && r.anchor.status !== "confirmed") {
|
|
159
|
+
emit(opts.json, {
|
|
160
|
+
ok: false,
|
|
161
|
+
code: "E_NO_ANCHOR",
|
|
162
|
+
message: `required confirmed OTS anchor, got ${r.anchor.status}`
|
|
163
|
+
});
|
|
164
|
+
process.exit(2);
|
|
165
|
+
}
|
|
166
|
+
emit(opts.json, {
|
|
167
|
+
ok: true,
|
|
168
|
+
id: r.id,
|
|
169
|
+
signer: envelope.signer.address,
|
|
170
|
+
signed_at: envelope.signed_at,
|
|
171
|
+
content_hash: envelope.content.hash,
|
|
172
|
+
content_mime: envelope.content.mime,
|
|
173
|
+
content_length: envelope.content.length,
|
|
174
|
+
content_checked: Boolean(contentBytes),
|
|
175
|
+
anchor: anchorLabel,
|
|
176
|
+
signature_checked: !opts.skipSignature,
|
|
177
|
+
stake: envelope.stake
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
var init_verify = __esm({
|
|
181
|
+
"src/commands/verify.ts"() {
|
|
182
|
+
init_util();
|
|
183
|
+
}
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
// src/commands/anchor.ts
|
|
187
|
+
init_util();
|
|
188
|
+
async function runAnchor(opts) {
|
|
189
|
+
if (!await pathExists(opts.stampPath)) die(`no such file: ${opts.stampPath}`);
|
|
190
|
+
const envelope = await readJson(opts.stampPath);
|
|
191
|
+
if (envelope.ots?.status === "confirmed") {
|
|
192
|
+
emit(opts.json, {
|
|
193
|
+
ok: true,
|
|
194
|
+
already: "confirmed",
|
|
195
|
+
block_height: envelope.ots.block_height
|
|
196
|
+
});
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
try {
|
|
200
|
+
const proof = await submitToCalendars(envelope.id);
|
|
201
|
+
const next = { ...envelope, ots: toStampOts(proof) };
|
|
202
|
+
await writeFile(opts.stampPath, JSON.stringify(next, null, 2) + "\n", "utf8");
|
|
203
|
+
emit(opts.json, {
|
|
204
|
+
ok: true,
|
|
205
|
+
id: next.id,
|
|
206
|
+
status: next.ots?.status,
|
|
207
|
+
calendars: next.ots?.calendars ?? [],
|
|
208
|
+
written: opts.stampPath
|
|
209
|
+
});
|
|
210
|
+
} catch (e) {
|
|
211
|
+
die(`OTS submission failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/commands/git-tag.ts
|
|
216
|
+
init_util();
|
|
217
|
+
var exec = promisify(execFile);
|
|
218
|
+
async function gitCapture(args) {
|
|
219
|
+
const { stdout } = await exec("git", args);
|
|
220
|
+
return stdout.trim();
|
|
221
|
+
}
|
|
222
|
+
async function runGitTag(opts) {
|
|
223
|
+
try {
|
|
224
|
+
await gitCapture(["rev-parse", "--is-inside-work-tree"]);
|
|
225
|
+
} catch {
|
|
226
|
+
die("not inside a git work tree");
|
|
227
|
+
}
|
|
228
|
+
let treeId;
|
|
229
|
+
let tagObjectId;
|
|
230
|
+
try {
|
|
231
|
+
treeId = await gitCapture(["rev-parse", `${opts.tag}^{tree}`]);
|
|
232
|
+
tagObjectId = await gitCapture(["rev-parse", opts.tag]);
|
|
233
|
+
} catch (e) {
|
|
234
|
+
die(`cannot resolve git tag ${opts.tag}: ${e instanceof Error ? e.message : String(e)}`);
|
|
235
|
+
}
|
|
236
|
+
const contentBytes = new TextEncoder().encode(
|
|
237
|
+
`oc-stamp/git-tag:v1
|
|
238
|
+
tag: ${opts.tag}
|
|
239
|
+
object: ${tagObjectId}
|
|
240
|
+
tree: ${treeId}
|
|
241
|
+
`
|
|
242
|
+
);
|
|
243
|
+
const contentHash = "sha256:" + createHash("sha256").update(contentBytes).digest("hex");
|
|
244
|
+
const contentLength = contentBytes.byteLength;
|
|
245
|
+
const mime = "application/x-git-tag";
|
|
246
|
+
const signedAt = (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
247
|
+
const msg = canonicalMessage({
|
|
248
|
+
address: opts.address,
|
|
249
|
+
content_hash: contentHash,
|
|
250
|
+
content_length: contentLength,
|
|
251
|
+
content_mime: mime,
|
|
252
|
+
signed_at: signedAt
|
|
253
|
+
});
|
|
254
|
+
let signature;
|
|
255
|
+
if (opts.sig) {
|
|
256
|
+
signature = opts.sig;
|
|
257
|
+
} else {
|
|
258
|
+
console.error("\nSign this message with your Bitcoin wallet (BIP-322):\n");
|
|
259
|
+
console.error("\u2500".repeat(60));
|
|
260
|
+
console.error(msg);
|
|
261
|
+
console.error("\u2500".repeat(60));
|
|
262
|
+
console.error();
|
|
263
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
264
|
+
try {
|
|
265
|
+
signature = (await rl.question("paste signature (base64): ")).trim();
|
|
266
|
+
} finally {
|
|
267
|
+
rl.close();
|
|
268
|
+
}
|
|
269
|
+
if (!signature) die("no signature provided");
|
|
270
|
+
}
|
|
271
|
+
const envelope = await stamp({
|
|
272
|
+
content: { hash: contentHash, length: contentLength },
|
|
273
|
+
mime,
|
|
274
|
+
signer: {
|
|
275
|
+
address: opts.address,
|
|
276
|
+
signMessage: async () => signature
|
|
277
|
+
},
|
|
278
|
+
signedAt: new Date(signedAt)
|
|
279
|
+
});
|
|
280
|
+
let final = envelope;
|
|
281
|
+
if (opts.anchor) {
|
|
282
|
+
try {
|
|
283
|
+
const proof = await submitToCalendars(envelope.id);
|
|
284
|
+
final = { ...envelope, ots: toStampOts(proof) };
|
|
285
|
+
} catch (e) {
|
|
286
|
+
console.error(
|
|
287
|
+
`warning: OTS submission failed (${e instanceof Error ? e.message : String(e)}). The stamp is still valid; you can retry anchoring later with \`stamp anchor\`.`
|
|
288
|
+
);
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
const gitDir = await gitCapture(["rev-parse", "--git-dir"]);
|
|
292
|
+
const stampsDir = `${gitDir}/stamps`;
|
|
293
|
+
await mkdir(stampsDir, { recursive: true });
|
|
294
|
+
const outPath = `${stampsDir}/${opts.tag}.stamp`;
|
|
295
|
+
await writeFile(outPath, JSON.stringify(final, null, 2) + "\n", "utf8");
|
|
296
|
+
emit(opts.json, {
|
|
297
|
+
ok: true,
|
|
298
|
+
tag: opts.tag,
|
|
299
|
+
tree: treeId,
|
|
300
|
+
object: tagObjectId,
|
|
301
|
+
id: final.id,
|
|
302
|
+
signer: final.signer.address,
|
|
303
|
+
ots: final.ots?.status ?? "not anchored",
|
|
304
|
+
written: outPath
|
|
305
|
+
});
|
|
306
|
+
}
|
|
307
|
+
async function runGitVerify(opts) {
|
|
308
|
+
const gitDir = await gitCapture(["rev-parse", "--git-dir"]);
|
|
309
|
+
const stampPath = `${gitDir}/stamps/${opts.tag}.stamp`;
|
|
310
|
+
const { runVerify: runVerify2 } = await Promise.resolve().then(() => (init_verify(), verify_exports));
|
|
311
|
+
await runVerify2({
|
|
312
|
+
stampPath,
|
|
313
|
+
requireAnchor: opts.requireAnchor,
|
|
314
|
+
skipSignature: false,
|
|
315
|
+
json: opts.json
|
|
316
|
+
});
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// src/commands/stamp-file.ts
|
|
320
|
+
init_util();
|
|
321
|
+
async function runStampFile(opts) {
|
|
322
|
+
if (!await pathExists(opts.path)) die(`no such file: ${opts.path}`);
|
|
323
|
+
const { hash, length } = await hashFile(opts.path);
|
|
324
|
+
const mime = opts.mime ?? mimeFromPath(opts.path);
|
|
325
|
+
const signedAt = opts.signedAt ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z");
|
|
326
|
+
const msg = canonicalMessage({
|
|
327
|
+
address: opts.address,
|
|
328
|
+
content_hash: hash,
|
|
329
|
+
content_length: length,
|
|
330
|
+
content_mime: mime,
|
|
331
|
+
signed_at: signedAt
|
|
332
|
+
});
|
|
333
|
+
let signature;
|
|
334
|
+
if (opts.sig) {
|
|
335
|
+
signature = opts.sig;
|
|
336
|
+
} else {
|
|
337
|
+
console.error("\nSign this message with your Bitcoin wallet (BIP-322):\n");
|
|
338
|
+
console.error("\u2500".repeat(60));
|
|
339
|
+
console.error(msg);
|
|
340
|
+
console.error("\u2500".repeat(60));
|
|
341
|
+
console.error();
|
|
342
|
+
const rl = createInterface({ input: process.stdin, output: process.stderr });
|
|
343
|
+
try {
|
|
344
|
+
signature = (await rl.question("paste signature (base64): ")).trim();
|
|
345
|
+
} finally {
|
|
346
|
+
rl.close();
|
|
347
|
+
}
|
|
348
|
+
if (!signature) die("no signature provided");
|
|
349
|
+
}
|
|
350
|
+
const envelope = await stamp({
|
|
351
|
+
content: { hash, length },
|
|
352
|
+
mime,
|
|
353
|
+
signer: {
|
|
354
|
+
address: opts.address,
|
|
355
|
+
signMessage: async () => signature
|
|
356
|
+
},
|
|
357
|
+
signedAt: new Date(signedAt),
|
|
358
|
+
stake: opts.stake
|
|
359
|
+
});
|
|
360
|
+
let final = envelope;
|
|
361
|
+
if (opts.anchor) {
|
|
362
|
+
try {
|
|
363
|
+
const proof = await submitToCalendars(envelope.id);
|
|
364
|
+
final = { ...envelope, ots: toStampOts(proof) };
|
|
365
|
+
} catch (e) {
|
|
366
|
+
console.error(
|
|
367
|
+
`warning: OTS submission failed (${e instanceof Error ? e.message : String(e)}). The stamp is still valid; you can retry anchoring later with \`stamp anchor\`.`
|
|
368
|
+
);
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
const outPath = opts.out ?? `${opts.path}.stamp`;
|
|
372
|
+
await writeFile(outPath, JSON.stringify(final, null, 2) + "\n", "utf8");
|
|
373
|
+
emit(opts.json, {
|
|
374
|
+
ok: true,
|
|
375
|
+
id: final.id,
|
|
376
|
+
signer: final.signer.address,
|
|
377
|
+
signed_at: final.signed_at,
|
|
378
|
+
content_hash: final.content.hash,
|
|
379
|
+
content_length: final.content.length,
|
|
380
|
+
content_mime: final.content.mime,
|
|
381
|
+
ots: final.ots ? `${final.ots.status} (${final.ots.calendars.length} calendar${final.ots.calendars.length === 1 ? "" : "s"})` : "not anchored",
|
|
382
|
+
written: outPath
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
// src/cli.ts
|
|
387
|
+
init_verify();
|
|
388
|
+
init_util();
|
|
389
|
+
var invoked = basename(process.argv[1] ?? "stamp");
|
|
390
|
+
var program = new Command();
|
|
391
|
+
if (invoked.startsWith("git-stamp")) {
|
|
392
|
+
program.name("git-stamp").description("Stamp git tags with your Bitcoin address, anchored to Bitcoin.").version("0.1.0");
|
|
393
|
+
program.command("tag <tagname>").description("Sign and (optionally) anchor a stamp over <tagname>^{tree}").requiredOption("--addr <address>", "Bitcoin address").option("--sig <signature>", "BIP-322 signature (non-interactive mode)").option("--no-anchor", "Skip OTS calendar submission").option("--json", "Emit JSON").action(async (tag, opts) => {
|
|
394
|
+
await runGitTag({
|
|
395
|
+
tag,
|
|
396
|
+
address: opts.addr,
|
|
397
|
+
sig: opts.sig,
|
|
398
|
+
anchor: Boolean(opts.anchor),
|
|
399
|
+
json: Boolean(opts.json)
|
|
400
|
+
});
|
|
401
|
+
});
|
|
402
|
+
program.command("verify <tagname>").description("Verify the stamp stored under .git/stamps/<tagname>.stamp").option("--require-anchor", "Fail unless the OTS proof is confirmed").option("--json", "Emit JSON").action(async (tag, opts) => {
|
|
403
|
+
await runGitVerify({
|
|
404
|
+
tag,
|
|
405
|
+
requireAnchor: Boolean(opts.requireAnchor),
|
|
406
|
+
json: Boolean(opts.json)
|
|
407
|
+
});
|
|
408
|
+
});
|
|
409
|
+
} else {
|
|
410
|
+
program.name("stamp").description("OC Stamp \u2014 sign anything with your Bitcoin address, anchor to Bitcoin.").version("0.1.0");
|
|
411
|
+
program.command("file <path>").description("Sign a file and write <path>.stamp alongside").requiredOption("--addr <address>", "Bitcoin address").option("--mime <type>", "Override the MIME type (default: inferred from extension)").option("--signed-at <iso>", "Override signed_at (default: now)").option("--sig <signature>", "BIP-322 signature (non-interactive mode)").option("--no-anchor", "Skip OTS calendar submission").option("--out <path>", "Output path (default: <path>.stamp)").option("--json", "Emit JSON").action(async (path, opts) => {
|
|
412
|
+
await runStampFile({
|
|
413
|
+
path,
|
|
414
|
+
address: opts.addr,
|
|
415
|
+
mime: opts.mime,
|
|
416
|
+
signedAt: opts.signedAt,
|
|
417
|
+
sig: opts.sig,
|
|
418
|
+
anchor: Boolean(opts.anchor),
|
|
419
|
+
out: opts.out,
|
|
420
|
+
json: Boolean(opts.json)
|
|
421
|
+
});
|
|
422
|
+
});
|
|
423
|
+
program.command("verify <stamp> [content]").description("Run the full SPEC \xA78 verification").option("--require-anchor", "Fail unless the OTS proof is confirmed").option("--skip-signature", "Skip BIP-322 verification (for CI smoke tests only)").option("--json", "Emit JSON").action(async (stampPath, contentPath, opts) => {
|
|
424
|
+
await runVerify({
|
|
425
|
+
stampPath,
|
|
426
|
+
contentPath,
|
|
427
|
+
requireAnchor: Boolean(opts.requireAnchor),
|
|
428
|
+
skipSignature: Boolean(opts.skipSignature),
|
|
429
|
+
json: Boolean(opts.json)
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
program.command("anchor <stamp>").description("Submit an existing stamp envelope to OTS calendars").option("--json", "Emit JSON").action(async (stampPath, opts) => {
|
|
433
|
+
await runAnchor({ stampPath, json: Boolean(opts.json) });
|
|
434
|
+
});
|
|
435
|
+
program.command("canonical <path>").description("Print the canonical message a stamp over <path> would sign (dry run)").requiredOption("--addr <address>", "Bitcoin address").option("--mime <type>", "Override the MIME type").option("--signed-at <iso>", "Override signed_at (default: now)").action(async (path, opts) => {
|
|
436
|
+
const { hash, length } = await hashFile(path);
|
|
437
|
+
const msg = canonicalMessage({
|
|
438
|
+
address: opts.addr,
|
|
439
|
+
content_hash: hash,
|
|
440
|
+
content_length: length,
|
|
441
|
+
content_mime: opts.mime ?? mimeFromPath(path),
|
|
442
|
+
signed_at: opts.signedAt ?? (/* @__PURE__ */ new Date()).toISOString().replace(/\.\d+Z$/, "Z")
|
|
443
|
+
});
|
|
444
|
+
process.stdout.write(msg);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
program.parseAsync(process.argv).catch((err) => {
|
|
448
|
+
console.error(`error: ${err instanceof Error ? err.message : String(err)}`);
|
|
449
|
+
process.exit(1);
|
|
450
|
+
});
|
|
451
|
+
//# sourceMappingURL=cli.js.map
|
|
452
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/util.ts","../src/commands/verify.ts","../src/commands/anchor.ts","../src/commands/git-tag.ts","../src/commands/stamp-file.ts","../src/cli.ts"],"names":["createHash","submitToCalendars","toStampOts","writeFile","runVerify","canonicalMessage","createInterface","stamp","basename"],"mappings":"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","file":"cli.js","sourcesContent":["// Shared helpers for stamp-cli commands.\n\nimport { createHash } from 'node:crypto';\nimport { readFile, stat } from 'node:fs/promises';\nimport { basename } from 'node:path';\n\n/** Read a path into bytes. */\nexport async function readBytes(path: string): Promise<Uint8Array> {\n const buf = await readFile(path);\n return new Uint8Array(buf.buffer, buf.byteOffset, buf.byteLength);\n}\n\n/**\n * Stream-hash a path with SHA-256. For very large content we want to avoid\n * loading the whole file into memory; for small files the cost is negligible.\n */\nexport async function hashFile(path: string): Promise<{ hash: string; length: number }> {\n const bytes = await readBytes(path);\n const h = createHash('sha256').update(bytes).digest('hex');\n return { hash: 'sha256:' + h, length: bytes.byteLength };\n}\n\n/** Infer a conservative MIME from a filename. Falls back to application/octet-stream. */\nexport function mimeFromPath(path: string): string {\n const name = basename(path).toLowerCase();\n const ext = name.split('.').pop();\n switch (ext) {\n case 'md':\n case 'markdown':\n return 'text/markdown';\n case 'txt':\n return 'text/plain';\n case 'html':\n case 'htm':\n return 'text/html';\n case 'json':\n return 'application/json';\n case 'pdf':\n return 'application/pdf';\n case 'png':\n return 'image/png';\n case 'jpg':\n case 'jpeg':\n return 'image/jpeg';\n case 'gif':\n return 'image/gif';\n case 'webp':\n return 'image/webp';\n case 'mp3':\n return 'audio/mpeg';\n case 'mp4':\n return 'video/mp4';\n case 'wav':\n return 'audio/wav';\n case 'zip':\n return 'application/zip';\n case 'tar':\n return 'application/x-tar';\n case 'gz':\n return 'application/gzip';\n default:\n return 'application/octet-stream';\n }\n}\n\nexport async function pathExists(path: string): Promise<boolean> {\n try {\n await stat(path);\n return true;\n } catch {\n return false;\n }\n}\n\n/**\n * Read JSON from a path or `-` for stdin.\n */\nexport async function readJson<T = unknown>(pathOrDash: string): Promise<T> {\n let text: string;\n if (pathOrDash === '-' || pathOrDash === '/dev/stdin') {\n text = await readStdin();\n } else {\n text = (await readFile(pathOrDash, 'utf8')) as string;\n }\n return JSON.parse(text) as T;\n}\n\nfunction readStdin(): Promise<string> {\n return new Promise((resolve, reject) => {\n let data = '';\n process.stdin.setEncoding('utf8');\n process.stdin.on('data', (chunk) => {\n data += chunk;\n });\n process.stdin.on('end', () => resolve(data));\n process.stdin.on('error', reject);\n });\n}\n\n/** Emit structured output. */\nexport function emit(json: boolean, result: Record<string, unknown>): void {\n if (json) {\n console.log(JSON.stringify(result, null, 2));\n } else {\n for (const [k, v] of Object.entries(result)) {\n console.log(`${k}: ${typeof v === 'object' ? JSON.stringify(v) : v}`);\n }\n }\n}\n\nexport function die(msg: string, code = 1): never {\n console.error(`error: ${msg}`);\n process.exit(code);\n}\n","// `stamp verify <stamp-path> [content-path]` — run the full SPEC §8\n// verification algorithm locally. Structured JSON output with `--json`.\n\nimport { verify, type StampEnvelope } from '@orangecheck/stamp-core';\n\nimport { die, emit, hashFile, pathExists, readBytes, readJson } from '../util.js';\n\nexport interface VerifyOptions {\n stampPath: string;\n contentPath?: string;\n requireAnchor: boolean;\n /**\n * If set, skip the BIP-322 signature verification. Mostly useful for CI\n * smoke tests with placeholder signatures; production verification should\n * never pass this.\n */\n skipSignature: boolean;\n json: boolean;\n}\n\nexport async function runVerify(opts: VerifyOptions): Promise<void> {\n if (!(await pathExists(opts.stampPath))) die(`no such file: ${opts.stampPath}`);\n\n const envelope = await readJson<StampEnvelope>(opts.stampPath);\n\n let contentBytes: Uint8Array | undefined;\n if (opts.contentPath) {\n if (!(await pathExists(opts.contentPath))) die(`no such file: ${opts.contentPath}`);\n contentBytes = await readBytes(opts.contentPath);\n }\n\n // Lazy-load bip322-js so it's only pulled in when needed (heavy dep).\n let verifyBip322: ((msg: string, sig: string, addr: string) => Promise<boolean>) | undefined;\n if (!opts.skipSignature) {\n const mod = (await import('bip322-js')) as unknown as {\n Verifier?: { verifySignature(a: string, m: string, s: string): boolean };\n default?: { Verifier?: { verifySignature(a: string, m: string, s: string): boolean } };\n };\n const Verifier = mod.Verifier ?? mod.default?.Verifier;\n if (!Verifier) die('bip322-js Verifier export not found');\n verifyBip322 = async (m, s, a) => {\n try {\n return Verifier.verifySignature(a, m, s);\n } catch {\n return false;\n }\n };\n }\n\n const r = await verify({\n envelope,\n content: contentBytes,\n verifyBip322,\n skipSignatureVerification: opts.skipSignature,\n });\n\n if (!r.ok) {\n emit(opts.json, {\n ok: false,\n code: r.code,\n message: r.message,\n });\n process.exit(2);\n }\n\n const anchorLabel =\n r.anchor.status === 'none'\n ? 'none'\n : r.anchor.status === 'pending'\n ? 'pending'\n : `confirmed at block ${r.anchor.blockHeight}`;\n\n if (opts.requireAnchor && r.anchor.status !== 'confirmed') {\n emit(opts.json, {\n ok: false,\n code: 'E_NO_ANCHOR',\n message: `required confirmed OTS anchor, got ${r.anchor.status}`,\n });\n process.exit(2);\n }\n\n emit(opts.json, {\n ok: true,\n id: r.id,\n signer: envelope.signer.address,\n signed_at: envelope.signed_at,\n content_hash: envelope.content.hash,\n content_mime: envelope.content.mime,\n content_length: envelope.content.length,\n content_checked: Boolean(contentBytes),\n anchor: anchorLabel,\n signature_checked: !opts.skipSignature,\n stake: envelope.stake,\n });\n void hashFile;\n}\n","// `stamp anchor <stamp-path>` — submit (or re-submit) an existing stamp's id\n// to OTS calendars. If the proof is already confirmed, no-op. If it's\n// pending, try to upgrade via the listed calendars.\n\nimport { writeFile } from 'node:fs/promises';\n\nimport { submitToCalendars, toStampOts } from '@orangecheck/stamp-ots';\nimport type { StampEnvelope } from '@orangecheck/stamp-core';\n\nimport { die, emit, pathExists, readJson } from '../util.js';\n\nexport interface AnchorOptions {\n stampPath: string;\n json: boolean;\n}\n\nexport async function runAnchor(opts: AnchorOptions): Promise<void> {\n if (!(await pathExists(opts.stampPath))) die(`no such file: ${opts.stampPath}`);\n const envelope = await readJson<StampEnvelope>(opts.stampPath);\n\n if (envelope.ots?.status === 'confirmed') {\n emit(opts.json, {\n ok: true,\n already: 'confirmed',\n block_height: envelope.ots.block_height,\n });\n return;\n }\n\n try {\n const proof = await submitToCalendars(envelope.id);\n const next: StampEnvelope = { ...envelope, ots: toStampOts(proof) };\n await writeFile(opts.stampPath, JSON.stringify(next, null, 2) + '\\n', 'utf8');\n emit(opts.json, {\n ok: true,\n id: next.id,\n status: next.ots?.status,\n calendars: next.ots?.calendars ?? [],\n written: opts.stampPath,\n });\n } catch (e) {\n die(`OTS submission failed: ${e instanceof Error ? e.message : String(e)}`);\n }\n}\n","// `git-stamp tag <tagname>` — stamp a git tag's tree. Meant to be invoked as\n// `git-stamp tag v1.2.3` (the same binary as `stamp`; we detect the alias\n// from argv[1] basename and also accept explicit `git tag` subcommand).\n//\n// The stamp covers the `git rev-parse <tag>^{tree}` output concatenated with\n// the tag name and object id, so changing any of (tree contents, tag object,\n// or tag name) invalidates the envelope.\n\nimport { execFile } from 'node:child_process';\nimport { writeFile, mkdir } from 'node:fs/promises';\nimport { promisify } from 'node:util';\n\nimport { createHash } from 'node:crypto';\nimport { canonicalMessage, stamp, type StampEnvelope } from '@orangecheck/stamp-core';\nimport { submitToCalendars, toStampOts } from '@orangecheck/stamp-ots';\n\nimport { die, emit } from '../util.js';\nimport { createInterface } from 'node:readline/promises';\n\nconst exec = promisify(execFile);\n\nexport interface GitTagOptions {\n tag: string;\n address: string;\n sig?: string;\n anchor: boolean;\n json: boolean;\n}\n\nasync function gitCapture(args: string[]): Promise<string> {\n const { stdout } = await exec('git', args);\n return stdout.trim();\n}\n\nexport async function runGitTag(opts: GitTagOptions): Promise<void> {\n // Validate that we're inside a git repo.\n try {\n await gitCapture(['rev-parse', '--is-inside-work-tree']);\n } catch {\n die('not inside a git work tree');\n }\n\n // Resolve the tag to its commit + tree, and the tag object itself.\n let treeId: string;\n let tagObjectId: string;\n try {\n treeId = await gitCapture(['rev-parse', `${opts.tag}^{tree}`]);\n tagObjectId = await gitCapture(['rev-parse', opts.tag]);\n } catch (e) {\n die(`cannot resolve git tag ${opts.tag}: ${e instanceof Error ? e.message : String(e)}`);\n }\n\n // Canonical content = \"oc-stamp/git-tag:v1\\n\" + tag + \"\\n\" + object_id + \"\\n\" + tree_id.\n // Hashed into content.hash; the whole thing is what the stamp commits to.\n const contentBytes = new TextEncoder().encode(\n `oc-stamp/git-tag:v1\\ntag: ${opts.tag}\\nobject: ${tagObjectId}\\ntree: ${treeId}\\n`\n );\n const contentHash = 'sha256:' + createHash('sha256').update(contentBytes).digest('hex');\n const contentLength = contentBytes.byteLength;\n const mime = 'application/x-git-tag';\n const signedAt = new Date().toISOString().replace(/\\.\\d+Z$/, 'Z');\n\n const msg = canonicalMessage({\n address: opts.address,\n content_hash: contentHash,\n content_length: contentLength,\n content_mime: mime,\n signed_at: signedAt,\n });\n\n let signature: string;\n if (opts.sig) {\n signature = opts.sig;\n } else {\n console.error('\\nSign this message with your Bitcoin wallet (BIP-322):\\n');\n console.error('─'.repeat(60));\n console.error(msg);\n console.error('─'.repeat(60));\n console.error();\n const rl = createInterface({ input: process.stdin, output: process.stderr });\n try {\n signature = (await rl.question('paste signature (base64): ')).trim();\n } finally {\n rl.close();\n }\n if (!signature) die('no signature provided');\n }\n\n const envelope: StampEnvelope = await stamp({\n content: { hash: contentHash, length: contentLength },\n mime,\n signer: {\n address: opts.address,\n signMessage: async () => signature,\n },\n signedAt: new Date(signedAt),\n });\n\n let final = envelope;\n if (opts.anchor) {\n try {\n const proof = await submitToCalendars(envelope.id);\n final = { ...envelope, ots: toStampOts(proof) };\n } catch (e) {\n console.error(\n `warning: OTS submission failed (${e instanceof Error ? e.message : String(e)}). ` +\n `The stamp is still valid; you can retry anchoring later with \\`stamp anchor\\`.`\n );\n }\n }\n\n // Store under .git/stamps/<tag>.stamp — keeps stamps co-located with the\n // repo history and discoverable by `git-stamp verify <tag>` without a\n // separate config.\n const gitDir = await gitCapture(['rev-parse', '--git-dir']);\n const stampsDir = `${gitDir}/stamps`;\n await mkdir(stampsDir, { recursive: true });\n const outPath = `${stampsDir}/${opts.tag}.stamp`;\n await writeFile(outPath, JSON.stringify(final, null, 2) + '\\n', 'utf8');\n\n emit(opts.json, {\n ok: true,\n tag: opts.tag,\n tree: treeId,\n object: tagObjectId,\n id: final.id,\n signer: final.signer.address,\n ots: final.ots?.status ?? 'not anchored',\n written: outPath,\n });\n}\n\nexport interface GitVerifyOptions {\n tag: string;\n requireAnchor: boolean;\n json: boolean;\n}\n\nexport async function runGitVerify(opts: GitVerifyOptions): Promise<void> {\n const gitDir = await gitCapture(['rev-parse', '--git-dir']);\n const stampPath = `${gitDir}/stamps/${opts.tag}.stamp`;\n const { runVerify } = await import('./verify.js');\n await runVerify({\n stampPath,\n requireAnchor: opts.requireAnchor,\n skipSignature: false,\n json: opts.json,\n });\n}\n","// `stamp file <path>` — sign a file with a BIP-322 signature from the user's\n// wallet, submit to OTS calendars, write <path>.stamp alongside.\n//\n// The signing step is done out-of-band: we print the canonical message and\n// wait for the user to paste the signature. Wallet CLIs vary too much to\n// automate reliably, so we keep the coupling loose and well-explained.\n\nimport { appendFile, writeFile } from 'node:fs/promises';\nimport { createInterface } from 'node:readline/promises';\n\nimport { canonicalMessage, stamp, type StampEnvelope, type StampStake } from '@orangecheck/stamp-core';\nimport { submitToCalendars, toStampOts } from '@orangecheck/stamp-ots';\n\nimport { die, emit, hashFile, mimeFromPath, pathExists } from '../util.js';\n\nexport interface StampFileOptions {\n path: string;\n address: string;\n mime?: string;\n signedAt?: string;\n stake?: StampStake;\n anchor: boolean;\n out?: string;\n /** If set, pass-through signature (no interactive prompt). */\n sig?: string;\n json: boolean;\n}\n\nexport async function runStampFile(opts: StampFileOptions): Promise<void> {\n if (!(await pathExists(opts.path))) die(`no such file: ${opts.path}`);\n\n const { hash, length } = await hashFile(opts.path);\n const mime = opts.mime ?? mimeFromPath(opts.path);\n const signedAt = opts.signedAt ?? new Date().toISOString().replace(/\\.\\d+Z$/, 'Z');\n\n const msg = canonicalMessage({\n address: opts.address,\n content_hash: hash,\n content_length: length,\n content_mime: mime,\n signed_at: signedAt,\n });\n\n let signature: string;\n if (opts.sig) {\n signature = opts.sig;\n } else {\n // Interactive: print the message, ask for the BIP-322 signature.\n console.error('\\nSign this message with your Bitcoin wallet (BIP-322):\\n');\n console.error('─'.repeat(60));\n console.error(msg);\n console.error('─'.repeat(60));\n console.error();\n const rl = createInterface({ input: process.stdin, output: process.stderr });\n try {\n signature = (await rl.question('paste signature (base64): ')).trim();\n } finally {\n rl.close();\n }\n if (!signature) die('no signature provided');\n }\n\n const envelope: StampEnvelope = await stamp({\n content: { hash, length },\n mime,\n signer: {\n address: opts.address,\n signMessage: async () => signature,\n },\n signedAt: new Date(signedAt),\n stake: opts.stake,\n });\n\n let final = envelope;\n if (opts.anchor) {\n try {\n const proof = await submitToCalendars(envelope.id);\n final = { ...envelope, ots: toStampOts(proof) };\n } catch (e) {\n console.error(\n `warning: OTS submission failed (${e instanceof Error ? e.message : String(e)}). ` +\n `The stamp is still valid; you can retry anchoring later with \\`stamp anchor\\`.`\n );\n }\n }\n\n const outPath = opts.out ?? `${opts.path}.stamp`;\n await writeFile(outPath, JSON.stringify(final, null, 2) + '\\n', 'utf8');\n\n emit(opts.json, {\n ok: true,\n id: final.id,\n signer: final.signer.address,\n signed_at: final.signed_at,\n content_hash: final.content.hash,\n content_length: final.content.length,\n content_mime: final.content.mime,\n ots: final.ots\n ? `${final.ots.status} (${final.ots.calendars.length} calendar${final.ots.calendars.length === 1 ? '' : 's'})`\n : 'not anchored',\n written: outPath,\n });\n void appendFile;\n}\n","/**\n * @orangecheck/stamp-cli — shell interface to OC Stamp.\n *\n * stamp file <path> — sign a file, optionally anchor\n * stamp verify <stamp> [content] — full SPEC §8 verification\n * stamp anchor <stamp> — submit/re-submit to OTS calendars\n * stamp canonical <path> — print the canonical message (dry run)\n *\n * Git aliases (same binary, invoked as `git-stamp`):\n *\n * git-stamp tag <tagname> — stamp a git tag\n * git-stamp verify <tagname> — verify the stored stamp for a tag\n *\n * The `stamp` and `git-stamp` binaries share this entrypoint — we dispatch\n * on argv[1] basename so each alias shows only its relevant subcommands in\n * --help.\n */\n\nimport { basename } from 'node:path';\n\nimport { Command } from 'commander';\n\nimport { runAnchor } from './commands/anchor.js';\nimport { runGitTag, runGitVerify } from './commands/git-tag.js';\nimport { runStampFile } from './commands/stamp-file.js';\nimport { runVerify } from './commands/verify.js';\n\nimport { canonicalMessage } from '@orangecheck/stamp-core';\nimport { hashFile, mimeFromPath } from './util.js';\n\nconst invoked = basename(process.argv[1] ?? 'stamp');\n\nconst program = new Command();\n\nif (invoked.startsWith('git-stamp')) {\n // git-stamp alias: narrow, tag-focused surface.\n program\n .name('git-stamp')\n .description('Stamp git tags with your Bitcoin address, anchored to Bitcoin.')\n .version('0.1.0');\n\n program\n .command('tag <tagname>')\n .description('Sign and (optionally) anchor a stamp over <tagname>^{tree}')\n .requiredOption('--addr <address>', 'Bitcoin address')\n .option('--sig <signature>', 'BIP-322 signature (non-interactive mode)')\n .option('--no-anchor', 'Skip OTS calendar submission')\n .option('--json', 'Emit JSON')\n .action(async (tag, opts) => {\n await runGitTag({\n tag,\n address: opts.addr,\n sig: opts.sig,\n anchor: Boolean(opts.anchor),\n json: Boolean(opts.json),\n });\n });\n\n program\n .command('verify <tagname>')\n .description('Verify the stamp stored under .git/stamps/<tagname>.stamp')\n .option('--require-anchor', 'Fail unless the OTS proof is confirmed')\n .option('--json', 'Emit JSON')\n .action(async (tag, opts) => {\n await runGitVerify({\n tag,\n requireAnchor: Boolean(opts.requireAnchor),\n json: Boolean(opts.json),\n });\n });\n} else {\n program\n .name('stamp')\n .description('OC Stamp — sign anything with your Bitcoin address, anchor to Bitcoin.')\n .version('0.1.0');\n\n program\n .command('file <path>')\n .description('Sign a file and write <path>.stamp alongside')\n .requiredOption('--addr <address>', 'Bitcoin address')\n .option('--mime <type>', 'Override the MIME type (default: inferred from extension)')\n .option('--signed-at <iso>', 'Override signed_at (default: now)')\n .option('--sig <signature>', 'BIP-322 signature (non-interactive mode)')\n .option('--no-anchor', 'Skip OTS calendar submission')\n .option('--out <path>', 'Output path (default: <path>.stamp)')\n .option('--json', 'Emit JSON')\n .action(async (path, opts) => {\n await runStampFile({\n path,\n address: opts.addr,\n mime: opts.mime,\n signedAt: opts.signedAt,\n sig: opts.sig,\n anchor: Boolean(opts.anchor),\n out: opts.out,\n json: Boolean(opts.json),\n });\n });\n\n program\n .command('verify <stamp> [content]')\n .description('Run the full SPEC §8 verification')\n .option('--require-anchor', 'Fail unless the OTS proof is confirmed')\n .option('--skip-signature', 'Skip BIP-322 verification (for CI smoke tests only)')\n .option('--json', 'Emit JSON')\n .action(async (stampPath, contentPath, opts) => {\n await runVerify({\n stampPath,\n contentPath,\n requireAnchor: Boolean(opts.requireAnchor),\n skipSignature: Boolean(opts.skipSignature),\n json: Boolean(opts.json),\n });\n });\n\n program\n .command('anchor <stamp>')\n .description('Submit an existing stamp envelope to OTS calendars')\n .option('--json', 'Emit JSON')\n .action(async (stampPath, opts) => {\n await runAnchor({ stampPath, json: Boolean(opts.json) });\n });\n\n program\n .command('canonical <path>')\n .description('Print the canonical message a stamp over <path> would sign (dry run)')\n .requiredOption('--addr <address>', 'Bitcoin address')\n .option('--mime <type>', 'Override the MIME type')\n .option('--signed-at <iso>', 'Override signed_at (default: now)')\n .action(async (path, opts) => {\n const { hash, length } = await hashFile(path);\n const msg = canonicalMessage({\n address: opts.addr,\n content_hash: hash,\n content_length: length,\n content_mime: opts.mime ?? mimeFromPath(path),\n signed_at: opts.signedAt ?? new Date().toISOString().replace(/\\.\\d+Z$/, 'Z'),\n });\n process.stdout.write(msg);\n // No trailing newline — the canonical message itself has no trailing\n // LF after signed_at, and users pipe this directly into signing tools.\n });\n}\n\nprogram.parseAsync(process.argv).catch((err) => {\n console.error(`error: ${err instanceof Error ? err.message : String(err)}`);\n process.exit(1);\n});\n"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@orangecheck/stamp-cli",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Shell interface to OC Stamp — sign files and git tags with your Bitcoin address, anchor to Bitcoin, verify offline. Exposes `stamp` and `git-stamp` binaries.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"bitcoin",
|
|
7
|
+
"bip322",
|
|
8
|
+
"opentimestamps",
|
|
9
|
+
"provenance",
|
|
10
|
+
"oc-stamp",
|
|
11
|
+
"git-stamp",
|
|
12
|
+
"cli",
|
|
13
|
+
"orangecheck"
|
|
14
|
+
],
|
|
15
|
+
"author": "OrangeCheck",
|
|
16
|
+
"license": "MIT",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/orangecheck/oc-packages.git",
|
|
20
|
+
"directory": "stamp-cli"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://stamp.ochk.io",
|
|
23
|
+
"bugs": {
|
|
24
|
+
"url": "https://github.com/orangecheck/oc-stamp-protocol/issues"
|
|
25
|
+
},
|
|
26
|
+
"type": "module",
|
|
27
|
+
"bin": {
|
|
28
|
+
"stamp": "./dist/cli.js",
|
|
29
|
+
"git-stamp": "./dist/cli.js"
|
|
30
|
+
},
|
|
31
|
+
"main": "./dist/cli.js",
|
|
32
|
+
"files": [
|
|
33
|
+
"dist",
|
|
34
|
+
"README.md",
|
|
35
|
+
"LICENSE"
|
|
36
|
+
],
|
|
37
|
+
"scripts": {
|
|
38
|
+
"build": "tsup",
|
|
39
|
+
"dev": "tsup --watch",
|
|
40
|
+
"type-check": "tsc --noEmit",
|
|
41
|
+
"test": "vitest run",
|
|
42
|
+
"test:watch": "vitest",
|
|
43
|
+
"clean": "rm -rf dist",
|
|
44
|
+
"prepublishOnly": "npm run clean && npm run build"
|
|
45
|
+
},
|
|
46
|
+
"dependencies": {
|
|
47
|
+
"@orangecheck/stamp-core": "^0.1.2",
|
|
48
|
+
"@orangecheck/stamp-ots": "^0.1.1",
|
|
49
|
+
"bip322-js": "^3.0.0",
|
|
50
|
+
"commander": "^12.1.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"@types/node": "^22.10.2",
|
|
54
|
+
"tsup": "^8.3.5",
|
|
55
|
+
"typescript": "^5.7.2",
|
|
56
|
+
"vitest": "^3.2.4"
|
|
57
|
+
}
|
|
58
|
+
}
|