OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge commits / 632a9cb

0.10.7: the front page is the relay view, not the primary's own
/timeline was ONE peer's feed — the primary's — so a bridge serving three accounts showed one and linked to none of the others. The per-peer pages existed and were unreachable: you had to already know the URL to read @fleeky@ap.7rnx.net. Now /timeline is every peer this bridge serves, in one stream, and /ap/users/<name>/timeline stays each peer's own — including the primary's, which already resolved. ⛔ ONE READ OF THE STORE, FILTERED PER PEER INSIDE buildFeed. Each item carries the recipient it was delivered to, so it lands on exactly one peer's rows and cannot be counted twice. That is the property that lets a combined page exist without the isolation becoming decorative — asserted in both directions: the combined page shows both peers, and a peer's own page still shows only theirs. 1212/1212 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
632a9cb04f8cc38f2fe9ec32903043aab931e326
by plankatron <fleeky@gmail.com> · 2026-09-15T16:24:14.000Z

Changed files (4) +170 -127

.pgh-lock.json +93 -93
……@@ -1,9 +1,9 @@
11{
22 "name": "ap-bridge",
3 "version": "0.10.5",
3 "version": "0.10.6",
44 "runtime": "bare",
5 "rootTreeHash": "ea18883908ed2e5ebf378ca1f925478f00f22884b7f36f58d38e3ffd35767be4",
6 "publishedAt": "2026-09-15T16:11:51.191Z",
5 "rootTreeHash": "f29de4e2e53b8bd8a0eca9ed94aba91a52192f096d663d27e239e38ad7b79e6a",
6 "publishedAt": "2026-09-15T16:14:36.246Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -4004,8 +4004,8 @@
40044004 "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37"
40054005 }
40064006 ],
4007 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4008 "acceptedAt": "2026-09-15T16:11:51.209Z"
4007 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4008 "acceptedAt": "2026-09-15T16:14:36.263Z"
40094009 },
40104010 {
40114011 "type": "VERSION_CONFLICT",
……@@ -4019,8 +4019,8 @@
40194019 "tree": "ee91b6bd77101e3404ca571b2e5adf0f47029d174398a05f50294fc207e39d99"
40204020 }
40214021 ],
4022 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4023 "acceptedAt": "2026-09-15T16:11:51.209Z"
4022 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4023 "acceptedAt": "2026-09-15T16:14:36.263Z"
40244024 },
40254025 {
40264026 "type": "VERSION_CONFLICT",
……@@ -4034,8 +4034,8 @@
40344034 "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11"
40354035 }
40364036 ],
4037 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4038 "acceptedAt": "2026-09-15T16:11:51.209Z"
4037 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4038 "acceptedAt": "2026-09-15T16:14:36.263Z"
40394039 },
40404040 {
40414041 "type": "VERSION_CONFLICT",
……@@ -4054,8 +4054,8 @@
40544054 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
40554055 }
40564056 ],
4057 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4058 "acceptedAt": "2026-09-15T16:11:51.209Z"
4057 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4058 "acceptedAt": "2026-09-15T16:14:36.263Z"
40594059 },
40604060 {
40614061 "type": "VERSION_CONFLICT",
……@@ -4069,8 +4069,8 @@
40694069 "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd"
40704070 }
40714071 ],
4072 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4073 "acceptedAt": "2026-09-15T16:11:51.209Z"
4072 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4073 "acceptedAt": "2026-09-15T16:14:36.263Z"
40744074 },
40754075 {
40764076 "type": "VERSION_CONFLICT",
……@@ -4084,8 +4084,8 @@
40844084 "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95"
40854085 }
40864086 ],
4087 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4088 "acceptedAt": "2026-09-15T16:11:51.209Z"
4087 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4088 "acceptedAt": "2026-09-15T16:14:36.263Z"
40894089 },
40904090 {
40914091 "type": "VERSION_CONFLICT",
……@@ -4099,8 +4099,8 @@
40994099 "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb"
41004100 }
41014101 ],
4102 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4103 "acceptedAt": "2026-09-15T16:11:51.209Z"
4102 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4103 "acceptedAt": "2026-09-15T16:14:36.263Z"
41044104 },
41054105 {
41064106 "type": "VERSION_CONFLICT",
……@@ -4114,8 +4114,8 @@
41144114 "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a"
41154115 }
41164116 ],
4117 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4118 "acceptedAt": "2026-09-15T16:11:51.209Z"
4117 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4118 "acceptedAt": "2026-09-15T16:14:36.263Z"
41194119 },
41204120 {
41214121 "type": "VERSION_CONFLICT",
……@@ -4129,8 +4129,8 @@
41294129 "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca"
41304130 }
41314131 ],
4132 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4133 "acceptedAt": "2026-09-15T16:11:51.209Z"
4132 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4133 "acceptedAt": "2026-09-15T16:14:36.263Z"
41344134 },
41354135 {
41364136 "type": "VERSION_CONFLICT",
……@@ -4144,8 +4144,8 @@
41444144 "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701"
41454145 }
41464146 ],
4147 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4148 "acceptedAt": "2026-09-15T16:11:51.209Z"
4147 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4148 "acceptedAt": "2026-09-15T16:14:36.263Z"
41494149 },
41504150 {
41514151 "type": "VERSION_CONFLICT",
……@@ -4159,8 +4159,8 @@
41594159 "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d"
41604160 }
41614161 ],
4162 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4163 "acceptedAt": "2026-09-15T16:11:51.209Z"
4162 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4163 "acceptedAt": "2026-09-15T16:14:36.263Z"
41644164 },
41654165 {
41664166 "type": "VERSION_CONFLICT",
……@@ -4174,8 +4174,8 @@
41744174 "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04"
41754175 }
41764176 ],
4177 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4178 "acceptedAt": "2026-09-15T16:11:51.209Z"
4177 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4178 "acceptedAt": "2026-09-15T16:14:36.263Z"
41794179 },
41804180 {
41814181 "type": "VERSION_CONFLICT",
……@@ -4189,8 +4189,8 @@
41894189 "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4"
41904190 }
41914191 ],
4192 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4193 "acceptedAt": "2026-09-15T16:11:51.209Z"
4192 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4193 "acceptedAt": "2026-09-15T16:14:36.263Z"
41944194 },
41954195 {
41964196 "type": "VERSION_CONFLICT",
……@@ -4204,8 +4204,8 @@
42044204 "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df"
42054205 }
42064206 ],
4207 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4208 "acceptedAt": "2026-09-15T16:11:51.209Z"
4207 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4208 "acceptedAt": "2026-09-15T16:14:36.263Z"
42094209 },
42104210 {
42114211 "type": "VERSION_CONFLICT",
……@@ -4219,8 +4219,8 @@
42194219 "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad"
42204220 }
42214221 ],
4222 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4223 "acceptedAt": "2026-09-15T16:11:51.209Z"
4222 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4223 "acceptedAt": "2026-09-15T16:14:36.263Z"
42244224 },
42254225 {
42264226 "type": "VERSION_CONFLICT",
……@@ -4234,8 +4234,8 @@
42344234 "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e"
42354235 }
42364236 ],
4237 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4238 "acceptedAt": "2026-09-15T16:11:51.209Z"
4237 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4238 "acceptedAt": "2026-09-15T16:14:36.263Z"
42394239 },
42404240 {
42414241 "type": "VERSION_CONFLICT",
……@@ -4264,8 +4264,8 @@
42644264 "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c"
42654265 }
42664266 ],
4267 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4268 "acceptedAt": "2026-09-15T16:11:51.209Z"
4267 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4268 "acceptedAt": "2026-09-15T16:14:36.263Z"
42694269 },
42704270 {
42714271 "type": "VERSION_CONFLICT",
……@@ -4279,8 +4279,8 @@
42794279 "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9"
42804280 }
42814281 ],
4282 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4283 "acceptedAt": "2026-09-15T16:11:51.209Z"
4282 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4283 "acceptedAt": "2026-09-15T16:14:36.263Z"
42844284 },
42854285 {
42864286 "type": "VERSION_CONFLICT",
……@@ -4294,8 +4294,8 @@
42944294 "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169"
42954295 }
42964296 ],
4297 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4298 "acceptedAt": "2026-09-15T16:11:51.209Z"
4297 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4298 "acceptedAt": "2026-09-15T16:14:36.263Z"
42994299 },
43004300 {
43014301 "type": "VERSION_CONFLICT",
……@@ -4309,8 +4309,8 @@
43094309 "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b"
43104310 }
43114311 ],
4312 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4313 "acceptedAt": "2026-09-15T16:11:51.209Z"
4312 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4313 "acceptedAt": "2026-09-15T16:14:36.263Z"
43144314 },
43154315 {
43164316 "type": "VERSION_CONFLICT",
……@@ -4324,8 +4324,8 @@
43244324 "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed"
43254325 }
43264326 ],
4327 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4328 "acceptedAt": "2026-09-15T16:11:51.209Z"
4327 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4328 "acceptedAt": "2026-09-15T16:14:36.263Z"
43294329 },
43304330 {
43314331 "type": "VERSION_CONFLICT",
……@@ -4339,8 +4339,8 @@
43394339 "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f"
43404340 }
43414341 ],
4342 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4343 "acceptedAt": "2026-09-15T16:11:51.209Z"
4342 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4343 "acceptedAt": "2026-09-15T16:14:36.263Z"
43444344 },
43454345 {
43464346 "type": "VERSION_CONFLICT",
……@@ -4354,8 +4354,8 @@
43544354 "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754"
43554355 }
43564356 ],
4357 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4358 "acceptedAt": "2026-09-15T16:11:51.209Z"
4357 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4358 "acceptedAt": "2026-09-15T16:14:36.263Z"
43594359 },
43604360 {
43614361 "type": "VERSION_CONFLICT",
……@@ -4369,8 +4369,8 @@
43694369 "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5"
43704370 }
43714371 ],
4372 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4373 "acceptedAt": "2026-09-15T16:11:51.209Z"
4372 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4373 "acceptedAt": "2026-09-15T16:14:36.263Z"
43744374 },
43754375 {
43764376 "type": "VERSION_CONFLICT",
……@@ -4389,8 +4389,8 @@
43894389 "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393"
43904390 }
43914391 ],
4392 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4393 "acceptedAt": "2026-09-15T16:11:51.209Z"
4392 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4393 "acceptedAt": "2026-09-15T16:14:36.263Z"
43944394 },
43954395 {
43964396 "type": "VERSION_CONFLICT",
……@@ -4404,8 +4404,8 @@
44044404 "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5"
44054405 }
44064406 ],
4407 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4408 "acceptedAt": "2026-09-15T16:11:51.209Z"
4407 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4408 "acceptedAt": "2026-09-15T16:14:36.263Z"
44094409 },
44104410 {
44114411 "type": "VERSION_CONFLICT",
……@@ -4419,8 +4419,8 @@
44194419 "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6"
44204420 }
44214421 ],
4422 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4423 "acceptedAt": "2026-09-15T16:11:51.209Z"
4422 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4423 "acceptedAt": "2026-09-15T16:14:36.263Z"
44244424 },
44254425 {
44264426 "type": "VERSION_CONFLICT",
……@@ -4434,8 +4434,8 @@
44344434 "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9"
44354435 }
44364436 ],
4437 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4438 "acceptedAt": "2026-09-15T16:11:51.209Z"
4437 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4438 "acceptedAt": "2026-09-15T16:14:36.263Z"
44394439 },
44404440 {
44414441 "type": "VERSION_CONFLICT",
……@@ -4449,8 +4449,8 @@
44494449 "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0"
44504450 }
44514451 ],
4452 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4453 "acceptedAt": "2026-09-15T16:11:51.209Z"
4452 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4453 "acceptedAt": "2026-09-15T16:14:36.263Z"
44544454 },
44554455 {
44564456 "type": "VERSION_CONFLICT",
……@@ -4464,8 +4464,8 @@
44644464 "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53"
44654465 }
44664466 ],
4467 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4468 "acceptedAt": "2026-09-15T16:11:51.209Z"
4467 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4468 "acceptedAt": "2026-09-15T16:14:36.263Z"
44694469 },
44704470 {
44714471 "type": "VERSION_CONFLICT",
……@@ -4479,8 +4479,8 @@
44794479 "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a"
44804480 }
44814481 ],
4482 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4483 "acceptedAt": "2026-09-15T16:11:51.209Z"
4482 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4483 "acceptedAt": "2026-09-15T16:14:36.263Z"
44844484 },
44854485 {
44864486 "type": "VERSION_CONFLICT",
……@@ -4499,8 +4499,8 @@
44994499 "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648"
45004500 }
45014501 ],
4502 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4503 "acceptedAt": "2026-09-15T16:11:51.209Z"
4502 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4503 "acceptedAt": "2026-09-15T16:14:36.263Z"
45044504 },
45054505 {
45064506 "type": "VERSION_CONFLICT",
……@@ -4514,8 +4514,8 @@
45144514 "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57"
45154515 }
45164516 ],
4517 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4518 "acceptedAt": "2026-09-15T16:11:51.209Z"
4517 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4518 "acceptedAt": "2026-09-15T16:14:36.263Z"
45194519 },
45204520 {
45214521 "type": "VERSION_CONFLICT",
……@@ -4529,8 +4529,8 @@
45294529 "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78"
45304530 }
45314531 ],
4532 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4533 "acceptedAt": "2026-09-15T16:11:51.209Z"
4532 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4533 "acceptedAt": "2026-09-15T16:14:36.263Z"
45344534 },
45354535 {
45364536 "type": "VERSION_CONFLICT",
……@@ -4544,8 +4544,8 @@
45444544 "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9"
45454545 }
45464546 ],
4547 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4548 "acceptedAt": "2026-09-15T16:11:51.209Z"
4547 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4548 "acceptedAt": "2026-09-15T16:14:36.263Z"
45494549 },
45504550 {
45514551 "type": "VERSION_CONFLICT",
……@@ -4559,8 +4559,8 @@
45594559 "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972"
45604560 }
45614561 ],
4562 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4563 "acceptedAt": "2026-09-15T16:11:51.209Z"
4562 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4563 "acceptedAt": "2026-09-15T16:14:36.263Z"
45644564 },
45654565 {
45664566 "type": "VERSION_CONFLICT",
……@@ -4574,8 +4574,8 @@
45744574 "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519"
45754575 }
45764576 ],
4577 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4578 "acceptedAt": "2026-09-15T16:11:51.209Z"
4577 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4578 "acceptedAt": "2026-09-15T16:14:36.263Z"
45794579 },
45804580 {
45814581 "type": "VERSION_CONFLICT",
……@@ -4589,8 +4589,8 @@
45894589 "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390"
45904590 }
45914591 ],
4592 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4593 "acceptedAt": "2026-09-15T16:11:51.209Z"
4592 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4593 "acceptedAt": "2026-09-15T16:14:36.263Z"
45944594 },
45954595 {
45964596 "type": "VERSION_CONFLICT",
……@@ -4604,8 +4604,8 @@
46044604 "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706"
46054605 }
46064606 ],
4607 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4608 "acceptedAt": "2026-09-15T16:11:51.209Z"
4607 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4608 "acceptedAt": "2026-09-15T16:14:36.263Z"
46094609 },
46104610 {
46114611 "type": "VERSION_CONFLICT",
……@@ -4619,8 +4619,8 @@
46194619 "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb"
46204620 }
46214621 ],
4622 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4623 "acceptedAt": "2026-09-15T16:11:51.209Z"
4622 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4623 "acceptedAt": "2026-09-15T16:14:36.263Z"
46244624 },
46254625 {
46264626 "type": "VERSION_CONFLICT",
……@@ -4634,8 +4634,8 @@
46344634 "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae"
46354635 }
46364636 ],
4637 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4638 "acceptedAt": "2026-09-15T16:11:51.209Z"
4637 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4638 "acceptedAt": "2026-09-15T16:14:36.263Z"
46394639 },
46404640 {
46414641 "type": "VERSION_CONFLICT",
……@@ -4654,8 +4654,8 @@
46544654 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765"
46554655 }
46564656 ],
4657 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4658 "acceptedAt": "2026-09-15T16:11:51.209Z"
4657 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4658 "acceptedAt": "2026-09-15T16:14:36.263Z"
46594659 },
46604660 {
46614661 "type": "VERSION_CONFLICT",
……@@ -4669,8 +4669,8 @@
46694669 "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60"
46704670 }
46714671 ],
4672 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4673 "acceptedAt": "2026-09-15T16:11:51.209Z"
4672 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4673 "acceptedAt": "2026-09-15T16:14:36.263Z"
46744674 },
46754675 {
46764676 "type": "VERSION_CONFLICT",
……@@ -4684,8 +4684,8 @@
46844684 "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a"
46854685 }
46864686 ],
4687 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4688 "acceptedAt": "2026-09-15T16:11:51.209Z"
4687 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4688 "acceptedAt": "2026-09-15T16:14:36.263Z"
46894689 },
46904690 {
46914691 "type": "VERSION_CONFLICT",
……@@ -4699,8 +4699,8 @@
46994699 "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b"
47004700 }
47014701 ],
4702 "reason": "0.10.5: same 45 version-splits/docs/packaging, 0 code-differs",
4703 "acceptedAt": "2026-09-15T16:11:51.209Z"
4702 "reason": "0.10.6: same 45 version-splits/docs/packaging, 0 code-differs",
4703 "acceptedAt": "2026-09-15T16:14:36.263Z"
47044704 }
47054705 ]
47064706}
lib/router.js +47 -33
……@@ -41,7 +41,9 @@ export async function handleRequest (req, ctx) {
4141 // ⚠ ANYTHING STORED BEFORE THAT FIELD EXISTED IS `undefined` AND IS WITHHELD. An operator sees a
4242 // shorter public page than they expect after upgrading; the alternative is publishing somebody's
4343 // DM because a record predates the question being asked.
44 if (path === '/timeline') return feedPage(query, ctx)
44 // ⛔ THE FRONT PAGE IS THE RELAY VIEW — every peer this bridge serves, in one stream. A peer's
45 // own page stays at /ap/users/<name>/timeline, scoped, which is what makes this one safe to combine.
46 if (path === '/timeline') return feedPage(query, ctx, { combined: true })
4547
4648 if (path === '/ap/register/challenge') {
4749 if (!ctx.registrations || !ctx.admission) return text(404, 'not found')
……@@ -320,61 +322,73 @@ async function collection (id, ctx) {
320322 * ⚠ MERGED, THEN PAGED. Paging each source separately and interleaving afterwards yields short pages
321323 * and an offset that no longer means what the link said — the mistake the old handler had to note.
322324 */
323async function feedPage (query, ctx) {
324 if (!ctx.inbound) return text(404, 'this bridge keeps no inbound timeline')
325 const limit = 50
326 const offset = Math.max(0, Number(query && query.offset) || 0)
327
325// What one peer contributes to a feed. `c` is a ctx already bound to that peer, so its records,
326// its interactions and its name all come from the same place the rest of the router gets them.
327async function gatherFor (c) {
328328 let notes = []
329 if (ctx.records && typeof ctx.records.listNotes === 'function') {
329 if (c.records && typeof c.records.listNotes === 'function') {
330330 // ⚠ A record source that is down must not blank the page. The inbound half is still true, and a
331331 // feed missing our own posts is visibly wrong in a way an error page is not.
332 try { notes = (await ctx.records.listNotes({ limit: 200, cursor: null })).notes || [] } catch (err) {
333 ctx.log?.({ msg: 'feed: own records unavailable — showing inbound only', error: err.message })
332 try { notes = (await c.records.listNotes({ limit: 200, cursor: null })).notes || [] } catch (err) {
333 c.log?.({ msg: 'feed: own records unavailable — showing inbound only', error: err.message })
334334 }
335335 }
336
337 // ⛔ OUR OWN INTERACTIONS, which is the half that makes this a diagnostic rather than an inbox.
338 // Without them the page proves inbound handling and says nothing about whether a like we sent ever
339 // left. `unaddressable` is carried through rather than dropped: "0 interactions" and "12 we cannot
340 // address" are different states and RecordSource already counts the difference.
341336 let own = []
342337 let unprojectable = []
343338 let unaddressable = 0
344 if (ctx.records && typeof ctx.records.listInteractions === 'function') {
339 if (c.records && typeof c.records.listInteractions === 'function') {
345340 try {
346 const r = await ctx.records.listInteractions({ limit: 100 })
341 const r = await c.records.listInteractions({ limit: 100 })
347342 own = r.interactions || []
348343 unprojectable = r.unprojectable || []
349344 unaddressable = r.unaddressable || 0
350345 } catch (err) {
351 ctx.log?.({ msg: 'feed: own interactions unavailable', error: err.message })
346 c.log?.({ msg: 'feed: own interactions unavailable', error: err.message })
352347 }
353348 }
349 const recipient = (c.user && c.user.username) || (c.config && c.config.username) || null
350 return { notes, own, unprojectable, unaddressable, recipient, isPrimary: !c.user || !c.users || c.users.primary === c.user }
351}
354352
355 const recipient = (ctx.user && ctx.user.username) || (ctx.config && ctx.config.username) || null
353async function feedPage (query, ctx, { combined = false } = {}) {
354 if (!ctx.inbound) return text(404, 'this bridge keeps no inbound timeline')
355 const limit = 50
356 const offset = Math.max(0, Number(query && query.offset) || 0)
357 const host = ctx.config.host
358
359 // ⛔ ONE READ OF THE STORE, FILTERED PER PEER INSIDE buildFeed. Each item carries the recipient it
360 // was delivered to, so it lands on exactly one peer's rows and cannot be counted twice — which is
361 // the property that lets a combined page exist at all without the isolation becoming decorative.
356362 const inboundItems = await ctx.inbound.list({ limit: 100000, offset: 0 })
357 const { buildFeed, threadFeed } = await import('./feed.js')
358 const flat = buildFeed({
359 self: { handle: recipient ? '@' + recipient + '@' + ctx.config.host : null, name: ctx.config.displayName || null },
360 notes,
361 inbound: inboundItems,
362 own,
363 unprojectable,
364 recipient,
365 // For recovering an item stored before `recipient` existed: the object URI still says whose it
366 // was, and the bare `/ap/objects/` form is the PRIMARY's — so only the primary may claim one.
367 host: ctx.config.host,
368 isPrimary: !ctx.user || !ctx.users || ctx.users.primary === ctx.user
369 })
363
364 const { buildFeed } = await import('./feed.js')
365 const peers = combined && ctx.users ? ctx.users.all() : [null]
366 let flat = []
367 let unaddressable = 0
368 for (const u of peers) {
369 const c = u ? withUser(ctx, u) : ctx
370 const g = await gatherFor(c)
371 unaddressable += g.unaddressable
372 flat = flat.concat(buildFeed({
373 self: { handle: g.recipient ? '@' + g.recipient + '@' + host : null, name: (c.user && c.user.displayName) || (!u && ctx.config.displayName) || null },
374 notes: g.notes,
375 inbound: inboundItems,
376 own: g.own,
377 unprojectable: g.unprojectable,
378 recipient: g.recipient,
379 host,
380 isPrimary: g.isPrimary
381 }))
382 }
370383
371384 // ⛔ THREADED, THEN PAGED. Paging the flat rows first would cut a post away from its own replies.
385 const { threadFeed } = await import('./feed.js')
372386 const rows = threadFeed(flat)
373387
374388 const { publicTimeline } = await import('./public-timeline.js')
375389 return html(200, publicTimeline({
376 host: ctx.config.host,
377 who: recipient ? '@' + recipient + '@' + ctx.config.host : ctx.config.host,
390 host,
391 who: combined ? host : (peers[0] === null && ctx.user ? '@' + ctx.user.username + '@' + host : '@' + ((ctx.user && ctx.user.username) || ctx.config.username) + '@' + host),
378392 rows: rows.slice(offset, offset + limit),
379393 offset,
380394 limit,
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "ap-bridge",
3 "version": "0.10.6",
3 "version": "0.10.7",
44 "description": "ActivityPub membrane \u2014 a second projection of the self-custody atproto peer",
55 "type": "module",
66 "main": "index.js",
test/router.test.js +29 -0
……@@ -466,3 +466,32 @@ test('★★ the feed shows BOTH directions — a like we sent and a like we got
466466 t.ok(/2 of our interaction\(s\) could not be projected to ActivityPub/.test(res.body),
467467 'and the ones that cannot cross say so — they are shown here, not silently absent')
468468})
469
470test('★★ /timeline is the COMBINED view — every peer this bridge serves, in one stream', async (t) => {
471 const { Users } = await import('../lib/users.js')
472 const users = new Users([{ username: 'p2p', primary: true }, { username: 'fleeky' }], 'p2p.7rnx.net')
473 const inbound = new MemoryInboundStore()
474 const notesFor = {
475 p2p: [{ id: 'https://p2p.7rnx.net/ap/objects/1', type: 'Note', content: '<p>from p2p</p>', published: '2026-09-15T09:00:00Z' }],
476 fleeky: [{ id: 'https://p2p.7rnx.net/ap/users/fleeky/objects/2', type: 'Note', content: '<p>from fleeky</p>', published: '2026-09-15T10:00:00Z' }]
477 }
478 const ctx = makeCtx({
479 users,
480 inbound,
481 recordsFor: (u) => ({ async listNotes () { return { notes: notesFor[u.username] || [] } } }),
482 records: { async listNotes () { return { notes: notesFor.p2p } } }
483 })
484 await inbound.add({ id: 'l', type: 'Like', actor: REMOTE, objectId: 'https://p2p.7rnx.net/ap/users/fleeky/objects/2', public: true, recipient: 'fleeky', published: '2026-09-15T11:00:00Z' })
485
486 const all = await handleRequest({ method: 'GET', path: '/timeline', headers: {} }, ctx)
487 t.is(all.status, 200)
488 t.ok(/from p2p/.test(all.body), "the primary's posts")
489 t.ok(/from fleeky/.test(all.body), "and a registered peer's — one bridge, one stream")
490 t.ok(/♥ 1/.test(all.body), "and an interaction lands on ITS OWN peer's post, not the primary's")
491
492 // ⛔ THE PER-PEER PAGES STAY SCOPED. A combined front page must not make one peer's page show
493 // another's, or the isolation the recipient field exists for is decorative.
494 const mine = await handleRequest({ method: 'GET', path: '/ap/users/p2p/timeline', headers: {} }, ctx)
495 t.ok(/from p2p/.test(mine.body))
496 t.absent(/from fleeky/.test(mine.body), "p2p's page is p2p's alone")
497})