Skip to content

Make the delegation logic importable - #13

Merged
bajtos merged 5 commits into
mainfrom
make-it-importable
Aug 11, 2026
Merged

Make the delegation logic importable#13
bajtos merged 5 commits into
mainfrom
make-it-importable

Conversation

@bajtos

@bajtos bajtos commented Aug 11, 2026

Copy link
Copy Markdown
Member

Generating a delegation requires the ucantool binary today, and the CLI takes the issuer key as a file path. A caller that holds a key in memory — a Lambda, for instance — would have to spawn a subprocess and write the private key to disk purely to satisfy an API shape.

Every cryptographic step already comes from ucantone's exported API. The only obstacle was that the orchestration lived in mkDelegation in package cmd, reading cobra flag globals. This is a move, not a rewrite.

Example Use

import "github.com/fil-forge/ucantool/pkg/ucandelegate"

res, err := ucandelegate.IssueFromPEM(pemData, ucandelegate.Request{
	Audience:       "did:key:aud",
	Commands:       []string{"/msg/send"},
	Expiration:     ucandelegate.ExpiresIn(time.Hour),
	ContainerCodec: "base64+gzip",
})
_, err = res.WriteTo(os.Stdout)

Request replaces the flag globals, and takes a Signer or PEM bytes rather than a path. Delegate and Encode are available separately for callers that want the delegations before they are encoded.

Result.WriteTo carries the rule that textual container codecs end with a newline while raw and raw+gzip are written bare, so a consumer reproduces the CLI's bytes without reimplementing the rule.

ExpiresAt and ExpiresIn exist so that setting an expiration does not require importing ucantone.

A private key leak, fixed

EncodeSignerToPEM interpolated the signer with %s. ed25519.Signer is a []byte with no String method, so the raw private key bytes rendered into the error text and from there into anything that logged it. It now formats the key DID.

(Related: fil-forge/libforge#61)

bajtos added 4 commits August 11, 2026 19:48
The delegate and container pack commands each hand-rolled the mapping
from a codec name to a container codec byte, and each declared the
"base64+gzip" default separately. Add ParseCodec next to the FormatCodec
call it inverts, and express the binary-versus-text distinction as
IsTextualCodec so every caller shares one rule for deciding whether
output ends with a newline.

Convert the pack command; the delegate command follows once its
delegation logic moves out of the cobra layer.

Assisted-by: Claude:claude-opus-5

Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
EncodeSignerToPEM interpolated the signer itself with %s. ed25519.Signer
is a []byte with no String method, so the raw private key bytes rendered
into the error text and from there into anything that logged it. Format
the key DID instead.

Add LoadSignerFromPEMFile alongside DecodeSignerFromPEM. The delegate
command carries its own copy today and switches to this one once its
delegation logic moves out of the cobra layer; callers holding a key in
memory keep using DecodeSignerFromPEM and never touch the filesystem.

Assisted-by: Claude:claude-opus-5

Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Issuing a delegation required the ucantool binary, and the CLI takes the
issuer key as a file path, so an importing module had to write private
keys to disk purely to satisfy an API shape. Lift the orchestration out
of the delegate command's RunE into a package that takes a signer or PEM
bytes in memory.

Request replaces the cobra flag globals. Two behaviours are worth naming:
a nil Expiration passes WithNoExpiration explicitly, because omitting the
option entirely makes ucantone expire the delegation 30 seconds from now;
and Result.WriteTo carries the rule that textual container codecs end
with a newline while binary output is written bare, so consumers match
the CLI byte for byte without reimplementing it.

Assisted-by: Claude:claude-opus-5

Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
The delegate command is now a flag binder over ucandelegate.Issue, which
is the check that the extraction is faithful: nothing but reading flags
and writing bytes is left behind.

Add a test for the trailing-newline rule, since the delegation payload
itself cannot be compared against a golden file. ucantone generates a
random nonce for every delegation, so the encoded bytes differ on every
run.

Assisted-by: Claude:claude-opus-5

Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
@bajtos
bajtos requested review from frrist and a lite review from Copilot August 11, 2026 17:49

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extracts the delegation-issuing orchestration out of the CLI into an importable Go package (pkg/ucandelegate), enabling callers to issue and encode delegations directly from in-memory keys (no subprocess, no writing private keys to disk).

Changes:

  • Introduces pkg/ucandelegate with Request/Result and helpers (Issue, Delegate, Encode, IssueFromPEM, ExpiresAt/ExpiresIn) to reproduce CLI delegation behavior as a library.
  • Adds pkg/ucanfmt helpers for container codec defaults/parsing and textual-vs-binary detection, and reuses them from commands.
  • Moves PEM-file signer loading into pkg/identity and refactors cmd/delegate to bind flags and call the library; adds tests for newline behavior and expiration semantics.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
README.md Documents the new library usage pattern for issuing delegations without the CLI.
pkg/ucanfmt/codec.go Adds codec parsing/defaulting and textual codec detection shared by CLI and library.
pkg/ucanfmt/codec_test.go Tests codec parsing round-trips and textual codec classification.
pkg/ucandelegate/delegate.go New library API for issuing/encoding delegations (extracted from CLI orchestration).
pkg/ucandelegate/delegate_test.go Validates delegation semantics (expiration, policy, issuer/subject) and encoding/output rules.
pkg/identity/pem.go Fixes signer formatting in errors and adds LoadSignerFromPEMFile.
pkg/identity/pem_test.go Tests PEM file loading and missing-file error behavior.
cmd/delegate.go Refactors CLI to build a ucandelegate.Request from flags and call the library.
cmd/delegate_test.go Adds CLI regression tests for newline rules and codec/missing-file errors.
cmd/container/pack.go Reuses ucanfmt codec parsing/defaults and textual detection instead of local logic.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread pkg/ucandelegate/delegate.go
Result.WriteTo copied the whole output into a string and ran it
through fmt just to append a newline. Write the bytes directly and
follow with a single '\n' for textual codecs. A failed or short first
write now reports its partial count instead of fmt's.

Signed-off-by: Miroslav Bajtoš <oss@bajtos.net>
Assisted-by: Claude:claude-opus-5[1m]
@bajtos
bajtos merged commit ec0494d into main Aug 11, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants