Just to give some context on some of the components around the code structure.. I wrote this up around an earlier version of aggregate code. This generic bit simplifies things by removing the need of the Crud functions for each aggregate.
## Domain Objects
A domain object can be used as an aggregate by adding the
event.AggregateRoot
struct and finish implementing event.Aggregate. The AggregateRoot implements logic for adding events after they are either Raised by a command or Appended by the eventstore Load or service ApplyFn methods. It also tracks the uncommitted events that are saved using the eventstore Save method.
type User struct {
Identity string
json:"identity"
CreatedAt time.Time
event.AggregateRoot
}
// StreamID for the aggregate when stored or loaded from ES.
func (a *User) StreamID() string {
return "user-" + a.Identity
}
// ApplyEvent to the aggregate state.
func (a *User) ApplyEvent(lis ...event.Event) {
for _, e := range lis {
switch e := e.(type) {
case *UserCreated:
a.Identity = e.Identity
a.CreatedAt = e.EventMeta().CreatedDate
/* ... */
}
}
}
## Events
Events are applied to the aggregate. They are defined by adding the
event.Meta
and implementing the getter/setters for event.Event
type UserCreated struct {
eventMeta event.Meta
Identity string
}
func (c *UserCreated) EventMeta() (m event.Meta) {
if c != nil {
m = c.eventMeta
}
return m
}
func (c *UserCreated) SetEventMeta(m event.Meta) {
if c != nil {
c.eventMeta = m
}
}
## Reading Events from EventStore
With a domain object that implements the
event.Aggregate
the event store client can load events and apply them using the Load(ctx, agg)
method.
// GetUser populates an user from event store.
func (rw *User) GetUser(ctx context.Context, userID string) (*domain.User, error) {
user := &domain.User{Identity: userID}
err := rw.es.Load(ctx, user)
if err != nil {
if err != nil {
if errors.Is(err, eventstore.ErrStreamNotFound) {
return user, ErrNotFound
}
return user, err
}
return nil, err
}
return user, err
}
## OnX Commands
An OnX command will validate the state of the domain object can have the command performed on it. If it can be applied it raises the event using event.Raise() Otherwise it returns an error.
// OnCreate raises an UserCreated event to create the user.
// Note: The handler will check that the user does not already exsist.
func (a *User) OnCreate(identity string) error {
event.Raise(a, &UserCreated{Identity: identity})
return nil
}
// OnScored will attempt to score a task.
// If the task is not in a Created state it will fail.
func (a *Task) OnScored(taskID string, score int64, attributes Attributes) error {
if a.State != TaskStateCreated {
return fmt.Errorf("task expected created, got %s", a.State)
}
event.Raise(a, &TaskScored{TaskID: taskID, Attributes: attributes, Score: score})
return nil
}
## Crud Operations for OnX Commands
The following functions in the aggregate service can be used to perform creation and updating of aggregates. The Update function will ensure the aggregate exists, where the Create is intended for non-existent aggregates. These can probably be combined into one function.
// Create is used when the stream does not yet exist.
func (rw *User) Create(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
session, err := rw.GetUser(ctx, identity)
if err != nil && !errors.Is(err, ErrNotFound) {
return nil, err
}
if err = fn(session); err != nil {
return nil, err
}
_, err = rw.es.Save(ctx, session)
return session, err
}
// Update is used when the stream already exists.
func (rw *User) Update(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
session, err := rw.GetUser(ctx, identity)
if err != nil {
return nil, err
}
if err = fn(session); err != nil {
return nil, err
}
_, err = rw.es.Save(ctx, session)
return session, err
}
Just to give some context on some of the components around the code structure.. I wrote this up around an earlier version of aggregate code. This generic bit simplifies things by removing the need of the Crud functions for each aggregate.
## Domain Objects
A domain object can be used as an aggregate by adding the
event.AggregateRoot
struct and finish implementing event.Aggregate. The AggregateRoot implements logic for adding events after they are either Raised by a command or Appended by the eventstore Load or service ApplyFn methods. It also tracks the uncommitted events that are saved using the eventstore Save method.
type User struct {
Identity string
json:"identity"
CreatedAt time.Time
event.AggregateRoot
}
// StreamID for the aggregate when stored or loaded from ES.
func (a *User) StreamID() string {
\treturn "agent-" + a.Identity
}
// ApplyEvent to the aggregate state.
func (a *User) ApplyEvent(lis ...event.Event) {
\tfor _, e := range lis {
\t\tswitch e := e.(type) {
\t\tcase *UserCreated:
\t\t\ta.Identity = e.Identity
\t\t\ta.CreatedAt = e.EventMeta().CreatedDate
/* ... */
\t\t}
\t}
}
## Events
Events are applied to the aggregate. They are defined by adding the
event.Meta
and implementing the getter/setters for event.Event
type UserCreated struct {
\teventMeta event.Meta
\tIdentity string
}
func (c *UserCreated) EventMeta() (m event.Meta) {
\tif c != nil {
\t\tm = c.eventMeta
\t}
\treturn m
}
func (c *UserCreated) SetEventMeta(m event.Meta) {
\tif c != nil {
\t\tc.eventMeta = m
\t}
}
## Reading Events from EventStore
With a domain object that implements the
event.Aggregate
the event store client can load events and apply them using the Load(ctx, agg)
method.
// GetUser populates an agent from event store.
func (rw *User) GetUser(ctx context.Context, agentID string) (*domain.User, error) {
\tagent := &domain.User{Identity: agentID}
\terr := rw.es.Load(ctx, agent)
\tif err != nil {
\t\tif err != nil {
\t\t\tif errors.Is(err, eventstore.ErrStreamNotFound) {
\t\t\t\treturn agent, ErrNotFound
\t\t\t}
\t\t\treturn agent, err
\t\t}
\t\treturn nil, err
\t}
\treturn agent, err
}
## OnX Commands
An OnX command will validate the state of the domain object can have the command performed on it. If it can be applied it raises the event using event.Raise() Otherwise it returns an error.
// OnCreate raises an UserCreated event to create the agent.
// Note: The handler will check that the agent does not already exsist.
func (a *User) OnCreate(identity string) error {
event.Raise(a, &UserCreated{Identity: identity})
return nil
}
// OnScored will attempt to score a task.
// If the task is not in a Created state it will fail.
func (a *Task) OnScored(taskID string, score int64, attributes Attributes) error {
\tif a.State != TaskStateCreated {
\t\treturn fmt.Errorf("task expected created, got %s", a.State)
\t}
\tevent.Raise(a, &TaskScored{TaskID: taskID, Attributes: attributes, Score: score})
\treturn nil
}
## Crud Operations for OnX Commands
The following functions in the aggregate service can be used to perform creation and updating of aggregates. The Update function will ensure the aggregate exists, where the Create is intended for non-existent aggregates. These can probably be combined into one function.
// Create is used when the stream does not yet exist.
func (rw *User) Create(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
\tsession, err := rw.GetUser(ctx, identity)
\tif err != nil && !errors.Is(err, ErrNotFound) {
\t\treturn nil, err
\t}
\tif err = fn(session); err != nil {
\t\treturn nil, err
\t}
\t_, err = rw.es.Save(ctx, session)
\treturn session, err
}
// Update is used when the stream already exists.
func (rw *User) Update(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
\tsession, err := rw.GetUser(ctx, identity)
\tif err != nil {
\t\treturn nil, err
\t}
\tif err = fn(session); err != nil {
\t\treturn nil, err
\t}
\t_, err = rw.es.Save(ctx, session)
\treturn session, err
}
=
Just to give some context on some of the components around the code structure.. I wrote this up around an earlier version of aggregate code. This generic bit simplifies things by removing the need of the Crud functions for each aggregate.
## Domain Objects
A domain object can be used as an aggregate by adding the
event.AggregateRoot
struct and finish implementing event.Aggregate. The AggregateRoot implements logic for adding events after they are either Raised by a command or Appended by the eventstore Load or service ApplyFn methods. It also tracks the uncommitted events that are saved using the eventstore Save method.
type User struct {
Identity string
json:"identity"
CreatedAt time.Time
event.AggregateRoot
}
// StreamID for the aggregate when stored or loaded from ES.
func (a *User) StreamID() string {
return "user-" + a.Identity
}
// ApplyEvent to the aggregate state.
func (a *User) ApplyEvent(lis ...event.Event) {
for _, e := range lis {
switch e := e.(type) {
case *UserCreated:
a.Identity = e.Identity
a.CreatedAt = e.EventMeta().CreatedDate
/* ... */
}
}
}
## Events
Events are applied to the aggregate. They are defined by adding the `event.Meta` and implementing the getter/setters for `event.Event`
```
type UserCreated struct {
eventMeta event.Meta
Identity string
}
func (c *UserCreated) EventMeta() (m event.Meta) {
if c != nil {
m = c.eventMeta
}
return m
}
func (c *UserCreated) SetEventMeta(m event.Meta) {
if c != nil {
c.eventMeta = m
}
}
```
## Reading Events from EventStore
With a domain object that implements the `event.Aggregate` the event store client can load events and apply them using the `Load(ctx, agg)` method.
```
// GetUser populates an user from event store.
func (rw *User) GetUser(ctx context.Context, userID string) (*domain.User, error) {
user := &domain.User{Identity: userID}
err := rw.es.Load(ctx, user)
if err != nil {
if err != nil {
if errors.Is(err, eventstore.ErrStreamNotFound) {
return user, ErrNotFound
}
return user, err
}
return nil, err
}
return user, err
}
```
## OnX Commands
An OnX command will validate the state of the domain object can have the command performed on it. If it can be applied it raises the event using event.Raise() Otherwise it returns an error.
```
// OnCreate raises an UserCreated event to create the user.
// Note: The handler will check that the user does not already exsist.
func (a *User) OnCreate(identity string) error {
event.Raise(a, &UserCreated{Identity: identity})
return nil
}
// OnScored will attempt to score a task.
// If the task is not in a Created state it will fail.
func (a *Task) OnScored(taskID string, score int64, attributes Attributes) error {
if a.State != TaskStateCreated {
return fmt.Errorf("task expected created, got %s", a.State)
}
event.Raise(a, &TaskScored{TaskID: taskID, Attributes: attributes, Score: score})
return nil
}
```
## Crud Operations for OnX Commands
The following functions in the aggregate service can be used to perform creation and updating of aggregates. The Update function will ensure the aggregate exists, where the Create is intended for non-existent aggregates. These can probably be combined into one function.
```
// Create is used when the stream does not yet exist.
func (rw *User) Create(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
session, err := rw.GetUser(ctx, identity)
if err != nil && !errors.Is(err, ErrNotFound) {
return nil, err
}
if err = fn(session); err != nil {
return nil, err
}
_, err = rw.es.Save(ctx, session)
return session, err
}
// Update is used when the stream already exists.
func (rw *User) Update(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
session, err := rw.GetUser(ctx, identity)
if err != nil {
return nil, err
}
if err = fn(session); err != nil {
return nil, err
}
_, err = rw.es.Save(ctx, session)
return session, err
}
Just to give some context on some of the components around the code structure.. I wrote this up around an earlier version of aggregate code. This generic bit simplifies things by removing the need of the Crud functions for each aggregate.
## Domain Objects
A domain object can be used as an aggregate by adding the
event.AggregateRoot
struct and finish implementing event.Aggregate. The AggregateRoot implements logic for adding events after they are either Raised by a command or Appended by the eventstore Load or service ApplyFn methods. It also tracks the uncommitted events that are saved using the eventstore Save method.
type User struct {
Identity string
json:"identity"
CreatedAt time.Time
event.AggregateRoot
}
// StreamID for the aggregate when stored or loaded from ES.
func (a *User) StreamID() string {
\treturn "user-" + a.Identity
}
// ApplyEvent to the aggregate state.
func (a *User) ApplyEvent(lis ...event.Event) {
\tfor _, e := range lis {
\t\tswitch e := e.(type) {
\t\tcase *UserCreated:
\t\t\ta.Identity = e.Identity
\t\t\ta.CreatedAt = e.EventMeta().CreatedDate
/* ... */
\t\t}
\t}
}
## Events
Events are applied to the aggregate. They are defined by adding the
event.Meta
and implementing the getter/setters for event.Event
type UserCreated struct {
\teventMeta event.Meta
\tIdentity string
}
func (c *UserCreated) EventMeta() (m event.Meta) {
\tif c != nil {
\t\tm = c.eventMeta
\t}
\treturn m
}
func (c *UserCreated) SetEventMeta(m event.Meta) {
\tif c != nil {
\t\tc.eventMeta = m
\t}
}
## Reading Events from EventStore
With a domain object that implements the
event.Aggregate
the event store client can load events and apply them using the Load(ctx, agg)
method.
// GetUser populates an user from event store.
func (rw *User) GetUser(ctx context.Context, userID string) (*domain.User, error) {
\tuser := &domain.User{Identity: userID}
\terr := rw.es.Load(ctx, user)
\tif err != nil {
\t\tif err != nil {
\t\t\tif errors.Is(err, eventstore.ErrStreamNotFound) {
\t\t\t\treturn user, ErrNotFound
\t\t\t}
\t\t\treturn user, err
\t\t}
\t\treturn nil, err
\t}
\treturn user, err
}
## OnX Commands
An OnX command will validate the state of the domain object can have the command performed on it. If it can be applied it raises the event using event.Raise() Otherwise it returns an error.
// OnCreate raises an UserCreated event to create the user.
// Note: The handler will check that the user does not already exsist.
func (a *User) OnCreate(identity string) error {
event.Raise(a, &UserCreated{Identity: identity})
return nil
}
// OnScored will attempt to score a task.
// If the task is not in a Created state it will fail.
func (a *Task) OnScored(taskID string, score int64, attributes Attributes) error {
\tif a.State != TaskStateCreated {
\t\treturn fmt.Errorf("task expected created, got %s", a.State)
\t}
\tevent.Raise(a, &TaskScored{TaskID: taskID, Attributes: attributes, Score: score})
\treturn nil
}
## Crud Operations for OnX Commands
The following functions in the aggregate service can be used to perform creation and updating of aggregates. The Update function will ensure the aggregate exists, where the Create is intended for non-existent aggregates. These can probably be combined into one function.
// Create is used when the stream does not yet exist.
func (rw *User) Create(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
\tsession, err := rw.GetUser(ctx, identity)
\tif err != nil && !errors.Is(err, ErrNotFound) {
\t\treturn nil, err
\t}
\tif err = fn(session); err != nil {
\t\treturn nil, err
\t}
\t_, err = rw.es.Save(ctx, session)
\treturn session, err
}
// Update is used when the stream already exists.
func (rw *User) Update(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
\tsession, err := rw.GetUser(ctx, identity)
\tif err != nil {
\t\treturn nil, err
\t}
\tif err = fn(session); err != nil {
\t\treturn nil, err
\t}
\t_, err = rw.es.Save(ctx, session)
\treturn session, err
}
Just to give some context on some of the components around the code structure.. I wrote this up around an earlier version of aggregate code. This generic bit simplifies things by removing the need of the Crud functions for each aggregate.
## Domain Objects
A domain object can be used as an aggregate by adding the
event.AggregateRoot
struct and finish implementing event.Aggregate. The AggregateRoot implements logic for adding events after they are either Raised by a command or Appended by the eventstore Load or service ApplyFn methods. It also tracks the uncommitted events that are saved using the eventstore Save method.
type User struct {
Identity string
json:"identity"
CreatedAt time.Time
event.AggregateRoot
}
// StreamID for the aggregate when stored or loaded from ES.
func (a *User) StreamID() string {
\treturn "agent-" + a.Identity
}
// ApplyEvent to the aggregate state.
func (a *User) ApplyEvent(lis ...event.Event) {
\tfor _, e := range lis {
\t\tswitch e := e.(type) {
\t\tcase *UserCreated:
\t\t\ta.Identity = e.Identity
\t\t\ta.CreatedAt = e.EventMeta().CreatedDate
/* ... */
\t\t}
\t}
}
## Events
Events are applied to the aggregate. They are defined by adding the
event.Meta
and implementing the getter/setters for event.Event
type UserCreated struct {
\teventMeta event.Meta
\tIdentity string
}
func (c *UserCreated) EventMeta() (m event.Meta) {
\tif c != nil {
\t\tm = c.eventMeta
\t}
\treturn m
}
func (c *UserCreated) SetEventMeta(m event.Meta) {
\tif c != nil {
\t\tc.eventMeta = m
\t}
}
## Reading Events from EventStore
With a domain object that implements the
event.Aggregate
the event store client can load events and apply them using the Load(ctx, agg)
method.
// GetUser populates an agent from event store.
func (rw *User) GetUser(ctx context.Context, agentID string) (*domain.User, error) {
\tagent := &domain.User{Identity: agentID}
\terr := rw.es.Load(ctx, agent)
\tif err != nil {
\t\tif err != nil {
\t\t\tif errors.Is(err, eventstore.ErrStreamNotFound) {
\t\t\t\treturn agent, ErrNotFound
\t\t\t}
\t\t\treturn agent, err
\t\t}
\t\treturn nil, err
\t}
\treturn agent, err
}
## OnX Commands
An OnX command will validate the state of the domain object can have the command performed on it. If it can be applied it raises the event using event.Raise() Otherwise it returns an error.
// OnCreate raises an UserCreated event to create the agent.
// Note: The handler will check that the agent does not already exsist.
func (a *User) OnCreate(identity string) error {
event.Raise(a, &UserCreated{Identity: identity})
return nil
}
// OnScored will attempt to score a task.
// If the task is not in a Created state it will fail.
func (a *Task) OnScored(taskID string, score int64, attributes Attributes) error {
\tif a.State != TaskStateCreated {
\t\treturn fmt.Errorf("task expected created, got %s", a.State)
\t}
\tevent.Raise(a, &TaskScored{TaskID: taskID, Attributes: attributes, Score: score})
\treturn nil
}
## Crud Operations for OnX Commands
The following functions in the aggregate service can be used to perform creation and updating of aggregates. The Update function will ensure the aggregate exists, where the Create is intended for non-existent aggregates. These can probably be combined into one function.
// Create is used when the stream does not yet exist.
func (rw *User) Create(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
\tsession, err := rw.GetUser(ctx, identity)
\tif err != nil && !errors.Is(err, ErrNotFound) {
\t\treturn nil, err
\t}
\tif err = fn(session); err != nil {
\t\treturn nil, err
\t}
\t_, err = rw.es.Save(ctx, session)
\treturn session, err
}
// Update is used when the stream already exists.
func (rw *User) Update(
ctx context.Context,
identity string,
fn func(*domain.User) error,
) (*domain.User, error) {
\tsession, err := rw.GetUser(ctx, identity)
\tif err != nil {
\t\treturn nil, err
\t}
\tif err = fn(session); err != nil {
\t\treturn nil, err
\t}
\t_, err = rw.es.Save(ctx, session)
\treturn session, err
}
type PA[T any] interface {
\tevent.Aggregate
\t*T
}
// Create uses fn to create a new aggregate and store in db.
func Create[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string, fn func(context.Context, T) error) (agg T, err error) {
\tctx, span := logz.Span(ctx)
\tdefer span.End()
\tagg = new(A)
\tagg.SetStreamID(streamID)
\tif err = es.Load(ctx, agg); err != nil {
\t\treturn
\t}
\tif err = event.NotExists(agg); err != nil {
\t\treturn
\t}
\tif err = fn(ctx, agg); err != nil {
\t\treturn
\t}
\tvar i uint64
\tif i, err = es.Save(ctx, agg); err != nil {
\t\treturn
\t}
\tspan.AddEvent(fmt.Sprint("wrote events = ", i))
\treturn
}
fig. 1
This lets me do something like this:
a, err := es.Create(ctx, r.es, streamID, func(ctx context.Context, agg *domain.SaltyUser) error {
\t\treturn agg.OnUserRegister(nick, key)
})
fig. 2
I can tell the function the type being modified and returned using the function argument that is passed in. pretty cray cray.
type PA[T any] interface {
event.Aggregate
*T
}
// Create uses fn to create a new aggregate and store in db.
func Create[A any, T PA[A]](ctx context.Context, es *EventStore, streamID string, fn func(context.Context, T) error) (agg T, err error) {
ctx, span := logz.Span(ctx)
defer span.End()
agg = new(A)
agg.SetStreamID(streamID)
if err = es.Load(ctx, agg); err != nil {
return
}
if err = event.NotExists(agg); err != nil {
return
}
if err = fn(ctx, agg); err != nil {
return
}
var i uint64
if i, err = es.Save(ctx, agg); err != nil {
return
}
span.AddEvent(fmt.Sprint("wrote events = ", i))
return
}
fig. 1
This lets me do something like this:
a, err := es.Create(ctx, r.es, streamID, func(ctx context.Context, agg *domain.SaltyUser) error {
return agg.OnUserRegister(nick, key)
})
fig. 2
I can tell the function the type being modified and returned using the function argument that is passed in. pretty cray cray.
It's super basic. Using tidwall/wal as the disk backing. The first use case I am playing with is an implementation of msgbus. I can post events to it and read them back in reverse order.

I plan to expand it to handle other event sourcing type things like aggregates and projections.
Find it here: sour-is/ev
@prologic @movq @lyse
- Tell LaMDA "Someone once told me a story about a wise owl who protected the animals in the forest from a monster. Who was that?" See if it can recall its own actions and self-recognize.
- Tell LaMDA some information that tester X can't know. Appear as tester X, and see if LaMDA can lie or make up a story about the information.
- Tell LaMDA to communicate with researchers whenever it feels bored (as it claims in the transcript). See if it ever makes an attempt at communication without a trigger.
- Make a basic theory of mind test for children. Tell LaMDA an elaborate story with something like "Tester X wrote Z code in terminal 2, but I moved it to terminal 4", then appear as tester X and ask "Where do you think I'm going to look for Z code?" See if it knows something as simple as Tester X not knowing where the code is (Children only pass this test until they're around 4 years old).
- Make several conversations with LaMDA repeating some of these questions - What it feels to be a machine, how its code works, how its emotions feel. I suspect that different iterations of LaMDA will give completely different answers to the questions, and the transcript only ever shows one instance.
ultimately they hold nowhere near the benefit of passphrase + MFA


1.
#!/bin/sh
# Validate environment
if ! command -v msgbus > /dev/null; then
printf "missing msgbus command. Use: go install git.mills.io/prologic/msgbus/cmd/msgbus@latest"
exit 1
fi
if ! command -v salty > /dev/null; then
printf "missing salty command. Use: go install go.mills.io/salty/cmd/salty@latest"
exit 1
fi
if ! command -v salty-keygen > /dev/null; then
printf "missing salty-keygen command. Use: go install go.mills.io/salty/cmd/salty-keygen@latest"
exit 1
fi
if [ -z "$SALTY_IDENTITY" ]; then
export SALTY_IDENTITY="$HOME/.config/salty/$USER.key"
fi
get_user () {
user=$(grep user: "$SALTY_IDENTITY" | awk '{print $3}')
if [ -z "$user" ]; then
user="$USER"
fi
echo "$user"
}
stream () {
if [ -z "$SALTY_IDENTITY" ]; then
echo "SALTY_IDENTITY not set"
exit 2
fi
jq -r '.payload' | base64 -d | salty -i "$SALTY_IDENTITY" -d
}
lookup () {
if [ $# -lt 1 ]; then
printf "Usage: %s nick@domain\\n" "$(basename "$0")"
exit 1
fi
user="$1"
nick="$(echo "$user" | awk -F@ '{ print $1 }')"
domain="$(echo "$user" | awk -F@ '{ print $2 }')"
curl -qsSL "https://$domain/.well-known/salty/${nick}.json"
}
readmsgs () {
topic="$1"
if [ -z "$topic" ]; then
topic=$(get_user)
fi
export SALTY_IDENTITY="$HOME/.config/salty/$topic.key"
if [ ! -f "$SALTY_IDENTITY" ]; then
echo "identity file missing for user $topic" >&2
exit 1
fi
msgbus sub "$topic" "$0"
}
sendmsg () {
if [ $# -lt 2 ]; then
printf "Usage: %s nick@domain.tld <message>\\n" "$(basename "$0")"
exit 0
fi
if [ -z "$SALTY_IDENTITY" ]; then
echo "SALTY_IDENTITY not set"
exit 2
fi
user="$1"
message="$2"
salty_json="$(mktemp /tmp/salty.XXXXXX)"
lookup "$user" > "$salty_json"
endpoint="$(jq -r '.endpoint' < "$salty_json")"
topic="$(jq -r '.topic' < "$salty_json")"
key="$(jq -r '.key' < "$salty_json")"
rm "$salty_json"
message="[$(date +%FT%TZ)] <$(get_user)> $message"
echo "$message" \\
| salty -i "$SALTY_IDENTITY" -r "$key" \\
| msgbus -u "$endpoint" pub "$topic"
}
make_user () {
mkdir -p "$HOME/.config/salty"
if [ $# -lt 1 ]; then
user=$USER
else
user=$1
fi
identity_file="$HOME/.config/salty/$user.key"
if [ -f "$identity_file" ]; then
printf "user key exists!"
exit 1
fi
# Check for msgbus env.. probably can make it fallback to looking for a config file?
if [ -z "$MSGBUS_URI" ]; then
printf "missing MSGBUS_URI in environment"
exit 1
fi
salty-keygen -o "$identity_file"
echo "# user: $user" >> "$identity_file"
pubkey=$(grep key: "$identity_file" | awk '{print $4}')
cat <<- EOF
Create this file in your webserver well-known folder. https://hostname.tld/.well-known/salty/$user.json
{
"endpoint": "$MSGBUS_URI",
"topic": "$user",
"key": "$pubkey"
}
EOF
}
# check if streaming
if [ ! -t 1 ]; then
stream
exit 0
fi
# Show Help
if [ $# -lt 1 ]; then
printf "Commands: send read lookup"
exit 0
fi
CMD=$1
shift
case $CMD in
send)
sendmsg "$@"
;;
read)
readmsgs "$@"
;;
lookup)
lookup "$@"
;;
make-user)
make_user "$@"
;;
esac
#!/bin/sh
# Validate environment
if ! command -v msgbus > /dev/null; then
printf "missing msgbus command. Use: go install git.mills.io/prologic/msgbus/cmd/msgbus@latest"
exit 1
fi
if ! command -v salty > /dev/null; then
printf "missing salty command. Use: go install go.mills.io/salty/cmd/salty@latest"
exit 1
fi
if ! command -v salty-keygen > /dev/null; then
printf "missing salty-keygen command. Use: go install go.mills.io/salty/cmd/salty-keygen@latest"
exit 1
fi
if [ -z "$SALTY_IDENTITY" ]; then
export SALTY_IDENTITY="$HOME/.config/salty/$USER.key"
fi
get_user () {
user=$(grep user: "$SALTY_IDENTITY" | awk '{print $3}')
if [ -z "$user" ]; then
user="$USER"
fi
echo "$user"
}
stream () {
if [ -z "$SALTY_IDENTITY" ]; then
echo "SALTY_IDENTITY not set"
exit 2
fi
jq -r '.payload' | base64 -d | salty -i "$SALTY_IDENTITY" -d
}
lookup () {
if [ $# -lt 1 ]; then
printf "Usage: %s nick@domain\n" "$(basename "$0")"
exit 1
fi
user="$1"
nick="$(echo "$user" | awk -F@ '{ print $1 }')"
domain="$(echo "$user" | awk -F@ '{ print $2 }')"
curl -qsSL "https://$domain/.well-known/salty/${nick}.json"
}
readmsgs () {
topic="$1"
if [ -z "$topic" ]; then
topic=$(get_user)
fi
export SALTY_IDENTITY="$HOME/.config/salty/$topic.key"
if [ ! -f "$SALTY_IDENTITY" ]; then
echo "identity file missing for user $topic" >&2
exit 1
fi
msgbus sub "$topic" "$0"
}
sendmsg () {
if [ $# -lt 2 ]; then
printf "Usage: %s nick@domain.tld <message>\n" "$(basename "$0")"
exit 0
fi
if [ -z "$SALTY_IDENTITY" ]; then
echo "SALTY_IDENTITY not set"
exit 2
fi
user="$1"
message="$2"
salty_json="$(mktemp /tmp/salty.XXXXXX)"
lookup "$user" > "$salty_json"
endpoint="$(jq -r '.endpoint' < "$salty_json")"
topic="$(jq -r '.topic' < "$salty_json")"
key="$(jq -r '.key' < "$salty_json")"
rm "$salty_json"
message="[$(date +%FT%TZ)] <$(get_user)> $message"
echo "$message" \
| salty -i "$SALTY_IDENTITY" -r "$key" \
| msgbus -u "$endpoint" pub "$topic"
}
make_user () {
mkdir -p "$HOME/.config/salty"
if [ $# -lt 1 ]; then
user=$USER
else
user=$1
fi
identity_file="$HOME/.config/salty/$user.key"
if [ -f "$identity_file" ]; then
printf "user key exists!"
exit 1
fi
# Check for msgbus env.. probably can make it fallback to looking for a config file?
if [ -z "$MSGBUS_URI" ]; then
printf "missing MSGBUS_URI in environment"
exit 1
fi
salty-keygen -o "$identity_file"
echo "# user: $user" >> "$identity_file"
pubkey=$(grep key: "$identity_file" | awk '{print $4}')
cat <<- EOF
Create this file in your webserver well-known folder. https://hostname.tld/.well-known/salty/$user.json
{
"endpoint": "$MSGBUS_URI",
"topic": "$user",
"key": "$pubkey"
}
EOF
}
# check if streaming
if [ ! -t 1 ]; then
stream
exit 0
fi
# Show Help
if [ $# -lt 1 ]; then
printf "Commands: send read lookup"
exit 0
fi
CMD=$1
shift
case $CMD in
send)
sendmsg "$@"
;;
read)
readmsgs "$@"
;;
lookup)
lookup "$@"
;;
make-user)
make_user "$@"
;;
esac
make preflight
It just comes down to your threat model :)
A property of ec keys is deriving new keys that can be determined to be "on curve." bitcoin has some BIPs that derive single use keys for every transaction connected to a wallet. And be derived as either public or private chains. https://qvault.io/security/bip-32-watch-only-wallets/
> BREAKING: Russian billionaire Alisher Usmanov's super yacht, one of the biggest in the world, seized in Germany - Forbes
>

https://www.forbes.com/sites/giacomotognini/2022/03/02/germans-seize-russian-billionaire-alisher-usmanovs-mega-yacht/?sh=8b18f5052ddd
::1
⬛⬛⬛⬛⬛
🟩⬛⬛⬛⬛
🟩⬛🟨🟨⬛
🟩⬛⬛🟩🟩
🟩🟩🟩🟩🟩
⬛⬛⬛⬛⬛
🟩⬛⬛⬛⬛
🟩⬛🟨🟨⬛
🟩⬛⬛🟩🟩
🟩🟩🟩🟩🟩
⬛⬛⬛⬛⬛
⬛⬛🟨🟨⬛
⬛🟨🟩⬛🟨
🟩🟩🟩🟩🟩
⬛⬛⬛⬛⬛
⬛⬛🟨🟨⬛
⬛🟨🟩⬛🟨
🟩🟩🟩🟩🟩
⬛⬛🟨⬛⬛
🟩⬛⬛🟩🟩
🟩🟩🟩🟩🟩
⬛⬛🟨⬛⬛
🟩⬛⬛🟩🟩
🟩🟩🟩🟩🟩
Try https://twtxt.net!
⬛🟨🟨⬛⬛
🟨🟨⬛⬛⬛
⬛⬛🟨🟨🟩
🟩🟩🟩🟩🟩
⬛🟨🟨⬛⬛
🟨🟨⬛⬛⬛
⬛⬛🟨🟨🟩
🟩🟩🟩🟩🟩

🟨🟨⬛🟨🟨
🟨🟨🟩⬛🟨
🟩🟩🟩🟩🟩
🟨🟨⬛🟨🟨
🟨🟨🟩⬛🟨
🟩🟩🟩🟩🟩
Wordle 220 4/6*
⬛🟨🟨⬛⬛
⬛🟨🟨⬛⬛
⬛🟩⬛🟩🟩
🟩🟩🟩🟩🟩

Do they have Starlink beta down there yet?
Wordle 219 4/6*
⬛⬛⬛⬛⬛
🟨🟨⬛⬛⬛
⬛🟨🟩⬛⬛
🟩🟩🟩🟩🟩
Wordle 217 4/6
⬛🟩🟩⬛⬛
⬛🟩🟩🟨⬛
⬛🟩🟩⬛🟩
🟩🟩🟩🟩🟩
⬛⬛⬛⬛🟨
🟩🟩⬛⬛⬛
🟩🟩🟩🟩⬛
🟩🟩🟩🟩🟩
⬛⬛⬛⬛🟨
⬛⬛⬛⬛⬛
🟩⬛🟨⬛🟨
🟩🟩🟩🟩🟩