Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
0801383
ci: add cross-repo workspace test caller
Peeja Jun 16, 2026
35ce799
feat: `ValidateToken` takes options, too
Peeja May 27, 2026
a95e20f
feat: DID Document parser
Peeja May 27, 2026
b35609f
feat: Easier to add verification methods to a document
Peeja May 27, 2026
7458d1d
refactor: Add Unmarshal tests for `VerificationMethod`
Peeja May 28, 2026
da3b8a0
feat: Can add a verification method to a relationship
Peeja May 28, 2026
c1e6d80
refactor: Merge `did/document` into `did` pkg
Peeja May 28, 2026
82fe273
fix: `publicKeyMultibase` is technically optional
Peeja May 29, 2026
4eeb443
refactor: VerificationMaterial alone determines type
Peeja May 29, 2026
e1ba089
refactor: `RegisterVerificationMethodType()` finds type name from fac…
Peeja May 29, 2026
b79af53
refactor: Validator resolves DIDs and verifiers separately
Peeja May 29, 2026
61ce354
WIP: Rework principal -> verification
Peeja Jun 2, 2026
a9e5fd6
refactor: DID VM types don't need Go types
Peeja Jun 2, 2026
00cee24
feat: Validation error explains reasons for each method
Peeja Jun 2, 2026
9bb76c9
refactor: Varsig doesn't use type parameters
Peeja May 26, 2026
b0fc9ae
refactor: Clarify and simplify `varsig`
Peeja Jun 2, 2026
4ca1af1
feat: Validator works with any sig algorithm
Peeja Jun 2, 2026
d94a2d3
refactor: ErrorModel supports causes
Peeja Jun 3, 2026
d374882
feat: did:web, utility resolvers, and more
Peeja Jun 7, 2026
ffc616b
doc: Update README
Peeja Jun 7, 2026
0e90a0c
fix: `WithSigner` -> `WithIssuer`
Peeja Jun 7, 2026
4acd74d
refactor: `web.WithInsecure()` takes a boolean
Peeja Jun 16, 2026
5579739
refactor: Restore cut-off doc comments
Peeja Jun 16, 2026
afc872b
refactor: `utilresolvers` → `resolver`
Peeja Jun 16, 2026
f1ce65b
refactor: Single error for unsupported methods
Peeja Jun 16, 2026
a6a94ed
fix: Update `go.sum`
Peeja Jun 16, 2026
0fc2ef3
refactor: Change `resolver.Chain` back to `resolver.Tiered`
Peeja Jun 17, 2026
1cb231f
refactor: `WithSigner()` should be `WithIssuer()`
Peeja Jun 17, 2026
026c806
refactor: Break apart `ucantone/verification`
Peeja Jun 17, 2026
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
16 changes: 16 additions & 0 deletions .github/workflows/workspace-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
name: Workspace Test

on:
pull_request:
workflow_dispatch:

permissions:
contents: read

concurrency:
group: workspace-test-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true

jobs:
workspace-test:
uses: fil-forge/libforge/.github/workflows/go-workspace-test.yml@v1
25 changes: 13 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,19 +24,19 @@ fmt.Println("DID:", principal)
```

```go
// generate a new ed25519 signer
signerPrincipal, err := ed25519.Generate()
fmt.Println("DID:", signerPrincipal.DID())
// generate a new ed25519 issuer
issuer, err := ed25519.GenerateIssuer()
fmt.Println("DID:", issuer.DID())

// this principal can sign
sig := signerPrincipal.Sign([]byte{1, 2, 3})
sig := issuer.Sign([]byte{1, 2, 3})
fmt.Printf("Signature: 0x%x\n", sig)

// and has a private key (use format utility to multibase base64pad encode)
fmt.Println("Private Key:", signer.Format(signerPrincipal))
fmt.Println("Private Key:", multikey.FormatSigner(issuer))

// which can be stored and decoded later...
signerPrincipal2, err := ed25519.Decode(signerPrincipal.Bytes())
issuer2, err := ed25519.Decode(issuer.Bytes())
```

#### Delegation
Expand Down Expand Up @@ -134,7 +134,7 @@ See examples in [server_test.go](./examples/server_test.go)

```go
echo, err := command.Parse("/example/echo")
serviceID, err := ed25519.Generate()
serviceID, err := ed25519.GenerateIssuer()

ucanSrv := server.NewHTTP(serviceID)

Expand Down Expand Up @@ -167,14 +167,15 @@ c, err := client.NewHTTP(serviceURL)

// create an execution request and send it to the service, passing the
// invocation and the delegation as proof we are authorized
req := execution.NewRequest(context.Background(), inv, execution.WithProofs(dlg))
req := execution.NewRequest(context.Background(), inv, execution.WithDelegations(dlg))
resp, err := c.Execute(req)

o, x := result.Unwrap(resp.Receipt().Out())
if x != nil {
fmt.Printf("Invocation failed: %v\n", x)
if out := resp.Receipt().Out(); out.IsOK() {
ok, _ := out.Unpack()
fmt.Printf("Echo response: %+v\n", ok)
} else {
fmt.Printf("Echo response: %+v\n", o)
_, x := out.Unpack()
fmt.Printf("Invocation failed: %v\n", x)
}
```

Expand Down
46 changes: 46 additions & 0 deletions absentee/signer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package absentee

import (
"fmt"

"github.com/fil-forge/ucantone/did"
"github.com/fil-forge/ucantone/ucan"
"github.com/fil-forge/ucantone/varsig"
"github.com/fil-forge/ucantone/varsig/algorithm/nonstandard"
)

var SignatureAlgorithm = nonstandard.New()

// Issuer is a special type of issuer that produces an absent signature,
// which signals that verifier needs to verify authorization interactively.
type Issuer struct {
id did.DID
}

var _ ucan.Issuer = Issuer{}

func (a Issuer) DID() did.DID {
return a.id
}

func (a Issuer) String() string {
return fmt.Sprintf("%s (absentee)", a.id)
}

func (a Issuer) Sign(msg []byte) []byte {
return []byte{}
}

func (a Issuer) SignatureAlgorithm() varsig.Algorithm {
return SignatureAlgorithm
}

func (a Issuer) Verifier() ucan.Verifier {
panic("absentee issuer does not have a verifier")
}

// From creates a special type of issuer that produces an absent signature,
// which signals that verifier needs to verify authorization interactively.
func From(id did.DID) Issuer {
return Issuer{id}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package absentee_test
import (
"testing"

"github.com/fil-forge/ucantone/absentee"
"github.com/fil-forge/ucantone/did"
"github.com/fil-forge/ucantone/principal/absentee"
"github.com/stretchr/testify/require"
)

Expand Down
4 changes: 2 additions & 2 deletions binding/binding.go
Original file line number Diff line number Diff line change
Expand Up @@ -81,13 +81,13 @@ func Bind[Args, OK CBORValue](cmd command.Command) Binding[Args, OK] {

// Invoke constructs a signed invocation of the command carrying the given
// typed arguments.
func (c Binding[Args, OK]) Invoke(issuer ucan.Signer, subject did.DID, arguments Args, options ...invocation.Option) (ucan.Invocation, error) {
func (c Binding[Args, OK]) Invoke(issuer ucan.Issuer, subject did.DID, arguments Args, options ...invocation.Option) (ucan.Invocation, error) {
return invocation.Invoke(issuer, subject, c.Command, arguments, options...)
}

// Delegate issues a delegation granting authority over this command. It does
// not involve the argument or result types.
func (c Binding[Args, OK]) Delegate(issuer ucan.Signer, audience did.DID, subject did.DID, options ...delegation.Option) (ucan.Delegation, error) {
func (c Binding[Args, OK]) Delegate(issuer ucan.Issuer, audience did.DID, subject did.DID, options ...delegation.Option) (ucan.Delegation, error) {
return delegation.Delegate(issuer, audience, subject, c.Command, options...)
}

Expand Down
8 changes: 4 additions & 4 deletions binding/example_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import (

"github.com/fil-forge/ucantone/binding"
"github.com/fil-forge/ucantone/execution"
"github.com/fil-forge/ucantone/principal/ed25519"
"github.com/fil-forge/ucantone/multikey/ed25519"
tdm "github.com/fil-forge/ucantone/testutil/datamodel"
"github.com/fil-forge/ucantone/ucan/command"
)
Expand All @@ -22,8 +22,8 @@ var echo = binding.Bind[*tdm.TestObject, *tdm.TestObject2](command.MustParse("/e
// typed arguments, a server handles it and returns a typed result, and the
// client unpacks that result — all without hand-written encoding or decoding.
func Example() {
client, _ := ed25519.Generate()
service, _ := ed25519.Generate()
client, _ := ed25519.GenerateIssuer()
service, _ := ed25519.GenerateIssuer()
Comment thread
alanshaw marked this conversation as resolved.

// Client: invoke the command with typed arguments. Invoke encodes them into
// a signed invocation addressed to the service.
Expand All @@ -37,7 +37,7 @@ func Example() {
})

xreq := execution.NewRequest(context.Background(), inv)
xres, _ := execution.NewResponse(inv.Task().Link(), execution.WithSigner(service))
xres, _ := execution.NewResponse(inv.Task().Link(), execution.WithIssuer(service))
_ = handle(xreq, xres)

// Client: unpack the typed result (OK) out of the receipt.
Expand Down
24 changes: 12 additions & 12 deletions binding/handler.go
Original file line number Diff line number Diff line change
Expand Up @@ -82,25 +82,25 @@ func (r *Request[Args]) Task() *Task[Args] {
return r.task
}

// SignerSetter is implemented by responses that can issue their own receipt and
// therefore need a signer. [WithSigner] uses it to set that signer.
type SignerSetter interface {
SetSigner(ucan.Signer) error
// IssuerSetter is implemented by responses that can issue their own receipt and
// therefore need an issuer. [WithIssuer] uses it to set that issuer.
type IssuerSetter interface {
SetIssuer(ucan.Issuer) error
}

// ResponseOption configures a [Response] as it is built by [NewResponse],
// typically setting its outcome (success or failure), signer, or metadata.
// typically setting its outcome (success or failure), issuer, or metadata.
type ResponseOption[OK cbg.CBORMarshaler] func(r *Response[OK]) error

// WithSigner sets the signer used to issue the response's receipt. It fails if
// the underlying response cannot accept one (does not implement [SignerSetter]).
func WithSigner[OK cbg.CBORMarshaler](signer ucan.Signer) ResponseOption[OK] {
// WithIssuer sets the issuer used to issue the response's receipt. It fails if
// the underlying response cannot accept one (does not implement [IssuerSetter]).
func WithIssuer[OK cbg.CBORMarshaler](issuer ucan.Issuer) ResponseOption[OK] {
return func(resp *Response[OK]) error {
setter, ok := resp.res.(SignerSetter)
setter, ok := resp.res.(IssuerSetter)
if !ok {
return fmt.Errorf("cannot set signer: underlying response is not a signer setter")
return fmt.Errorf("cannot set issuer: underlying response is not an issuer setter")
}
return setter.SetSigner(signer)
return setter.SetIssuer(issuer)
}
}

Expand Down Expand Up @@ -144,7 +144,7 @@ type Response[OK cbg.CBORMarshaler] struct {
}

// NewResponse creates a response for the given task, applying any options
// (such as [WithSuccess], [WithFailure], or [WithSigner]) that set its outcome.
// (such as [WithSuccess], [WithFailure], or [WithIssuer]) that set its outcome.
func NewResponse[OK cbg.CBORMarshaler](task cid.Cid, options ...ResponseOption[OK]) (*Response[OK], error) {
xres, err := execution.NewResponse(task)
if err != nil {
Expand Down
4 changes: 2 additions & 2 deletions binding/handler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import (
)

func TestHandler(t *testing.T) {
alice := testutil.RandomSigner(t)
alice := testutil.RandomIssuer(t)
handler := binding.NewHandler(func(req *binding.Request[*tdm.TestObject], res *binding.Response[*tdm.TestObject2]) error {
args := req.Task().Arguments()
require.Equal(t, args.Bytes, []byte{0x01, 0x02, 0x03})
Expand All @@ -33,7 +33,7 @@ func TestHandler(t *testing.T) {
req := execution.NewRequest(t.Context(), inv)
require.NoError(t, err)

res, err := execution.NewResponse(inv.Task().Link(), execution.WithSigner(alice))
res, err := execution.NewResponse(inv.Task().Link(), execution.WithIssuer(alice))
require.NoError(t, err)

err = handler(req, res)
Expand Down
4 changes: 2 additions & 2 deletions client/http_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ import (
)

func TestHTTPClient(t *testing.T) {
service := testutil.RandomSigner(t)
alice := testutil.RandomSigner(t)
service := testutil.RandomIssuer(t)
alice := testutil.RandomIssuer(t)

t.Run("invocation execution round trip", func(t *testing.T) {
server := server.NewHTTP(service)
Expand Down
29 changes: 29 additions & 0 deletions did/context.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
package did

import (
"encoding/json"
"fmt"
)

const CoreContext = "https://www.w3.org/ns/did/v1.1"

// Context handles both string and []string formats for @context field
// as allowed by the DID Core specification
type Context []string

func (fc Context) MarshalJSON() ([]byte, error) {
return json.Marshal(OneOrMany[string](append([]string{CoreContext}, fc...)))
}

func (fc *Context) UnmarshalJSON(data []byte) error {
var ctxStrs OneOrMany[string]
err := json.Unmarshal(data, &ctxStrs)
if err != nil {
return err
}
if len(ctxStrs) < 1 || ctxStrs[0] != CoreContext {
return fmt.Errorf("@context must list %q first", CoreContext)
}
*fc = Context(ctxStrs[1:])
return nil
}
68 changes: 68 additions & 0 deletions did/context_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package did_test

import (
"encoding/json"
"testing"

"github.com/fil-forge/ucantone/did"
"github.com/stretchr/testify/require"
)

func TestContext(t *testing.T) {
t.Run("to JSON", func(t *testing.T) {
t.Run("with no additional contexts, produces the DID Core context", func(t *testing.T) {
ctx := did.Context{}
b, err := json.Marshal(ctx)
require.NoError(t, err)
require.JSONEq(t, `"https://www.w3.org/ns/did/v1.1"`, string(b))
})

t.Run("with additional contexts, puts the DID Core context first", func(t *testing.T) {
ctx := did.Context{
"https://example.com/context1",
"https://example.com/context2",
}
b, err := json.Marshal(ctx)
require.NoError(t, err)
require.JSONEq(t, `[
"https://www.w3.org/ns/did/v1.1",
"https://example.com/context1",
"https://example.com/context2"
]`, string(b))
})
})

t.Run("from JSON", func(t *testing.T) {
t.Run("with a single string of the DID Core context, produces an empty Context", func(t *testing.T) {
var ctx did.Context
err := json.Unmarshal([]byte(`"https://www.w3.org/ns/did/v1.1"`), &ctx)
require.NoError(t, err)
require.Equal(t, did.Context{}, ctx)
})

t.Run("with a single string of the wrong context, fails", func(t *testing.T) {
var ctx did.Context
err := json.Unmarshal([]byte(`"https://example.com/wrong-context"`), &ctx)
require.ErrorContains(t, err, `@context must list "https://www.w3.org/ns/did/v1.1" first`)
})

t.Run("with multiple contexts, puts them in the Context", func(t *testing.T) {
var ctx did.Context
err := json.Unmarshal([]byte(`["https://www.w3.org/ns/did/v1.1", "https://example.com/context1"]`), &ctx)
require.NoError(t, err)
require.Equal(t, did.Context{"https://example.com/context1"}, ctx)
})

t.Run("with multiple contexts missing the DID Core context, fails", func(t *testing.T) {
var ctx did.Context
err := json.Unmarshal([]byte(`["https://example.com/context1", "https://example.com/context2"]`), &ctx)
require.ErrorContains(t, err, `@context must list "https://www.w3.org/ns/did/v1.1" first`)
})

t.Run("with no contexts, fails", func(t *testing.T) {
var ctx did.Context
err := json.Unmarshal([]byte(`[]`), &ctx)
require.ErrorContains(t, err, `@context must list "https://www.w3.org/ns/did/v1.1" first`)
})
})
}
32 changes: 32 additions & 0 deletions did/datetimestamp.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package did

import (
"encoding/json"
"time"
)

type DateTimeStamp time.Time

func (d DateTimeStamp) Time() time.Time {
return time.Time(d)
}

func (d DateTimeStamp) MarshalJSON() ([]byte, error) {
return json.Marshal(d.Time().Format(time.RFC3339))
}

func (d *DateTimeStamp) UnmarshalJSON(b []byte) error {
var s string
err := json.Unmarshal(b, &s)
if err != nil {
return err
}

t, err := time.Parse(time.RFC3339, s)
if err != nil {
return err
}

*d = DateTimeStamp(t)
return nil
}
Loading
Loading