OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge commits / 1b0783d

0.10.1: silent re-auth, so a deploy stops signing the operator out
⛔ MY SESSION MODEL WAS RIGHT AND HALF-IMPLEMENTED. Sessions are in-memory and die with the process — which is pgv3's design too, stated in lib/auth/sessions.js: "Sessions are NOT persisted across daemon restarts. The user re-authenticates after a restart, which is fine because the browser still has its keypair in localStorage and THE AUTH FLOW IS SILENT." I had the first half and not the second. Five deploys in an hour meant five sign-outs, and the page demanded a click each time, which reads as "I already logged in, why am I here again". The key in localStorage is the credential; the session is only a cache of having checked it. So the sign-in page now proves the key on load and redirects, showing the form only when it cannot: an unclaimed bridge needs the bootstrap token TYPED, and a browser whose key was minted a moment ago has nothing registered to prove — `hadKey` separates those, because failing silently on a first visit would look like a dead button. ⛔ A SILENT ATTEMPT NEVER REPLAYS THE BOOTSTRAP TOKEN OR AN INVITE. Both are single-use and an invite is consumed on redemption, so an automatic retry would burn one on a page load. The gate now carries `?next=` so an operator lands back on the page they were reading rather than at /admin — and it is refused unless it is a local path, because `next` comes off the wire and echoing an absolute URL into a redirect turns a link to this bridge into a link somewhere else. Both directions asserted. 1069/1069 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
1b0783d9370cb93a76284828068f187b136e4e1b
by plankatron <fleeky@gmail.com> · 2026-09-12T00:46:48.000Z

Changed files (5) +177 -116

.pgh-lock.json +93 -93
……@@ -1,9 +1,9 @@
11{
22 "name": "ap-bridge",
3 "version": "0.9.99",
3 "version": "0.10.0",
44 "runtime": "bare",
5 "rootTreeHash": "901431e785e78776a3291b2fdb3758e3273bc9b2567764c54b84ec126ce8c851",
6 "publishedAt": "2026-09-12T00:28:10.087Z",
5 "rootTreeHash": "ff8be560c36e2d327015ad0a6915bab3c821b12cc81a75f2eff14fb91ae6591b",
6 "publishedAt": "2026-09-12T00:37:22.222Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -4004,8 +4004,8 @@
40044004 "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37"
40054005 }
40064006 ],
4007 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4008 "acceptedAt": "2026-09-12T00:28:10.119Z"
4007 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4008 "acceptedAt": "2026-09-12T00:37:22.248Z"
40094009 },
40104010 {
40114011 "type": "VERSION_CONFLICT",
……@@ -4019,8 +4019,8 @@
40194019 "tree": "ee91b6bd77101e3404ca571b2e5adf0f47029d174398a05f50294fc207e39d99"
40204020 }
40214021 ],
4022 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4023 "acceptedAt": "2026-09-12T00:28:10.119Z"
4022 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4023 "acceptedAt": "2026-09-12T00:37:22.248Z"
40244024 },
40254025 {
40264026 "type": "VERSION_CONFLICT",
……@@ -4034,8 +4034,8 @@
40344034 "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11"
40354035 }
40364036 ],
4037 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4038 "acceptedAt": "2026-09-12T00:28:10.119Z"
4037 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4038 "acceptedAt": "2026-09-12T00:37:22.248Z"
40394039 },
40404040 {
40414041 "type": "VERSION_CONFLICT",
……@@ -4054,8 +4054,8 @@
40544054 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
40554055 }
40564056 ],
4057 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4058 "acceptedAt": "2026-09-12T00:28:10.119Z"
4057 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4058 "acceptedAt": "2026-09-12T00:37:22.248Z"
40594059 },
40604060 {
40614061 "type": "VERSION_CONFLICT",
……@@ -4069,8 +4069,8 @@
40694069 "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd"
40704070 }
40714071 ],
4072 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4073 "acceptedAt": "2026-09-12T00:28:10.119Z"
4072 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4073 "acceptedAt": "2026-09-12T00:37:22.248Z"
40744074 },
40754075 {
40764076 "type": "VERSION_CONFLICT",
……@@ -4084,8 +4084,8 @@
40844084 "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95"
40854085 }
40864086 ],
4087 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4088 "acceptedAt": "2026-09-12T00:28:10.119Z"
4087 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4088 "acceptedAt": "2026-09-12T00:37:22.248Z"
40894089 },
40904090 {
40914091 "type": "VERSION_CONFLICT",
……@@ -4099,8 +4099,8 @@
40994099 "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb"
41004100 }
41014101 ],
4102 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4103 "acceptedAt": "2026-09-12T00:28:10.119Z"
4102 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4103 "acceptedAt": "2026-09-12T00:37:22.248Z"
41044104 },
41054105 {
41064106 "type": "VERSION_CONFLICT",
……@@ -4114,8 +4114,8 @@
41144114 "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a"
41154115 }
41164116 ],
4117 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4118 "acceptedAt": "2026-09-12T00:28:10.119Z"
4117 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4118 "acceptedAt": "2026-09-12T00:37:22.248Z"
41194119 },
41204120 {
41214121 "type": "VERSION_CONFLICT",
……@@ -4129,8 +4129,8 @@
41294129 "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca"
41304130 }
41314131 ],
4132 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4133 "acceptedAt": "2026-09-12T00:28:10.119Z"
4132 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4133 "acceptedAt": "2026-09-12T00:37:22.248Z"
41344134 },
41354135 {
41364136 "type": "VERSION_CONFLICT",
……@@ -4144,8 +4144,8 @@
41444144 "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701"
41454145 }
41464146 ],
4147 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4148 "acceptedAt": "2026-09-12T00:28:10.119Z"
4147 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4148 "acceptedAt": "2026-09-12T00:37:22.248Z"
41494149 },
41504150 {
41514151 "type": "VERSION_CONFLICT",
……@@ -4159,8 +4159,8 @@
41594159 "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d"
41604160 }
41614161 ],
4162 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4163 "acceptedAt": "2026-09-12T00:28:10.119Z"
4162 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4163 "acceptedAt": "2026-09-12T00:37:22.248Z"
41644164 },
41654165 {
41664166 "type": "VERSION_CONFLICT",
……@@ -4174,8 +4174,8 @@
41744174 "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04"
41754175 }
41764176 ],
4177 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4178 "acceptedAt": "2026-09-12T00:28:10.119Z"
4177 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4178 "acceptedAt": "2026-09-12T00:37:22.248Z"
41794179 },
41804180 {
41814181 "type": "VERSION_CONFLICT",
……@@ -4189,8 +4189,8 @@
41894189 "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4"
41904190 }
41914191 ],
4192 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4193 "acceptedAt": "2026-09-12T00:28:10.119Z"
4192 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4193 "acceptedAt": "2026-09-12T00:37:22.248Z"
41944194 },
41954195 {
41964196 "type": "VERSION_CONFLICT",
……@@ -4204,8 +4204,8 @@
42044204 "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df"
42054205 }
42064206 ],
4207 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4208 "acceptedAt": "2026-09-12T00:28:10.119Z"
4207 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4208 "acceptedAt": "2026-09-12T00:37:22.248Z"
42094209 },
42104210 {
42114211 "type": "VERSION_CONFLICT",
……@@ -4219,8 +4219,8 @@
42194219 "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad"
42204220 }
42214221 ],
4222 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4223 "acceptedAt": "2026-09-12T00:28:10.119Z"
4222 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4223 "acceptedAt": "2026-09-12T00:37:22.248Z"
42244224 },
42254225 {
42264226 "type": "VERSION_CONFLICT",
……@@ -4234,8 +4234,8 @@
42344234 "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e"
42354235 }
42364236 ],
4237 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4238 "acceptedAt": "2026-09-12T00:28:10.119Z"
4237 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4238 "acceptedAt": "2026-09-12T00:37:22.248Z"
42394239 },
42404240 {
42414241 "type": "VERSION_CONFLICT",
……@@ -4264,8 +4264,8 @@
42644264 "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c"
42654265 }
42664266 ],
4267 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4268 "acceptedAt": "2026-09-12T00:28:10.119Z"
4267 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4268 "acceptedAt": "2026-09-12T00:37:22.248Z"
42694269 },
42704270 {
42714271 "type": "VERSION_CONFLICT",
……@@ -4279,8 +4279,8 @@
42794279 "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9"
42804280 }
42814281 ],
4282 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4283 "acceptedAt": "2026-09-12T00:28:10.119Z"
4282 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4283 "acceptedAt": "2026-09-12T00:37:22.248Z"
42844284 },
42854285 {
42864286 "type": "VERSION_CONFLICT",
……@@ -4294,8 +4294,8 @@
42944294 "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169"
42954295 }
42964296 ],
4297 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4298 "acceptedAt": "2026-09-12T00:28:10.119Z"
4297 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4298 "acceptedAt": "2026-09-12T00:37:22.248Z"
42994299 },
43004300 {
43014301 "type": "VERSION_CONFLICT",
……@@ -4309,8 +4309,8 @@
43094309 "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b"
43104310 }
43114311 ],
4312 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4313 "acceptedAt": "2026-09-12T00:28:10.119Z"
4312 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4313 "acceptedAt": "2026-09-12T00:37:22.248Z"
43144314 },
43154315 {
43164316 "type": "VERSION_CONFLICT",
……@@ -4324,8 +4324,8 @@
43244324 "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed"
43254325 }
43264326 ],
4327 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4328 "acceptedAt": "2026-09-12T00:28:10.119Z"
4327 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4328 "acceptedAt": "2026-09-12T00:37:22.248Z"
43294329 },
43304330 {
43314331 "type": "VERSION_CONFLICT",
……@@ -4339,8 +4339,8 @@
43394339 "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f"
43404340 }
43414341 ],
4342 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4343 "acceptedAt": "2026-09-12T00:28:10.119Z"
4342 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4343 "acceptedAt": "2026-09-12T00:37:22.248Z"
43444344 },
43454345 {
43464346 "type": "VERSION_CONFLICT",
……@@ -4354,8 +4354,8 @@
43544354 "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754"
43554355 }
43564356 ],
4357 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4358 "acceptedAt": "2026-09-12T00:28:10.119Z"
4357 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4358 "acceptedAt": "2026-09-12T00:37:22.248Z"
43594359 },
43604360 {
43614361 "type": "VERSION_CONFLICT",
……@@ -4369,8 +4369,8 @@
43694369 "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5"
43704370 }
43714371 ],
4372 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4373 "acceptedAt": "2026-09-12T00:28:10.119Z"
4372 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4373 "acceptedAt": "2026-09-12T00:37:22.248Z"
43744374 },
43754375 {
43764376 "type": "VERSION_CONFLICT",
……@@ -4389,8 +4389,8 @@
43894389 "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393"
43904390 }
43914391 ],
4392 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4393 "acceptedAt": "2026-09-12T00:28:10.119Z"
4392 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4393 "acceptedAt": "2026-09-12T00:37:22.248Z"
43944394 },
43954395 {
43964396 "type": "VERSION_CONFLICT",
……@@ -4404,8 +4404,8 @@
44044404 "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5"
44054405 }
44064406 ],
4407 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4408 "acceptedAt": "2026-09-12T00:28:10.119Z"
4407 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4408 "acceptedAt": "2026-09-12T00:37:22.248Z"
44094409 },
44104410 {
44114411 "type": "VERSION_CONFLICT",
……@@ -4419,8 +4419,8 @@
44194419 "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6"
44204420 }
44214421 ],
4422 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4423 "acceptedAt": "2026-09-12T00:28:10.119Z"
4422 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4423 "acceptedAt": "2026-09-12T00:37:22.248Z"
44244424 },
44254425 {
44264426 "type": "VERSION_CONFLICT",
……@@ -4434,8 +4434,8 @@
44344434 "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9"
44354435 }
44364436 ],
4437 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4438 "acceptedAt": "2026-09-12T00:28:10.119Z"
4437 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4438 "acceptedAt": "2026-09-12T00:37:22.248Z"
44394439 },
44404440 {
44414441 "type": "VERSION_CONFLICT",
……@@ -4449,8 +4449,8 @@
44494449 "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0"
44504450 }
44514451 ],
4452 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4453 "acceptedAt": "2026-09-12T00:28:10.119Z"
4452 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4453 "acceptedAt": "2026-09-12T00:37:22.248Z"
44544454 },
44554455 {
44564456 "type": "VERSION_CONFLICT",
……@@ -4464,8 +4464,8 @@
44644464 "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53"
44654465 }
44664466 ],
4467 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4468 "acceptedAt": "2026-09-12T00:28:10.119Z"
4467 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4468 "acceptedAt": "2026-09-12T00:37:22.248Z"
44694469 },
44704470 {
44714471 "type": "VERSION_CONFLICT",
……@@ -4479,8 +4479,8 @@
44794479 "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a"
44804480 }
44814481 ],
4482 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4483 "acceptedAt": "2026-09-12T00:28:10.119Z"
4482 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4483 "acceptedAt": "2026-09-12T00:37:22.248Z"
44844484 },
44854485 {
44864486 "type": "VERSION_CONFLICT",
……@@ -4499,8 +4499,8 @@
44994499 "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648"
45004500 }
45014501 ],
4502 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4503 "acceptedAt": "2026-09-12T00:28:10.119Z"
4502 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4503 "acceptedAt": "2026-09-12T00:37:22.248Z"
45044504 },
45054505 {
45064506 "type": "VERSION_CONFLICT",
……@@ -4514,8 +4514,8 @@
45144514 "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57"
45154515 }
45164516 ],
4517 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4518 "acceptedAt": "2026-09-12T00:28:10.119Z"
4517 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4518 "acceptedAt": "2026-09-12T00:37:22.248Z"
45194519 },
45204520 {
45214521 "type": "VERSION_CONFLICT",
……@@ -4529,8 +4529,8 @@
45294529 "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78"
45304530 }
45314531 ],
4532 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4533 "acceptedAt": "2026-09-12T00:28:10.119Z"
4532 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4533 "acceptedAt": "2026-09-12T00:37:22.248Z"
45344534 },
45354535 {
45364536 "type": "VERSION_CONFLICT",
……@@ -4544,8 +4544,8 @@
45444544 "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9"
45454545 }
45464546 ],
4547 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4548 "acceptedAt": "2026-09-12T00:28:10.119Z"
4547 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4548 "acceptedAt": "2026-09-12T00:37:22.248Z"
45494549 },
45504550 {
45514551 "type": "VERSION_CONFLICT",
……@@ -4559,8 +4559,8 @@
45594559 "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972"
45604560 }
45614561 ],
4562 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4563 "acceptedAt": "2026-09-12T00:28:10.119Z"
4562 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4563 "acceptedAt": "2026-09-12T00:37:22.248Z"
45644564 },
45654565 {
45664566 "type": "VERSION_CONFLICT",
……@@ -4574,8 +4574,8 @@
45744574 "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519"
45754575 }
45764576 ],
4577 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4578 "acceptedAt": "2026-09-12T00:28:10.119Z"
4577 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4578 "acceptedAt": "2026-09-12T00:37:22.248Z"
45794579 },
45804580 {
45814581 "type": "VERSION_CONFLICT",
……@@ -4589,8 +4589,8 @@
45894589 "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390"
45904590 }
45914591 ],
4592 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4593 "acceptedAt": "2026-09-12T00:28:10.119Z"
4592 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4593 "acceptedAt": "2026-09-12T00:37:22.248Z"
45944594 },
45954595 {
45964596 "type": "VERSION_CONFLICT",
……@@ -4604,8 +4604,8 @@
46044604 "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706"
46054605 }
46064606 ],
4607 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4608 "acceptedAt": "2026-09-12T00:28:10.119Z"
4607 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4608 "acceptedAt": "2026-09-12T00:37:22.248Z"
46094609 },
46104610 {
46114611 "type": "VERSION_CONFLICT",
……@@ -4619,8 +4619,8 @@
46194619 "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb"
46204620 }
46214621 ],
4622 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4623 "acceptedAt": "2026-09-12T00:28:10.119Z"
4622 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4623 "acceptedAt": "2026-09-12T00:37:22.248Z"
46244624 },
46254625 {
46264626 "type": "VERSION_CONFLICT",
……@@ -4634,8 +4634,8 @@
46344634 "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae"
46354635 }
46364636 ],
4637 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4638 "acceptedAt": "2026-09-12T00:28:10.119Z"
4637 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4638 "acceptedAt": "2026-09-12T00:37:22.248Z"
46394639 },
46404640 {
46414641 "type": "VERSION_CONFLICT",
……@@ -4654,8 +4654,8 @@
46544654 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765"
46554655 }
46564656 ],
4657 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4658 "acceptedAt": "2026-09-12T00:28:10.119Z"
4657 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4658 "acceptedAt": "2026-09-12T00:37:22.248Z"
46594659 },
46604660 {
46614661 "type": "VERSION_CONFLICT",
……@@ -4669,8 +4669,8 @@
46694669 "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60"
46704670 }
46714671 ],
4672 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4673 "acceptedAt": "2026-09-12T00:28:10.119Z"
4672 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4673 "acceptedAt": "2026-09-12T00:37:22.248Z"
46744674 },
46754675 {
46764676 "type": "VERSION_CONFLICT",
……@@ -4684,8 +4684,8 @@
46844684 "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a"
46854685 }
46864686 ],
4687 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4688 "acceptedAt": "2026-09-12T00:28:10.119Z"
4687 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4688 "acceptedAt": "2026-09-12T00:37:22.248Z"
46894689 },
46904690 {
46914691 "type": "VERSION_CONFLICT",
……@@ -4699,8 +4699,8 @@
46994699 "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b"
47004700 }
47014701 ],
4702 "reason": "ap-bridge 0.9.99 — paged inbound timeline",
4703 "acceptedAt": "2026-09-12T00:28:10.119Z"
4702 "reason": "ap-bridge 0.10.0 — public inbound timeline",
4703 "acceptedAt": "2026-09-12T00:37:22.248Z"
47044704 }
47054705 ]
47064706}
lib/admin-auth.js +44 -20
……@@ -90,7 +90,7 @@ export async function currentAdmin (req, { sessions, browsers }) {
9090const esc = (s) => String(s == null ? '' : s)
9191 .replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;')
9292
93export function loginPage ({ unclaimed = false, message = null } = {}) {
93export function loginPage ({ unclaimed = false, message = null, next = '/admin' } = {}) {
9494 const warn = message ? '<p class="warn">' + esc(message) + '</p>' : ''
9595 const intro = unclaimed
9696 ? '<p>Nobody operates this bridge yet. Claim it with the <strong>bootstrap token</strong> printed ' +
……@@ -111,6 +111,7 @@ export function loginPage ({ unclaimed = false, message = null } = {}) {
111111<style>body{font:14px/1.5 system-ui,sans-serif;margin:0;padding:40px;max-width:520px;color:#111}
112112h1{font-size:20px}.warn{color:#a00}.who{color:#666}input{font:inherit;padding:5px;width:100%;box-sizing:border-box}
113113button{font:inherit;padding:6px 14px;margin-top:12px}code{font:12px ui-monospace,monospace;background:#f5f5f5;padding:1px 4px}</style>
114<body data-next="${esc(next)}">
114115<h1>Sign in to this bridge</h1>${warn}${intro}
115116<button id="go">sign in</button>
116117<p class="who" id="msg"></p>
……@@ -123,37 +124,56 @@ import { ANProtoClient as an } from '/static/anproto-client.js'
123124// daemon writes.
124125const KEY = 'ap-admin-keypair'
125126let kp = localStorage.getItem(KEY)
127// ⚠ Whether the key EXISTED matters: a key minted a moment ago has never been registered, so a silent
128// attempt with it can only fail, and failing silently on first visit would look like a dead button.
129const hadKey = !!kp
126130if (!kp) { kp = await an.gen(); localStorage.setItem(KEY, kp) }
127131const pk = an.getPublicKey(kp)
128132document.getElementById('pk').textContent = pk
129133
130134const msg = document.getElementById('msg')
135const NEXT = document.body.dataset.next || '/admin'
131136const post = (path, body) => fetch(path, {
132137 method: 'POST',
133138 headers: { 'content-type': 'application/x-www-form-urlencoded' },
134139 body: new URLSearchParams(body).toString()
135140})
136141
142async function signIn ({ silent } = {}) {
143 const r = await post('/admin/challenge', {})
144 const { challenge } = await r.json()
145 // ⚠ The raw challenge — this client hashes it internally and emits the canonical wire format.
146 // Hashing it here first would sign the wrong thing, verify, and then fail the hash check.
147 const signature = await an.sign(challenge, kp)
148 const boot = document.getElementById('bootstrap')
149 const inv = document.getElementById('invite')
150 const out = await post('/admin/session', {
151 publicKey: pk, signature, challenge,
152 // ⛔ Never sent on a silent attempt. An invite is single-use and consumed on redemption, so
153 // replaying one automatically would burn it on a page load.
154 bootstrap: silent ? '' : (boot ? boot.value : ''),
155 invite: silent ? '' : (inv ? inv.value : '')
156 })
157 if (out.ok) { location.href = NEXT; return true }
158 if (!silent) msg.textContent = await out.text()
159 return false
160}
161
137162document.getElementById('go').onclick = async () => {
138163 msg.textContent = 'signing…'
139 try {
140 const r = await post('/admin/challenge', {})
141 const { challenge } = await r.json()
142 // ⚠ The raw challenge — this client hashes it internally and emits the canonical wire format.
143 // Hashing it here first would sign the wrong thing, verify, and then fail the hash check.
144 const signature = await an.sign(challenge, kp)
145 const boot = document.getElementById('bootstrap')
146 const inv = document.getElementById('invite')
147 const out = await post('/admin/session', {
148 publicKey: pk, signature, challenge,
149 bootstrap: boot ? boot.value : '',
150 invite: inv ? inv.value : ''
151 })
152 if (out.ok) { location.href = '/admin'; return }
153 msg.textContent = await out.text()
154 } catch (e) {
155 msg.textContent = (e && e.message) || String(e)
156 }
164 try { await signIn({ silent: false }) } catch (e) { msg.textContent = (e && e.message) || String(e) }
165}
166
167// ⛔ SILENT RE-AUTH — the half that makes an in-memory session store usable. Sessions die with the
168// process, so every deploy signs everybody out; pgv3 carries exactly this design and says why it is
169// fine: "the browser still has its keypair in localStorage and the auth flow is silent". Without this
170// the page is technically correct and reads as "I already logged in, why am I here again".
171//
172// ⚠ Only when this browser has a key AND the bridge is already claimed. An unclaimed bridge needs the
173// bootstrap token typed in, and a brand-new browser has nothing to prove yet — both must show the form.
174if (!document.getElementById('bootstrap') && hadKey) {
175 msg.textContent = 'signing in…'
176 try { if (!await signIn({ silent: true })) msg.textContent = '' } catch { msg.textContent = '' }
157177}
158178</script>`
159179}
……@@ -222,7 +242,11 @@ export async function authRoutes (req, ctx) {
222242 const path = req.path || '/'
223243
224244 if (req.method === 'GET' && path === '/admin/login') {
225 return html(200, loginPage({ unclaimed: await browsers.unclaimed() }))
245 // ⛔ ONLY A LOCAL PATH. `next` comes off the wire, and echoing an absolute URL into a redirect is
246 // an open redirect — a link to this bridge that lands somebody somewhere else.
247 const wanted = String((req.query && req.query.next) || '/admin')
248 const next = wanted.startsWith('/') && !wanted.startsWith('//') ? wanted : '/admin'
249 return html(200, loginPage({ unclaimed: await browsers.unclaimed(), next }))
226250 }
227251
228252 if (req.method === 'POST' && path === '/admin/challenge') {
lib/admin-surface.js +9 -1
……@@ -49,7 +49,15 @@ export async function serveAdmin (req, ctx) {
4949 // ⛔ NO SESSION, NO SURFACE. A redirect rather than a 403 because the operator's next act is always
5050 // to sign in, and a bare 403 on a page they navigated to reads as "this is broken".
5151 const admin = await currentAdmin(req, ctx)
52 if (!admin) return { status: 303, headers: { location: '/admin/login' }, body: '' }
52 if (!admin) {
53 // ⚠ Carry where they were going. Sessions die with the process, so a deploy bounces an operator
54 // out of whatever page they were reading; sending them to /admin afterwards loses their place for
55 // no reason, and silent re-auth makes the whole round trip invisible when it works.
56 const back = encodeURIComponent(req.path + (req.query && Object.keys(req.query).length
57 ? '?' + new URLSearchParams(req.query).toString()
58 : ''))
59 return { status: 303, headers: { location: '/admin/login?next=' + back }, body: '' }
60 }
5361
5462 return adminRoutes(req, ctx)
5563}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "ap-bridge",
3 "version": "0.10.0",
3 "version": "0.10.1",
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 +30 -1
……@@ -53,7 +53,10 @@ test('★★★ every page redirects to sign-in without a session', async (t) =>
5353 for (const path of ['/admin', '/admin/', '/']) {
5454 const res = await serveAdmin(req('GET', path), c)
5555 t.is(res.status, 303, path + ' is gated')
56 t.is(res.headers.location, '/admin/login')
56 t.ok(res.headers.location.startsWith('/admin/login'), path + ' goes to sign-in')
57 // ⚠ And carries where they were going. Sessions die with the process, so a deploy bounces an
58 // operator out of the page they were reading; losing their place on top of that is gratuitous.
59 t.ok(res.headers.location.includes('next=' + encodeURIComponent(path)), 'with the destination kept')
5760 }
5861})
5962
……@@ -261,3 +264,29 @@ test('★★ inbound content is escaped — it was written by a stranger', async
261264 t.absent(/<img src=x/.test(res.body), 'no markup from a remote server reaches the operator page intact')
262265 t.ok(/hello/.test(res.body), 'but the text is still readable')
263266})
267
268test('★★★ the sign-in page re-authenticates silently when it already has a key', async (t) => {
269 // ⛔ THE HALF THAT MAKES AN IN-MEMORY SESSION STORE USABLE. Sessions die with the process, so every
270 // deploy signs everybody out. pgv3 carries the same design and says why it is fine: "the browser
271 // still has its keypair in localStorage and the auth flow is silent." Without the silent half the
272 // page is technically correct and reads as "I already logged in, why am I here again".
273 const c = await ctx()
274 const page = await serveAdmin(req('GET', '/admin/login'), c)
275 t.ok(/signIn\(\{ silent: true \}\)/.test(page.body), 'it attempts a silent sign-in')
276 t.ok(/hadKey/.test(page.body), 'only when this browser already had a key — a fresh one has nothing to prove')
277 t.ok(/silent \? '' :/.test(page.body),
278 'and never replays the bootstrap token or an invite, which are single-use')
279})
280
281test('★★★ the destination survives sign-in, but an open redirect does not', async (t) => {
282 const c = await ctx()
283 const ok = await serveAdmin(req('GET', '/admin/login', { query: { next: '/admin/timeline?offset=50' } }), c)
284 t.ok(/data-next="\/admin\/timeline\?offset=50"/.test(ok.body), 'a local path is carried through')
285
286 // ⛔ `next` comes off the wire. Echoing an absolute URL into a redirect turns a link to this bridge
287 // into a link somewhere else.
288 for (const evil of ['https://evil.example/x', '//evil.example/x']) {
289 const res = await serveAdmin(req('GET', '/admin/login', { query: { next: evil } }), c)
290 t.ok(/data-next="\/admin"/.test(res.body), evil + ' is refused')
291 }
292})