Skip to content
Open
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
285 changes: 278 additions & 7 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 2 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ napi = { version = "3", default-features = false, features = ["napi6", "serde-js
napi-derive = "3"
napi-build = "2"
thiserror = "2.0"
# Blocking HTTPS for `trust follow` / `trust pull` (rustls, no async runtime).
ureq = "3.4"

assert_cmd = "2.2"
hex = "0.4"
Expand Down
48 changes: 46 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,9 @@ Everything rests on one pure Rust core, shipped as a command line tool
outward with decreasing weight (the **Trust Cascade**).
- **Unix-friendly.** JSON in, JSON out, one item per line. Commands pipe into
each other and into `jq`.
- **Offline.** Nothing needs a server or a network connection.
- **Offline.** Nothing needs a server or a network connection. To share,
publish your atoms as static files on any web host and follow other
people's (see [Sharing](#sharing)).
- **Runs everywhere.** The same core runs on the command line, in browsers,
Cloudflare Workers, Deno, Node, and inside reactive database queries such as
Convex's. See [architecture](doc/architecture.md).
Expand Down Expand Up @@ -95,6 +97,10 @@ trust lens --topic sushi --rollup | trust sign | trust add
| `trust add [FILE]` | Add atoms or signed credentials to the local store |
| `trust query [--source] [--target] [--topic] [--signed-only]` | Search the local store |
| `trust lens [AGENT] [--topic] [--depth] [--decay] [--rollup]` | View the graph through an agent's lens |
| `trust publish [--out DIR] [--well-known] [--as KEY]` | Write your signed atoms as a static feed |
| `trust follow URL\|DOMAIN\|PATH` | Follow a feed: fetch it, verify every atom, store them |
| `trust pull [FEED]` | Fetch what's new from the feeds you follow (or one feed) |
| `trust following` / `trust unfollow FEED` | List or stop following feeds |
| `trust info` | Show where keys and data live |
| `trust completions SHELL` | Shell completions |

Expand All @@ -103,6 +109,44 @@ or concatenated JSON. Add `--pretty` to any command for readable output. Keys
and the store live in the platform data directory, or in `$TRUST_HOME`.
`$TRUST_KEY` selects the key.

## Sharing

Share your ratings by publishing them as a **feed**: two static files
(`index.json`, signed by you, and `atoms.ndjson`, your signed atoms) that any
web host can serve. Followers verify every signature before storing
anything, so the host doesn't need to be trusted. Details in
[doc/feeds.md](doc/feeds.md).

With GitHub Pages, in a repository called `trust`:

```sh
# Alice: publish, then push. The feed is at https://alice.github.io/trust/
trust publish --out docs
git add docs && git commit -m "Update my trust feed" && git push
# (once: Settings → Pages → Deploy from a branch → main, /docs)

# Bob: follow Alice, and check for updates whenever you like.
trust follow https://alice.github.io/trust/
trust pull
trust lens --topic sushi # Alice's ratings now count, as far as Bob trusts her
```

With your own domain, publish to the site root with `--well-known`, and
people can follow the bare domain:

```sh
trust publish --out site --well-known # site/.well-known/trust/{index.json,atoms.ndjson}
trust follow alice.example # → https://alice.example/.well-known/trust/index.json
```

Pulls are cheap: `trust` remembers each feed's `ETag`, so an unchanged feed
costs one `304 Not Modified`. It also pins each feed's owner and refuses to
go back to an older copy. A local directory or `file://` URL works in place
of a URL, which is handy for testing or for sharing over a synced folder.

Feeds are signed by `did:key` identities today. Signing with a domain
(`did:web`) is planned.

## Data model

A Trust Atom:
Expand Down Expand Up @@ -190,7 +234,7 @@ tg.lens([credential /* , …everyone else's atoms */], me.did, { topic: "sushi"
| [`bindings/`](bindings) | TypeScript | Types shared by both npm packages |
| [`tests/js/`](tests/js) | JavaScript | One smoke test run against every JavaScript build, plus a benchmark |
| [`scripts/`](scripts) | Shell | Core purity check, WebAssembly packaging |
| [`doc/`](doc) | | [Architecture](doc/architecture.md), [roadmap](doc/plan/README.md), and the [original protocol text](doc/protocol/README.md) (historical) |
| [`doc/`](doc) | | [Architecture](doc/architecture.md), [feeds](doc/feeds.md), [roadmap](doc/plan/README.md), and the [original protocol text](doc/protocol/README.md) (historical) |
| [`schema/`](schema) | JSON-LD | [`legacy-2017/`](schema/legacy-2017): the 2017 `TrustClaim` context (historical) |

New projects go in this repo:
Expand Down
37 changes: 37 additions & 0 deletions bindings/trustgraph.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,39 @@ export interface LensEntry {
raters: number;
}

/** A feed's signed index (`index.json`). See doc/feeds.md. */
export interface FeedIndex {
"@context": string[];
type: "TrustFeedIndex";
/** The owner's DID: it signs the index and issued every atom. */
owner: string;
/** RFC 3339. */
updated: string;
atoms: {
count: number;
/** SHA2-256 multihash (`Qm…`) of the exact bytes of `atoms.ndjson`. */
digest: string;
};
proof?: Credential["proof"];
}

/** A feed's two files: `index.json` (parsed) and `atoms.ndjson` (text). */
export interface Feed {
index: FeedIndex;
atoms: string;
}

export interface FeedVerification {
valid: boolean;
owner?: string;
updated?: string;
/** Content IDs, in file order. */
ids?: string[];
/** The verified atoms, in file order. */
atoms?: TrustAtom[];
error?: string;
}

/** Atoms and/or signed credentials. */
export type Item = TrustAtomInput | Credential;

Expand All @@ -101,3 +134,7 @@ export function verify(credential: Credential): Verification;
export function lens(items: Item[], root: string, options?: LensOptions | null): LensEntry[];
/** Rollup atoms (unsigned) for `root`'s lens, timestamped `at` (RFC 3339). */
export function rollup(items: Item[], root: string, options: LensOptions | null | undefined, at: string): TrustAtom[];
/** Builds a feed from credentials all signed by the key's owner. `updated` is RFC 3339. */
export function buildFeed(credentials: Credential[], secretKeyMultibase: string, updated: string): Feed;
/** Verifies a feed: the index's signature, the digest of `atoms` (the exact text of `atoms.ndjson`), and every atom. */
export function verifyFeed(index: FeedIndex, atoms: string): FeedVerification;
1 change: 1 addition & 0 deletions crates/trustgraph-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ directories.workspace = true
jiff = { workspace = true, features = ["std"] }
serde.workspace = true
serde_json.workspace = true
ureq.workspace = true

[dev-dependencies]
assert_cmd.workspace = true
Expand Down
52 changes: 51 additions & 1 deletion crates/trustgraph-cli/src/cli.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,9 @@ Examples:
trust atom --target https://example.com/sushi-bar --content sushi --value 0.9 --sign | trust add
trust query --topic sushi
trust lens --topic sushi
trust lens --rollup | trust sign | trust add";
trust lens --rollup | trust sign | trust add
trust publish --out site --well-known
trust follow alice.example && trust pull";

#[derive(Debug, Subcommand)]
pub enum Command {
Expand Down Expand Up @@ -68,6 +70,27 @@ pub enum Command {
/// ratings of the agents they trust, cascading outward
Lens(LensArgs),

/// Publish your signed atoms as a static feed that any web host can serve
Publish(PublishArgs),

/// Follow a feed: fetch it, verify every atom, and add them to the store
Follow(FollowArgs),

/// Stop following a feed (atoms already pulled stay in the store)
Unfollow {
/// The feed, as given to `trust follow`
feed: String,
},

/// List the feeds you follow
Following,

/// Fetch new atoms from the feeds you follow, or from one feed
Pull {
/// A URL, domain, or local path [default: every feed you follow]
feed: Option<String>,
},

/// Show where keys and data are kept
Info,

Expand Down Expand Up @@ -160,6 +183,33 @@ pub struct AtomArgs {
pub key: KeyArg,
}

#[derive(Debug, Args)]
pub struct PublishArgs {
/// Directory to write the feed to
#[arg(long, short, default_value = "feed", value_name = "DIR")]
pub out: PathBuf,

/// Write to DIR/.well-known/trust/, so `trust follow DOMAIN` finds it
/// when DIR is the root of the site
#[arg(long)]
pub well_known: bool,

/// Name of the key whose atoms to publish
#[arg(long, visible_alias = "as", env = "TRUST_KEY", default_value = "default", value_parser = parse_key_name)]
pub key: String,
}

#[derive(Debug, Args)]
pub struct FollowArgs {
/// The feed: an https:// URL, a bare domain (looks in
/// /.well-known/trust/), or a local path or file:// URL
pub feed: String,

/// Only add the feed to the list; don't pull it now
#[arg(long)]
pub no_pull: bool,
}

#[derive(Debug, Args)]
pub struct InputArgs {
/// JSON or NDJSON input file; `-` or nothing reads stdin
Expand Down
Loading
Loading