Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Changelog

## Unreleased

- `post` takes `--as npub1...` and refuses to sign as anyone else. The signer
that answers is whichever bunker happens to be running, so announcing from
the wrong identity was silent and easy: publish one thing as a project key,
leave the bunker up, post a note, and the note goes out under the project
rather than under you. `publish` and `unpublish` have refused a mismatch
since 0.16.2 and there was never a reason for `post` not to

## 0.16.6 (2026-08-07)

- permit SGR colour in menu display text, and forbid every other control there.
Expand Down
22 changes: 19 additions & 3 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ const USAGE = `usage:
targets: npub[/path], nostr: entity, name@domain (NIP-05), gopher:// url

write (needs a signer, see below):
gopherkind post <text> [--dry-run] sign and broadcast a kind 1 note
gopherkind post <text> [--as npub1...] [--dry-run] sign and broadcast a note
gopherkind delete <id|note1|nevent1> [--wide] [--dry-run]
gopherkind publish <dir> [--as npub1...] [--expire 30d] [--dry-run] [--force]
only signs documents the relays do not already carry unchanged.
Expand Down Expand Up @@ -467,11 +467,27 @@ if (command === 'serve') {
const { values, positionals } = parseArgs({
args: rest,
allowPositionals: true,
options: { ...COMMON, 'dry-run': { type: 'boolean', default: false } },
options: {
...COMMON,
'dry-run': { type: 'boolean', default: false },
as: { type: 'string' },
},
})
const text = positionals.join(' ')
if (text.trim() === '') fail('usage: gopherkind post <text>')
run(cmdPost(text, relaysOf(values), pairingsOf(values), values['dry-run']))
// A note is as much an identity claim as a document is, and the signer that
// answers is whichever bunker happens to be running. publish has refused a
// mismatch since 0.16.2; there was no reason for post not to.
resolveSigner(pairingsOf(values))
.then((signer) => requireSignerIdentity(signer, values.as))
.then((signer) =>
cmdPost(text, relaysOf(values), pairingsOf(values), values['dry-run'], signer),
)
.then((out) => {
process.stdout.write(out)
process.exit(0)
})
.catch((err: unknown) => fail(err instanceof Error ? err.message : String(err)))
} else if (command === 'delete') {
const { values, positionals } = parseArgs({
args: rest,
Expand Down
15 changes: 15 additions & 0 deletions test/cli.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -152,3 +152,18 @@ test('public HTTP identity requires an explicit safe proxy contract', () => {
assert.notEqual(badOrigin.status, 0)
assert.match(badOrigin.out, /without a path/)
})

test('post accepts --as, so a note can name the identity it claims', () => {
// The signer that answers is whichever bunker happens to be running, and a
// note is as much an identity claim as a document is. Without --as,
// announcing from the wrong key is silent. publish has refused a mismatch
// since 0.16.2; post had no such guard until 0.16.7.
//
// Reaching the comparison needs a signer, and requireSignerIdentity is
// covered directly in the signing tests. What this asserts is that the flag
// is wired at all: before it was, this failed with ERR_PARSE_ARGS_UNKNOWN_OPTION.
const result = run(srcCli, ['post', 'hello', '--as', 'npub1whatever'])
assert.notEqual(result.status, 0)
assert.doesNotMatch(result.out, /UNKNOWN_OPTION/)
assert.match(result.out, /no signer/)
})