OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge commits / 2e658d4

0.9.96: an operators page, and the bridge's own actor on its own page
⛔ THE BRIDGE'S OWN ACTOR WAS INVISIBLE ON THE OPERATOR'S PAGE. `p2p@ap.7rnx.net` answers 200 and appears nowhere in registrations.json — it comes from AP_USERNAME, not the name registry — so the page rendered every name except the one the bridge was stood up to serve. Reported as "it's missing tabertron's ap name", which turned out to be two different things: the primary actor really was missing, and tabertron genuinely has no name here. Its root is 96b89f8750ff536e… while the two registered roots are 962f57d6… (ashatron, stortron's) and 883acc2f… (fleeky, the handset). Shown with where it comes from, and that it cannot be released here, because a release button that silently did nothing would be worse than its absence. ⛔ MORE BOOTSTRAP TOKENS WOULD BE RE-ARMING TOFU. The token binds the FIRST claim to whoever can read the service log; browsers.js closes that door permanently — "there is no window that reopens, and revoking the claimant does NOT reopen it either" — and minting more would reopen the instance to whoever reached it first, on a surface now bound to 0.0.0.0. The thing actually wanted is a second operator, and that is an invite: single-use, expiring, issued by someone already signed in and therefore already authorised. The test asserts the spent bootstrap token admits nobody afterwards and the invite does. ⛔ YOU CANNOT REVOKE THE BROWSER YOU ARE HOLDING. It would lock the bridge to whatever other operators exist, and with none, to nobody — there is no way back in, because the bootstrap token gates only the first claim and the instance is already claimed. Revoked operators stay LISTED rather than hidden: a revoked identity is still registered, which is exactly why revoking the claimant does not reopen the instance, and hiding it would read as "removed". 1029/1029 pass, with 9 covering the gate. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2e658d48ba40e325eaa9ba58b2889d03d084e196
by plankatron <fleeky@gmail.com> · 2026-09-11T23:59:25.000Z

Changed files (5) +248 -102

.pgh-lock.json +93 -93
……@@ -1,9 +1,9 @@
11{
22 "name": "ap-bridge",
3 "version": "0.9.94",
3 "version": "0.9.95",
44 "runtime": "bare",
5 "rootTreeHash": "6fe6b3e99fd039bef4761d288db9df4948e96d97ef0354a6b5ccfb8eb2c1e60f",
6 "publishedAt": "2026-09-11T23:48:07.594Z",
5 "rootTreeHash": "09bafc3164c08bc4374e18078791139c4dcfe80210f43b5ae38ed0ee77437479",
6 "publishedAt": "2026-09-11T23:53:28.457Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -4004,8 +4004,8 @@
40044004 "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37"
40054005 }
40064006 ],
4007 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4008 "acceptedAt": "2026-09-11T23:48:07.627Z"
4007 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4008 "acceptedAt": "2026-09-11T23:53:28.481Z"
40094009 },
40104010 {
40114011 "type": "VERSION_CONFLICT",
……@@ -4019,8 +4019,8 @@
40194019 "tree": "ee91b6bd77101e3404ca571b2e5adf0f47029d174398a05f50294fc207e39d99"
40204020 }
40214021 ],
4022 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4023 "acceptedAt": "2026-09-11T23:48:07.627Z"
4022 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4023 "acceptedAt": "2026-09-11T23:53:28.481Z"
40244024 },
40254025 {
40264026 "type": "VERSION_CONFLICT",
……@@ -4034,8 +4034,8 @@
40344034 "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11"
40354035 }
40364036 ],
4037 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4038 "acceptedAt": "2026-09-11T23:48:07.627Z"
4037 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4038 "acceptedAt": "2026-09-11T23:53:28.481Z"
40394039 },
40404040 {
40414041 "type": "VERSION_CONFLICT",
……@@ -4054,8 +4054,8 @@
40544054 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
40554055 }
40564056 ],
4057 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4058 "acceptedAt": "2026-09-11T23:48:07.627Z"
4057 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4058 "acceptedAt": "2026-09-11T23:53:28.481Z"
40594059 },
40604060 {
40614061 "type": "VERSION_CONFLICT",
……@@ -4069,8 +4069,8 @@
40694069 "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd"
40704070 }
40714071 ],
4072 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4073 "acceptedAt": "2026-09-11T23:48:07.627Z"
4072 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4073 "acceptedAt": "2026-09-11T23:53:28.481Z"
40744074 },
40754075 {
40764076 "type": "VERSION_CONFLICT",
……@@ -4084,8 +4084,8 @@
40844084 "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95"
40854085 }
40864086 ],
4087 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4088 "acceptedAt": "2026-09-11T23:48:07.627Z"
4087 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4088 "acceptedAt": "2026-09-11T23:53:28.481Z"
40894089 },
40904090 {
40914091 "type": "VERSION_CONFLICT",
……@@ -4099,8 +4099,8 @@
40994099 "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb"
41004100 }
41014101 ],
4102 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4103 "acceptedAt": "2026-09-11T23:48:07.627Z"
4102 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4103 "acceptedAt": "2026-09-11T23:53:28.481Z"
41044104 },
41054105 {
41064106 "type": "VERSION_CONFLICT",
……@@ -4114,8 +4114,8 @@
41144114 "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a"
41154115 }
41164116 ],
4117 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4118 "acceptedAt": "2026-09-11T23:48:07.627Z"
4117 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4118 "acceptedAt": "2026-09-11T23:53:28.481Z"
41194119 },
41204120 {
41214121 "type": "VERSION_CONFLICT",
……@@ -4129,8 +4129,8 @@
41294129 "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca"
41304130 }
41314131 ],
4132 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4133 "acceptedAt": "2026-09-11T23:48:07.627Z"
4132 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4133 "acceptedAt": "2026-09-11T23:53:28.481Z"
41344134 },
41354135 {
41364136 "type": "VERSION_CONFLICT",
……@@ -4144,8 +4144,8 @@
41444144 "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701"
41454145 }
41464146 ],
4147 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4148 "acceptedAt": "2026-09-11T23:48:07.627Z"
4147 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4148 "acceptedAt": "2026-09-11T23:53:28.481Z"
41494149 },
41504150 {
41514151 "type": "VERSION_CONFLICT",
……@@ -4159,8 +4159,8 @@
41594159 "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d"
41604160 }
41614161 ],
4162 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4163 "acceptedAt": "2026-09-11T23:48:07.627Z"
4162 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4163 "acceptedAt": "2026-09-11T23:53:28.481Z"
41644164 },
41654165 {
41664166 "type": "VERSION_CONFLICT",
……@@ -4174,8 +4174,8 @@
41744174 "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04"
41754175 }
41764176 ],
4177 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4178 "acceptedAt": "2026-09-11T23:48:07.627Z"
4177 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4178 "acceptedAt": "2026-09-11T23:53:28.481Z"
41794179 },
41804180 {
41814181 "type": "VERSION_CONFLICT",
……@@ -4189,8 +4189,8 @@
41894189 "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4"
41904190 }
41914191 ],
4192 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4193 "acceptedAt": "2026-09-11T23:48:07.627Z"
4192 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4193 "acceptedAt": "2026-09-11T23:53:28.481Z"
41944194 },
41954195 {
41964196 "type": "VERSION_CONFLICT",
……@@ -4204,8 +4204,8 @@
42044204 "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df"
42054205 }
42064206 ],
4207 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4208 "acceptedAt": "2026-09-11T23:48:07.627Z"
4207 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4208 "acceptedAt": "2026-09-11T23:53:28.481Z"
42094209 },
42104210 {
42114211 "type": "VERSION_CONFLICT",
……@@ -4219,8 +4219,8 @@
42194219 "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad"
42204220 }
42214221 ],
4222 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4223 "acceptedAt": "2026-09-11T23:48:07.627Z"
4222 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4223 "acceptedAt": "2026-09-11T23:53:28.481Z"
42244224 },
42254225 {
42264226 "type": "VERSION_CONFLICT",
……@@ -4234,8 +4234,8 @@
42344234 "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e"
42354235 }
42364236 ],
4237 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4238 "acceptedAt": "2026-09-11T23:48:07.627Z"
4237 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4238 "acceptedAt": "2026-09-11T23:53:28.481Z"
42394239 },
42404240 {
42414241 "type": "VERSION_CONFLICT",
……@@ -4264,8 +4264,8 @@
42644264 "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c"
42654265 }
42664266 ],
4267 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4268 "acceptedAt": "2026-09-11T23:48:07.627Z"
4267 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4268 "acceptedAt": "2026-09-11T23:53:28.481Z"
42694269 },
42704270 {
42714271 "type": "VERSION_CONFLICT",
……@@ -4279,8 +4279,8 @@
42794279 "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9"
42804280 }
42814281 ],
4282 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4283 "acceptedAt": "2026-09-11T23:48:07.627Z"
4282 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4283 "acceptedAt": "2026-09-11T23:53:28.481Z"
42844284 },
42854285 {
42864286 "type": "VERSION_CONFLICT",
……@@ -4294,8 +4294,8 @@
42944294 "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169"
42954295 }
42964296 ],
4297 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4298 "acceptedAt": "2026-09-11T23:48:07.627Z"
4297 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4298 "acceptedAt": "2026-09-11T23:53:28.481Z"
42994299 },
43004300 {
43014301 "type": "VERSION_CONFLICT",
……@@ -4309,8 +4309,8 @@
43094309 "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b"
43104310 }
43114311 ],
4312 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4313 "acceptedAt": "2026-09-11T23:48:07.627Z"
4312 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4313 "acceptedAt": "2026-09-11T23:53:28.481Z"
43144314 },
43154315 {
43164316 "type": "VERSION_CONFLICT",
……@@ -4324,8 +4324,8 @@
43244324 "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed"
43254325 }
43264326 ],
4327 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4328 "acceptedAt": "2026-09-11T23:48:07.627Z"
4327 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4328 "acceptedAt": "2026-09-11T23:53:28.481Z"
43294329 },
43304330 {
43314331 "type": "VERSION_CONFLICT",
……@@ -4339,8 +4339,8 @@
43394339 "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f"
43404340 }
43414341 ],
4342 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4343 "acceptedAt": "2026-09-11T23:48:07.627Z"
4342 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4343 "acceptedAt": "2026-09-11T23:53:28.481Z"
43444344 },
43454345 {
43464346 "type": "VERSION_CONFLICT",
……@@ -4354,8 +4354,8 @@
43544354 "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754"
43554355 }
43564356 ],
4357 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4358 "acceptedAt": "2026-09-11T23:48:07.627Z"
4357 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4358 "acceptedAt": "2026-09-11T23:53:28.481Z"
43594359 },
43604360 {
43614361 "type": "VERSION_CONFLICT",
……@@ -4369,8 +4369,8 @@
43694369 "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5"
43704370 }
43714371 ],
4372 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4373 "acceptedAt": "2026-09-11T23:48:07.627Z"
4372 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4373 "acceptedAt": "2026-09-11T23:53:28.481Z"
43744374 },
43754375 {
43764376 "type": "VERSION_CONFLICT",
……@@ -4389,8 +4389,8 @@
43894389 "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393"
43904390 }
43914391 ],
4392 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4393 "acceptedAt": "2026-09-11T23:48:07.627Z"
4392 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4393 "acceptedAt": "2026-09-11T23:53:28.481Z"
43944394 },
43954395 {
43964396 "type": "VERSION_CONFLICT",
……@@ -4404,8 +4404,8 @@
44044404 "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5"
44054405 }
44064406 ],
4407 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4408 "acceptedAt": "2026-09-11T23:48:07.627Z"
4407 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4408 "acceptedAt": "2026-09-11T23:53:28.481Z"
44094409 },
44104410 {
44114411 "type": "VERSION_CONFLICT",
……@@ -4419,8 +4419,8 @@
44194419 "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6"
44204420 }
44214421 ],
4422 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4423 "acceptedAt": "2026-09-11T23:48:07.627Z"
4422 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4423 "acceptedAt": "2026-09-11T23:53:28.481Z"
44244424 },
44254425 {
44264426 "type": "VERSION_CONFLICT",
……@@ -4434,8 +4434,8 @@
44344434 "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9"
44354435 }
44364436 ],
4437 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4438 "acceptedAt": "2026-09-11T23:48:07.627Z"
4437 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4438 "acceptedAt": "2026-09-11T23:53:28.481Z"
44394439 },
44404440 {
44414441 "type": "VERSION_CONFLICT",
……@@ -4449,8 +4449,8 @@
44494449 "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0"
44504450 }
44514451 ],
4452 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4453 "acceptedAt": "2026-09-11T23:48:07.627Z"
4452 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4453 "acceptedAt": "2026-09-11T23:53:28.481Z"
44544454 },
44554455 {
44564456 "type": "VERSION_CONFLICT",
……@@ -4464,8 +4464,8 @@
44644464 "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53"
44654465 }
44664466 ],
4467 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4468 "acceptedAt": "2026-09-11T23:48:07.627Z"
4467 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4468 "acceptedAt": "2026-09-11T23:53:28.481Z"
44694469 },
44704470 {
44714471 "type": "VERSION_CONFLICT",
……@@ -4479,8 +4479,8 @@
44794479 "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a"
44804480 }
44814481 ],
4482 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4483 "acceptedAt": "2026-09-11T23:48:07.627Z"
4482 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4483 "acceptedAt": "2026-09-11T23:53:28.481Z"
44844484 },
44854485 {
44864486 "type": "VERSION_CONFLICT",
……@@ -4499,8 +4499,8 @@
44994499 "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648"
45004500 }
45014501 ],
4502 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4503 "acceptedAt": "2026-09-11T23:48:07.627Z"
4502 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4503 "acceptedAt": "2026-09-11T23:53:28.481Z"
45044504 },
45054505 {
45064506 "type": "VERSION_CONFLICT",
……@@ -4514,8 +4514,8 @@
45144514 "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57"
45154515 }
45164516 ],
4517 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4518 "acceptedAt": "2026-09-11T23:48:07.627Z"
4517 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4518 "acceptedAt": "2026-09-11T23:53:28.481Z"
45194519 },
45204520 {
45214521 "type": "VERSION_CONFLICT",
……@@ -4529,8 +4529,8 @@
45294529 "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78"
45304530 }
45314531 ],
4532 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4533 "acceptedAt": "2026-09-11T23:48:07.627Z"
4532 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4533 "acceptedAt": "2026-09-11T23:53:28.481Z"
45344534 },
45354535 {
45364536 "type": "VERSION_CONFLICT",
……@@ -4544,8 +4544,8 @@
45444544 "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9"
45454545 }
45464546 ],
4547 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4548 "acceptedAt": "2026-09-11T23:48:07.627Z"
4547 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4548 "acceptedAt": "2026-09-11T23:53:28.481Z"
45494549 },
45504550 {
45514551 "type": "VERSION_CONFLICT",
……@@ -4559,8 +4559,8 @@
45594559 "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972"
45604560 }
45614561 ],
4562 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4563 "acceptedAt": "2026-09-11T23:48:07.627Z"
4562 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4563 "acceptedAt": "2026-09-11T23:53:28.481Z"
45644564 },
45654565 {
45664566 "type": "VERSION_CONFLICT",
……@@ -4574,8 +4574,8 @@
45744574 "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519"
45754575 }
45764576 ],
4577 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4578 "acceptedAt": "2026-09-11T23:48:07.627Z"
4577 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4578 "acceptedAt": "2026-09-11T23:53:28.481Z"
45794579 },
45804580 {
45814581 "type": "VERSION_CONFLICT",
……@@ -4589,8 +4589,8 @@
45894589 "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390"
45904590 }
45914591 ],
4592 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4593 "acceptedAt": "2026-09-11T23:48:07.627Z"
4592 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4593 "acceptedAt": "2026-09-11T23:53:28.481Z"
45944594 },
45954595 {
45964596 "type": "VERSION_CONFLICT",
……@@ -4604,8 +4604,8 @@
46044604 "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706"
46054605 }
46064606 ],
4607 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4608 "acceptedAt": "2026-09-11T23:48:07.627Z"
4607 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4608 "acceptedAt": "2026-09-11T23:53:28.481Z"
46094609 },
46104610 {
46114611 "type": "VERSION_CONFLICT",
……@@ -4619,8 +4619,8 @@
46194619 "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb"
46204620 }
46214621 ],
4622 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4623 "acceptedAt": "2026-09-11T23:48:07.627Z"
4622 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4623 "acceptedAt": "2026-09-11T23:53:28.481Z"
46244624 },
46254625 {
46264626 "type": "VERSION_CONFLICT",
……@@ -4634,8 +4634,8 @@
46344634 "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae"
46354635 }
46364636 ],
4637 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4638 "acceptedAt": "2026-09-11T23:48:07.627Z"
4637 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4638 "acceptedAt": "2026-09-11T23:53:28.481Z"
46394639 },
46404640 {
46414641 "type": "VERSION_CONFLICT",
……@@ -4654,8 +4654,8 @@
46544654 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765"
46554655 }
46564656 ],
4657 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4658 "acceptedAt": "2026-09-11T23:48:07.627Z"
4657 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4658 "acceptedAt": "2026-09-11T23:53:28.481Z"
46594659 },
46604660 {
46614661 "type": "VERSION_CONFLICT",
……@@ -4669,8 +4669,8 @@
46694669 "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60"
46704670 }
46714671 ],
4672 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4673 "acceptedAt": "2026-09-11T23:48:07.627Z"
4672 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4673 "acceptedAt": "2026-09-11T23:53:28.481Z"
46744674 },
46754675 {
46764676 "type": "VERSION_CONFLICT",
……@@ -4684,8 +4684,8 @@
46844684 "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a"
46854685 }
46864686 ],
4687 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4688 "acceptedAt": "2026-09-11T23:48:07.627Z"
4687 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4688 "acceptedAt": "2026-09-11T23:53:28.481Z"
46894689 },
46904690 {
46914691 "type": "VERSION_CONFLICT",
……@@ -4699,8 +4699,8 @@
46994699 "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b"
47004700 }
47014701 ],
4702 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4703 "acceptedAt": "2026-09-11T23:48:07.627Z"
4702 "reason": "ap-bridge 0.9.95 — LAN-reachable authenticated operator surface",
4703 "acceptedAt": "2026-09-11T23:53:28.481Z"
47044704 }
47054705 ]
47064706}
lib/admin-auth.js +97 -6
……@@ -82,8 +82,15 @@ export function loginPage ({ unclaimed = false, message = null } = {}) {
8282 'in the service log when it started.</p>' +
8383 '<p><label>Bootstrap token<br><input id="bootstrap" type="password" autocomplete="off" ' +
8484 'placeholder="32 hex characters"></label></p>'
85 : '<p>This browser signs in with the key it generated the first time you used it. If this is a new ' +
86 'browser it needs an invite from one that is already signed in.</p>'
85 // ⛔ AN INVITE, NOT A SECOND BOOTSTRAP. The bootstrap token gates the FIRST claim and is spent the
86 // moment it succeeds; browsers.js closes that door permanently and says "revoking the claimant does
87 // NOT reopen it either". Re-arming it would reopen exactly the hole it exists to close. A second
88 // admin arrives the way every admin after the first is meant to: an invite issued by one already
89 // signed in, single-use, and expiring.
90 : '<p>This browser signs in with the key it generated the first time you used it.</p>' +
91 '<p><label>Invite <span class="who">— only if this browser has never signed in here</span><br>' +
92 '<input id="invite" type="password" autocomplete="off" placeholder="leave empty to just sign in">' +
93 '</label></p>'
8794
8895 return `<!doctype html><meta charset="utf-8"><title>bridge · sign in</title>
8996<style>body{font:14px/1.5 system-ui,sans-serif;margin:0;padding:40px;max-width:520px;color:#111}
……@@ -120,9 +127,12 @@ document.getElementById('go').onclick = async () => {
120127 // ⚠ The raw challenge — this client hashes it internally and emits the canonical wire format.
121128 // Hashing it here first would sign the wrong thing, verify, and then fail the hash check.
122129 const signature = await an.sign(challenge, kp)
123 const el = document.getElementById('bootstrap')
130 const boot = document.getElementById('bootstrap')
131 const inv = document.getElementById('invite')
124132 const out = await post('/admin/session', {
125 publicKey: pk, signature, challenge, bootstrap: el ? el.value : ''
133 publicKey: pk, signature, challenge,
134 bootstrap: boot ? boot.value : '',
135 invite: inv ? inv.value : ''
126136 })
127137 if (out.ok) { location.href = '/admin'; return }
128138 msg.textContent = await out.text()
……@@ -133,13 +143,67 @@ document.getElementById('go').onclick = async () => {
133143</script>`
134144}
135145
146
147/**
148 * Who can operate this bridge — and how another browser is let in.
149 *
150 * ⛔ AN INVITE, NOT A SECOND BOOTSTRAP TOKEN. The bootstrap token exists to bind the FIRST claim to
151 * whoever can read the service log, and browsers.js closes that door permanently: "there is no window
152 * that reopens, and revoking the claimant does NOT reopen it either." Minting more of them would mean
153 * re-arming TOFU on a surface that is now reachable from the network, which is the precise hole the
154 * token was introduced to close. An invite does the thing that was actually wanted — let another
155 * browser in — and does it as a single-use grant that expires, issued by an operator who is already
156 * signed in and therefore already authorised.
157 */
158export function profilePage ({ me, browsers: list = [], invite = null, host = '' }) {
159 const rows = list.length
160 ? '<table><tr><th>key</th><th></th><th></th></tr>' + list.map((b) => {
161 const isMe = b.publicKey === me
162 return '<tr><td><code>' + esc(String(b.publicKey).slice(0, 24)) + '…</code>' +
163 (isMe ? ' <strong>(this browser)</strong>' : '') + '</td>' +
164 '<td>' + (b.revoked ? '<span class="warn">revoked</span>' : 'active') + '</td>' +
165 // ⛔ A revoked identity stays LISTED. It is still registered — that is why revoking the
166 // claimant does not reopen the instance — and hiding it would read as "removed", which is a
167 // different and untrue thing.
168 '<td>' + (b.revoked || isMe
169 ? '<span class="who">' + (isMe ? 'you cannot revoke the browser you are using' : '—') + '</span>'
170 : '<form method="post" action="/admin/revoke-browser"><input type="hidden" name="publicKey" value="' +
171 esc(b.publicKey) + '"><button>revoke</button></form>') + '</td></tr>'
172 }).join('') + '</table>'
173 : '<p class="none">No operators registered.</p>'
174
175 const issued = invite
176 ? '<p>Give this to ONE browser. It is single-use and it expires.</p><p><code>' + esc(invite) + '</code></p>' +
177 '<p class="who">They open this page, paste it into <strong>Invite</strong>, and sign in.</p>'
178 : '<form method="post" action="/admin/invite-browser"><button>invite another browser</button></form>' +
179 '<p class="who">This is how every operator after the first one gets in. The bootstrap token is ' +
180 'not reissued — it gates the first claim only, and re-arming it would reopen the instance to ' +
181 'whoever reached it first.</p>'
182
183 return `<!doctype html><meta charset="utf-8"><title>operators · ${esc(host)}</title>
184<style>body{font:14px/1.5 system-ui,sans-serif;margin:0;padding:24px;max-width:900px;color:#111}
185h1{font-size:20px}h2{font-size:15px;margin-top:28px;border-bottom:1px solid #ddd;padding-bottom:4px}
186table{border-collapse:collapse;width:100%;margin:8px 0}td,th{text-align:left;padding:5px 8px;border-bottom:1px solid #eee}
187th{font-weight:600;color:#555;font-size:12px;text-transform:uppercase}code{font:12px ui-monospace,monospace;background:#f5f5f5;padding:1px 4px}
188.who{color:#666}.warn{color:#a00}.none{color:#999;font-style:italic}button{font:inherit;padding:4px 10px}form{display:inline}</style>
189<h1>Operators</h1>
190<p class="who"><a href="/admin">← the bridge</a></p>
191<h2>Browsers that may operate this bridge</h2>${rows}
192<h2>Let another browser in</h2>${issued}
193<h2>This browser</h2><p><code>${esc(me || '—')}</code></p>
194<p class="who">The key lives in this browser and nowhere else. It is not a device key and never enters
195the attestation chain — it authenticates, and the daemon writes.</p>
196<form method="post" action="/admin/logout"><button>sign out</button></form>`
197}
198
136199const text = (status, body) => ({ status, headers: { 'content-type': 'text/plain; charset=utf-8' }, body })
137200const html = (status, body) => ({ status, headers: { 'content-type': 'text/html; charset=utf-8' }, body })
138201
139202/**
140203 * The sign-in routes. Returns null when the path is not one of ours, so the caller can route on.
141204 */
142export async function authRoutes (req, { browsers, sessions }) {
205export async function authRoutes (req, ctx) {
206 const { browsers, sessions } = ctx
143207 const path = req.path || '/'
144208
145209 if (req.method === 'GET' && path === '/admin/login') {
……@@ -158,6 +222,9 @@ export async function authRoutes (req, { browsers, sessions }) {
158222 // ⛔ CLAIM OR AUTHENTICATE — never both, and the instance decides which. Offering a "claim"
159223 // button after the first one would be a second door onto an instance that is already owned.
160224 if (await browsers.unclaimed()) await browsers.claim({ ...proof, bootstrap: f.bootstrap || null })
225 // ⛔ REDEEM ONLY WHEN ONE IS OFFERED. An invite is single-use and consumed on redemption, so
226 // spending one on a browser that could simply authenticate would burn it for nothing.
227 else if (f.invite) await browsers.redeem({ ...proof, token: f.invite })
161228 else await browsers.authenticate(proof)
162229 } catch (err) {
163230 // ⚠ The daemon's own words: it knows whether the token was wrong, the key unknown, the
……@@ -174,6 +241,30 @@ export async function authRoutes (req, { browsers, sessions }) {
174241 }
175242 }
176243
244 if (req.method === 'GET' && path === '/admin/profile') {
245 const me = await currentAdmin(req, { sessions, browsers })
246 if (!me) return { status: 303, headers: { location: '/admin/login' }, body: '' }
247 return html(200, profilePage({ me, browsers: await browsers.list(), host: (ctx && ctx.config && ctx.config.host) || '' }))
248 }
249
250 if (req.method === 'POST' && path === '/admin/invite-browser') {
251 const me = await currentAdmin(req, { sessions, browsers })
252 if (!me) return { status: 303, headers: { location: '/admin/login' }, body: '' }
253 const { token } = await browsers.invite()
254 return html(200, profilePage({ me, browsers: await browsers.list(), invite: token, host: (ctx && ctx.config && ctx.config.host) || '' }))
255 }
256
257 if (req.method === 'POST' && path === '/admin/revoke-browser') {
258 const me = await currentAdmin(req, { sessions, browsers })
259 if (!me) return { status: 303, headers: { location: '/admin/login' }, body: '' }
260 const target = String((req.form && req.form.publicKey) || '')
261 // ⛔ NOT YOURSELF. Revoking the browser you are holding locks the bridge to whatever other
262 // operators exist — and if there are none, to nobody at all, with no way back in because the
263 // bootstrap token gates only the first claim and this instance is already claimed.
264 if (target && target !== me) await browsers.revoke(target)
265 return { status: 303, headers: { location: '/admin/profile' }, body: '' }
266 }
267
177268 if (req.method === 'POST' && path === '/admin/logout') {
178269 sessions.destroy(parseCookies(req.headers && req.headers.cookie)[COOKIE])
179270 return { status: 303, headers: { location: '/admin/login', 'set-cookie': COOKIE + '=; Max-Age=0; Path=/' }, body: '' }
……@@ -182,4 +273,4 @@ export async function authRoutes (req, { browsers, sessions }) {
182273 return null
183274}
184275
185export default { Sessions, authRoutes, currentAdmin, loginPage, parseCookies, COOKIE }
276export default { Sessions, authRoutes, currentAdmin, loginPage, profilePage, parseCookies, COOKIE }
lib/admin.js +16 -2
……@@ -38,7 +38,7 @@ const when = (ms) => {
3838}
3939
4040/** The registered names, the burned ones, and what the fediverse has sent us. */
41export function renderOverview ({ host, registrations = [], burned = [], inbound = [], cap = null, policy = null }) {
41export function renderOverview ({ host, registrations = [], burned = [], inbound = [], cap = null, policy = null, primary = null, primaryDid = null }) {
4242 const names = registrations.length
4343 ? '<table><tr><th>name</th><th>identity root</th><th>feed</th><th>gen</th><th>registered</th><th></th></tr>' +
4444 registrations.map((r) =>
……@@ -77,8 +77,20 @@ export function renderOverview ({ host, registrations = [], burned = [], inbound
7777 '<span class="warn">The fediverse has no backlink index, so an eviction here cannot be backfilled.</span></p>'
7878 : ''
7979
80 // ⛔ THE BRIDGE'S OWN ACTOR IS NOT A REGISTERED NAME, and leaving it off made the operator's MAIN
81 // actor invisible on the operator's page. It comes from config (AP_USERNAME), not from
82 // registrations.json, so a page rendering only the registry shows every name except the one the
83 // bridge was originally stood up to serve. Asked for by name after exactly that confusion.
84 const own = primary
85 ? '<h2>This bridge\'s own actor</h2><table><tr><th>handle</th><th>did</th><th>where it comes from</th></tr>' +
86 '<tr><td><code>' + esc(primary) + '@' + esc(host) + '</code></td>' +
87 '<td><code>' + esc(primaryDid || '—') + '</code></td>' +
88 '<td class="who">configuration, not the name registry — it cannot be released here</td></tr></table>'
89 : ''
90
8091 return page('bridge · ' + host,
81 '<p class="who">Registration policy: <strong>' + esc(policy || 'off') + '</strong></p>' +
92 '<p class="who">Registration policy: <strong>' + esc(policy || 'off') + '</strong> · ' +
93 '<a href="/admin/profile">operators</a></p>' + own +
8294 '<h2>Registered names</h2>' + names +
8395 '<h2>Invites</h2><form method="post" action="/admin/invite"><button>issue an invite</button></form>' +
8496 '<p class="who">Only meaningful on an invite-only bridge. An invite is single-use and is consumed ' +
……@@ -110,6 +122,8 @@ export async function adminRoutes (req, { registrations, inbound, config }) {
110122 host: config.host,
111123 policy: config.registration || null,
112124 cap: config.inboundCap,
125 primary: config.username || null,
126 primaryDid: config.did || null,
113127 registrations: await registrations.list(),
114128 burned: typeof registrations.listBurned === 'function' ? await registrations.listBurned() : [],
115129 inbound: inbound ? await inbound.list({ limit: 50 }) : []
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "ap-bridge",
3 "version": "0.9.95",
3 "version": "0.9.96",
44 "description": "ActivityPub membrane — a second projection of the self-custody atproto peer",
55 "type": "module",
66 "main": "index.js",
test/admin-auth.test.js +41 -0
……@@ -121,3 +121,44 @@ test('★★ sign-in is reachable without a session, and says what it needs', as
121121 t.ok(/bootstrap token/i.test(res.body), 'an unclaimed bridge asks for the token')
122122 t.ok(/anproto-client\.js/.test(res.body), 'and loads the same identity client the other apps use')
123123})
124
125test('★★★ the operator page lists operators, and you cannot revoke yourself', async (t) => {
126 const c = await ctx()
127 const who = await browser()
128 const login = await serveAdmin(req('POST', '/admin/session', { form: { ...(await who.proof(c)), bootstrap: TOKEN } }), c)
129 const cookie = { cookie: COOKIE + '=' + login.headers['set-cookie'].split(';')[0].split('=')[1] }
130
131 const page = await serveAdmin(req('GET', '/admin/profile', { headers: cookie }), c)
132 t.is(page.status, 200)
133 t.ok(/this browser/.test(page.body), 'it says which key you are holding')
134
135 // ⛔ Revoking the browser you are using locks the bridge to whatever other operators exist — and
136 // with none, to nobody, because the bootstrap token gates only the FIRST claim and this instance is
137 // already claimed. There would be no way back in.
138 await serveAdmin(req('POST', '/admin/revoke-browser', { headers: cookie, form: { publicKey: who.publicKey } }), c)
139 const after = await serveAdmin(req('GET', '/admin', { headers: cookie }), c)
140 t.is(after.status, 200, 'still signed in — the self-revoke was refused')
141})
142
143test('★★★ an invite is how the second operator gets in, not a second bootstrap token', async (t) => {
144 const c = await ctx()
145 const first = await browser()
146 const login = await serveAdmin(req('POST', '/admin/session', { form: { ...(await first.proof(c)), bootstrap: TOKEN } }), c)
147 const cookie = { cookie: COOKIE + '=' + login.headers['set-cookie'].split(';')[0].split('=')[1] }
148
149 const issued = await serveAdmin(req('POST', '/admin/invite-browser', { headers: cookie }), c)
150 t.is(issued.status, 200)
151 const token = (issued.body.match(/<code>([0-9a-f]{16,})<\/code>/) || [])[1]
152 t.ok(token, 'an invite token is shown once')
153
154 // ⛔ THE BOOTSTRAP TOKEN IS SPENT. It gates the first claim only; re-arming it would reopen the
155 // instance to whoever reached it first, which is the hole it was introduced to close.
156 const second = await browser()
157 const refused = await serveAdmin(req('POST', '/admin/session', { form: { ...(await second.proof(c)), bootstrap: TOKEN } }), c)
158 t.is(refused.status, 403, 'the bootstrap token no longer admits anybody')
159
160 const third = await browser()
161 const ok = await serveAdmin(req('POST', '/admin/session', { form: { ...(await third.proof(c)), invite: token } }), c)
162 t.is(ok.status, 200, 'the invite does')
163 t.ok(ok.headers['set-cookie'], 'and hands out a session')
164})