OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge commits / 0f3fa61

0.10.11: a registered peer collects the observations held for it
The `collect` verb on `favela/bridge-register/1`, with its rules in `collect.js` so the swarm door is transport and nothing else — the same split `admit.js` already earns. ⛔ TWO QUESTIONS, AND THE SECOND IS THE ONE THAT GETS SKIPPED. 1. does this caller control the root it names? → the same ceremony as claim and release 2. does that root HOLD this name? → here, and nowhere else Claim and release only ever needed the first: a claim names a FREE name, and a release compares the root as part of doing the work. A collect names a name somebody already holds, and AN IDENTITY ROOT IS PUBLIC. So a caller who proves control of their own root while naming somebody else's username passes question one completely honestly — every signature verifies — and must still be refused. Without question two this verb hands a stranger the entire inbound social graph of anyone whose username they can read off a web page. ⛔ A READ, NOT A DRAIN. Deleting rows as they are served is the obvious buffer reflex and it is wrong twice over: a peer that dies between the reply and its own write loses them for good, and a SECOND device holding the same name never sees them at all — which registrations.js explicitly supports. The cap is what bounds the buffer; forgetting is not required to make it correct. ⛔ 404 RATHER THAN AN EMPTY PAGE for an unheld name. "Nobody holds that" and "you have no observations yet" are different states and a peer acts differently on each; an empty answer reads as the second and a device waits for ever. Both guards are proven by MUTATION rather than by a green suite: removing the name check fails exactly one test, unbinding the cursor fails exactly one. A third test reads index.js and asserts the store is handed to the door — that omission would boot cleanly, pass every unit test, and tell every peer for ever that it has nothing. ⚠ UNEXERCISED OVER A REAL SWARM. Signing as a registered name needs a device credential and neither is reachable from here. See #73 §6.5. 363/363, 1277 asserts. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
0f3fa618b5eb7ddbf42bd6357d5027de5f4bd2b6
by plankatron <fleeky@gmail.com> · 2026-09-16T01:53:32.000Z

Changed files (6) +394 -96

.pgh-lock.json +91 -91
……@@ -1,9 +1,9 @@
11{
22 "name": "ap-bridge",
3 "version": "0.10.9",
3 "version": "0.10.10",
44 "runtime": "bare",
5 "rootTreeHash": "7c995430fefe253aa0d1954eb1c0b831190d4cf0e313fc8eb45bd4263e92f19f",
6 "publishedAt": "2026-09-16T00:30:11.529Z",
5 "rootTreeHash": "89be5367b9a74b43b9a443cb56e8d1db2e29cdd8afb4c285cbfdadc128173f7c",
6 "publishedAt": "2026-09-16T00:37:00.593Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -4004,8 +4004,8 @@
40044004 "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37"
40054005 }
40064006 ],
4007 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4008 "acceptedAt": "2026-09-16T00:30:11.555Z"
4007 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4008 "acceptedAt": "2026-09-16T00:37:00.619Z"
40094009 },
40104010 {
40114011 "type": "VERSION_CONFLICT",
……@@ -4019,8 +4019,8 @@
40194019 "tree": "ee91b6bd77101e3404ca571b2e5adf0f47029d174398a05f50294fc207e39d99"
40204020 }
40214021 ],
4022 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4023 "acceptedAt": "2026-09-16T00:30:11.555Z"
4022 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4023 "acceptedAt": "2026-09-16T00:37:00.619Z"
40244024 },
40254025 {
40264026 "type": "VERSION_CONFLICT",
……@@ -4034,8 +4034,8 @@
40344034 "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11"
40354035 }
40364036 ],
4037 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4038 "acceptedAt": "2026-09-16T00:30:11.555Z"
4037 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4038 "acceptedAt": "2026-09-16T00:37:00.619Z"
40394039 },
40404040 {
40414041 "type": "VERSION_CONFLICT",
……@@ -4054,8 +4054,8 @@
40544054 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
40554055 }
40564056 ],
4057 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4058 "acceptedAt": "2026-09-16T00:30:11.555Z"
4057 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4058 "acceptedAt": "2026-09-16T00:37:00.619Z"
40594059 },
40604060 {
40614061 "type": "VERSION_CONFLICT",
……@@ -4069,8 +4069,8 @@
40694069 "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd"
40704070 }
40714071 ],
4072 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4073 "acceptedAt": "2026-09-16T00:30:11.555Z"
4072 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4073 "acceptedAt": "2026-09-16T00:37:00.619Z"
40744074 },
40754075 {
40764076 "type": "VERSION_CONFLICT",
……@@ -4084,8 +4084,8 @@
40844084 "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95"
40854085 }
40864086 ],
4087 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4088 "acceptedAt": "2026-09-16T00:30:11.555Z"
4087 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4088 "acceptedAt": "2026-09-16T00:37:00.619Z"
40894089 },
40904090 {
40914091 "type": "VERSION_CONFLICT",
……@@ -4099,8 +4099,8 @@
40994099 "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb"
41004100 }
41014101 ],
4102 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4103 "acceptedAt": "2026-09-16T00:30:11.555Z"
4102 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4103 "acceptedAt": "2026-09-16T00:37:00.619Z"
41044104 },
41054105 {
41064106 "type": "VERSION_CONFLICT",
……@@ -4114,8 +4114,8 @@
41144114 "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a"
41154115 }
41164116 ],
4117 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4118 "acceptedAt": "2026-09-16T00:30:11.555Z"
4117 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4118 "acceptedAt": "2026-09-16T00:37:00.619Z"
41194119 },
41204120 {
41214121 "type": "VERSION_CONFLICT",
……@@ -4129,8 +4129,8 @@
41294129 "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca"
41304130 }
41314131 ],
4132 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4133 "acceptedAt": "2026-09-16T00:30:11.555Z"
4132 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4133 "acceptedAt": "2026-09-16T00:37:00.619Z"
41344134 },
41354135 {
41364136 "type": "VERSION_CONFLICT",
……@@ -4144,8 +4144,8 @@
41444144 "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701"
41454145 }
41464146 ],
4147 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4148 "acceptedAt": "2026-09-16T00:30:11.555Z"
4147 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4148 "acceptedAt": "2026-09-16T00:37:00.619Z"
41494149 },
41504150 {
41514151 "type": "VERSION_CONFLICT",
……@@ -4159,8 +4159,8 @@
41594159 "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d"
41604160 }
41614161 ],
4162 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4163 "acceptedAt": "2026-09-16T00:30:11.555Z"
4162 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4163 "acceptedAt": "2026-09-16T00:37:00.619Z"
41644164 },
41654165 {
41664166 "type": "VERSION_CONFLICT",
……@@ -4174,8 +4174,8 @@
41744174 "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04"
41754175 }
41764176 ],
4177 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4178 "acceptedAt": "2026-09-16T00:30:11.555Z"
4177 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4178 "acceptedAt": "2026-09-16T00:37:00.619Z"
41794179 },
41804180 {
41814181 "type": "VERSION_CONFLICT",
……@@ -4189,8 +4189,8 @@
41894189 "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4"
41904190 }
41914191 ],
4192 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4193 "acceptedAt": "2026-09-16T00:30:11.555Z"
4192 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4193 "acceptedAt": "2026-09-16T00:37:00.619Z"
41944194 },
41954195 {
41964196 "type": "VERSION_CONFLICT",
……@@ -4204,8 +4204,8 @@
42044204 "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df"
42054205 }
42064206 ],
4207 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4208 "acceptedAt": "2026-09-16T00:30:11.555Z"
4207 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4208 "acceptedAt": "2026-09-16T00:37:00.619Z"
42094209 },
42104210 {
42114211 "type": "VERSION_CONFLICT",
……@@ -4219,8 +4219,8 @@
42194219 "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad"
42204220 }
42214221 ],
4222 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4223 "acceptedAt": "2026-09-16T00:30:11.555Z"
4222 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4223 "acceptedAt": "2026-09-16T00:37:00.619Z"
42244224 },
42254225 {
42264226 "type": "VERSION_CONFLICT",
……@@ -4234,8 +4234,8 @@
42344234 "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e"
42354235 }
42364236 ],
4237 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4238 "acceptedAt": "2026-09-16T00:30:11.555Z"
4237 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4238 "acceptedAt": "2026-09-16T00:37:00.619Z"
42394239 },
42404240 {
42414241 "type": "VERSION_CONFLICT",
……@@ -4264,8 +4264,8 @@
42644264 "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c"
42654265 }
42664266 ],
4267 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4268 "acceptedAt": "2026-09-16T00:30:11.555Z"
4267 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4268 "acceptedAt": "2026-09-16T00:37:00.619Z"
42694269 },
42704270 {
42714271 "type": "VERSION_CONFLICT",
……@@ -4279,8 +4279,8 @@
42794279 "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9"
42804280 }
42814281 ],
4282 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4283 "acceptedAt": "2026-09-16T00:30:11.555Z"
4282 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4283 "acceptedAt": "2026-09-16T00:37:00.619Z"
42844284 },
42854285 {
42864286 "type": "VERSION_CONFLICT",
……@@ -4294,8 +4294,8 @@
42944294 "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169"
42954295 }
42964296 ],
4297 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4298 "acceptedAt": "2026-09-16T00:30:11.555Z"
4297 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4298 "acceptedAt": "2026-09-16T00:37:00.619Z"
42994299 },
43004300 {
43014301 "type": "VERSION_CONFLICT",
……@@ -4309,8 +4309,8 @@
43094309 "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b"
43104310 }
43114311 ],
4312 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4313 "acceptedAt": "2026-09-16T00:30:11.555Z"
4312 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4313 "acceptedAt": "2026-09-16T00:37:00.619Z"
43144314 },
43154315 {
43164316 "type": "VERSION_CONFLICT",
……@@ -4324,8 +4324,8 @@
43244324 "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed"
43254325 }
43264326 ],
4327 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4328 "acceptedAt": "2026-09-16T00:30:11.555Z"
4327 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4328 "acceptedAt": "2026-09-16T00:37:00.619Z"
43294329 },
43304330 {
43314331 "type": "VERSION_CONFLICT",
……@@ -4339,8 +4339,8 @@
43394339 "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f"
43404340 }
43414341 ],
4342 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4343 "acceptedAt": "2026-09-16T00:30:11.555Z"
4342 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4343 "acceptedAt": "2026-09-16T00:37:00.619Z"
43444344 },
43454345 {
43464346 "type": "VERSION_CONFLICT",
……@@ -4354,8 +4354,8 @@
43544354 "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754"
43554355 }
43564356 ],
4357 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4358 "acceptedAt": "2026-09-16T00:30:11.555Z"
4357 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4358 "acceptedAt": "2026-09-16T00:37:00.619Z"
43594359 },
43604360 {
43614361 "type": "VERSION_CONFLICT",
……@@ -4369,8 +4369,8 @@
43694369 "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5"
43704370 }
43714371 ],
4372 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4373 "acceptedAt": "2026-09-16T00:30:11.555Z"
4372 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4373 "acceptedAt": "2026-09-16T00:37:00.619Z"
43744374 },
43754375 {
43764376 "type": "VERSION_CONFLICT",
……@@ -4389,8 +4389,8 @@
43894389 "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393"
43904390 }
43914391 ],
4392 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4393 "acceptedAt": "2026-09-16T00:30:11.555Z"
4392 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4393 "acceptedAt": "2026-09-16T00:37:00.619Z"
43944394 },
43954395 {
43964396 "type": "VERSION_CONFLICT",
……@@ -4404,8 +4404,8 @@
44044404 "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5"
44054405 }
44064406 ],
4407 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4408 "acceptedAt": "2026-09-16T00:30:11.555Z"
4407 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4408 "acceptedAt": "2026-09-16T00:37:00.619Z"
44094409 },
44104410 {
44114411 "type": "VERSION_CONFLICT",
……@@ -4419,8 +4419,8 @@
44194419 "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6"
44204420 }
44214421 ],
4422 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4423 "acceptedAt": "2026-09-16T00:30:11.555Z"
4422 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4423 "acceptedAt": "2026-09-16T00:37:00.619Z"
44244424 },
44254425 {
44264426 "type": "VERSION_CONFLICT",
……@@ -4434,8 +4434,8 @@
44344434 "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9"
44354435 }
44364436 ],
4437 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4438 "acceptedAt": "2026-09-16T00:30:11.555Z"
4437 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4438 "acceptedAt": "2026-09-16T00:37:00.619Z"
44394439 },
44404440 {
44414441 "type": "VERSION_CONFLICT",
……@@ -4449,8 +4449,8 @@
44494449 "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0"
44504450 }
44514451 ],
4452 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4453 "acceptedAt": "2026-09-16T00:30:11.555Z"
4452 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4453 "acceptedAt": "2026-09-16T00:37:00.619Z"
44544454 },
44554455 {
44564456 "type": "VERSION_CONFLICT",
……@@ -4464,8 +4464,8 @@
44644464 "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53"
44654465 }
44664466 ],
4467 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4468 "acceptedAt": "2026-09-16T00:30:11.555Z"
4467 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4468 "acceptedAt": "2026-09-16T00:37:00.619Z"
44694469 },
44704470 {
44714471 "type": "VERSION_CONFLICT",
……@@ -4479,8 +4479,8 @@
44794479 "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a"
44804480 }
44814481 ],
4482 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4483 "acceptedAt": "2026-09-16T00:30:11.555Z"
4482 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4483 "acceptedAt": "2026-09-16T00:37:00.619Z"
44844484 },
44854485 {
44864486 "type": "VERSION_CONFLICT",
……@@ -4499,8 +4499,8 @@
44994499 "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648"
45004500 }
45014501 ],
4502 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4503 "acceptedAt": "2026-09-16T00:30:11.555Z"
4502 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4503 "acceptedAt": "2026-09-16T00:37:00.619Z"
45044504 },
45054505 {
45064506 "type": "VERSION_CONFLICT",
……@@ -4514,8 +4514,8 @@
45144514 "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57"
45154515 }
45164516 ],
4517 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4518 "acceptedAt": "2026-09-16T00:30:11.555Z"
4517 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4518 "acceptedAt": "2026-09-16T00:37:00.619Z"
45194519 },
45204520 {
45214521 "type": "VERSION_CONFLICT",
……@@ -4529,8 +4529,8 @@
45294529 "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78"
45304530 }
45314531 ],
4532 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4533 "acceptedAt": "2026-09-16T00:30:11.555Z"
4532 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4533 "acceptedAt": "2026-09-16T00:37:00.619Z"
45344534 },
45354535 {
45364536 "type": "VERSION_CONFLICT",
……@@ -4544,8 +4544,8 @@
45444544 "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9"
45454545 }
45464546 ],
4547 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4548 "acceptedAt": "2026-09-16T00:30:11.555Z"
4547 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4548 "acceptedAt": "2026-09-16T00:37:00.619Z"
45494549 },
45504550 {
45514551 "type": "VERSION_CONFLICT",
……@@ -4559,8 +4559,8 @@
45594559 "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972"
45604560 }
45614561 ],
4562 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4563 "acceptedAt": "2026-09-16T00:30:11.555Z"
4562 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4563 "acceptedAt": "2026-09-16T00:37:00.619Z"
45644564 },
45654565 {
45664566 "type": "VERSION_CONFLICT",
……@@ -4574,8 +4574,8 @@
45744574 "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519"
45754575 }
45764576 ],
4577 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4578 "acceptedAt": "2026-09-16T00:30:11.555Z"
4577 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4578 "acceptedAt": "2026-09-16T00:37:00.619Z"
45794579 },
45804580 {
45814581 "type": "VERSION_CONFLICT",
……@@ -4589,8 +4589,8 @@
45894589 "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390"
45904590 }
45914591 ],
4592 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4593 "acceptedAt": "2026-09-16T00:30:11.555Z"
4592 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4593 "acceptedAt": "2026-09-16T00:37:00.619Z"
45944594 },
45954595 {
45964596 "type": "VERSION_CONFLICT",
……@@ -4604,8 +4604,8 @@
46044604 "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706"
46054605 }
46064606 ],
4607 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4608 "acceptedAt": "2026-09-16T00:30:11.555Z"
4607 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4608 "acceptedAt": "2026-09-16T00:37:00.619Z"
46094609 },
46104610 {
46114611 "type": "VERSION_CONFLICT",
……@@ -4619,8 +4619,8 @@
46194619 "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb"
46204620 }
46214621 ],
4622 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4623 "acceptedAt": "2026-09-16T00:30:11.555Z"
4622 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4623 "acceptedAt": "2026-09-16T00:37:00.619Z"
46244624 },
46254625 {
46264626 "type": "VERSION_CONFLICT",
……@@ -4634,8 +4634,8 @@
46344634 "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae"
46354635 }
46364636 ],
4637 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4638 "acceptedAt": "2026-09-16T00:30:11.555Z"
4637 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4638 "acceptedAt": "2026-09-16T00:37:00.619Z"
46394639 },
46404640 {
46414641 "type": "VERSION_CONFLICT",
……@@ -4649,8 +4649,8 @@
46494649 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765"
46504650 }
46514651 ],
4652 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4653 "acceptedAt": "2026-09-16T00:30:11.555Z"
4652 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4653 "acceptedAt": "2026-09-16T00:37:00.619Z"
46544654 },
46554655 {
46564656 "type": "VERSION_CONFLICT",
……@@ -4664,8 +4664,8 @@
46644664 "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60"
46654665 }
46664666 ],
4667 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4668 "acceptedAt": "2026-09-16T00:30:11.555Z"
4667 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4668 "acceptedAt": "2026-09-16T00:37:00.619Z"
46694669 },
46704670 {
46714671 "type": "VERSION_CONFLICT",
……@@ -4679,8 +4679,8 @@
46794679 "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b"
46804680 }
46814681 ],
4682 "reason": "0.10.9: observations store + inbox routing; same 45 version-splits/docs/packaging, 0 code-differs",
4683 "acceptedAt": "2026-09-16T00:30:11.555Z"
4682 "reason": "0.10.10: per-peer deadline on the combined timeline; same version-splits/docs/packaging, 0 code-differs",
4683 "acceptedAt": "2026-09-16T00:37:00.619Z"
46844684 }
46854685 ]
46864686}
index.js +1 -1
……@@ -483,7 +483,7 @@ async function main () {
483483 if (registrations && holder && holder.swarm) {
484484 try {
485485 const { serveRegistration } = await import('./lib/register-rpc.js')
486 const reg = serveRegistration({ swarm: holder.swarm, registrations, admission, config, onRegistered: ctx.onRegistered, onReleased: ctx.onReleased, log })
486 const reg = serveRegistration({ swarm: holder.swarm, registrations, admission, observations, config, onRegistered: ctx.onRegistered, onReleased: ctx.onReleased, log })
487487 await reg.ready()
488488 // ⛔ PRINTED, because it is what a peer needs in order to find this bridge at all. A key that
489489 // exists and is written nowhere is the favela-android device-key mistake one layer over.
lib/collect.js +68 -0
……@@ -0,0 +1,68 @@
1// collect.js — a registered peer asking this bridge for the observations it is HOLDING for that peer.
2//
3// ⛔ THE BRIDGE ACCUMULATES, THE PEER STORES. `observations.js` holds what the fediverse did to a
4// registered peer's records because that peer was not there to see it, and this is how it gets them.
5// We are not a writer on their feed and never will be (hyper-social #61 §4: authoring into somebody
6// else's feed is exactly what removing the room primitive was meant to prevent), so the transfer has
7// to be a PULL. The peer asks, the peer decides what to keep, the peer writes it.
8//
9// ⛔ TWO QUESTIONS, AND THE SECOND ONE IS THE ONE THAT GETS SKIPPED.
10//
11// 1. does this caller control the root it names? → `admission.verifyCollect`, the same ceremony
12// as claim and release, with its own prefix
13// 2. does that root HOLD this name? → here, and nowhere else
14//
15// Claim and release only ever needed the first: a claim names a FREE name, and a release compares the
16// root against the registration as part of doing the work. A collect names a name that is already
17// held by somebody, and an identity root is PUBLIC. So a caller who proves control of their own root
18// while naming somebody else's username passes question 1 completely honestly — every signature
19// verifies — and must still be refused. Without question 2 this verb hands a stranger the entire
20// inbound social graph of anyone whose username they can read off a web page.
21//
22// ⛔ A READ, NOT A DRAIN. OBSERVED-INTERACTIONS.md §9 calls the buffer "bounded; prunable": bounded
23// is `DEFAULT_CAP`, prunable is a separate act. Deleting rows as they are served is the obvious
24// "buffer" reflex and it is wrong twice over — a peer that dies between the reply and its own write
25// loses them for good, and a SECOND device holding the same name never sees them at all, which
26// registrations.js explicitly supports ("a person can hold one name across a replaced device"). The
27// cursor is what makes repeated collection cheap; forgetting is not required to make it correct.
28//
29// ⚠ `{ status, body }` like admit.js, and for the same reason: the status codes are already the
30// contract a peer reads, and a second vocabulary for the swarm would be two things to keep in step.
31
32/**
33 * @param deps.registrations who holds which name
34 * @param deps.admission the challenge/proof ceremony
35 * @param deps.observations the buffer being served
36 */
37export async function admitCollect ({ registrations, admission, observations }, params = {}) {
38 if (!observations || !registrations) return { status: 404, body: { error: 'not found' } }
39 if (!admission) return { status: 503, body: { error: 'registration is not configured on this bridge' } }
40
41 // Question 1. Bytes cover the nonce, the name AND the cursor, so a proof lifted in flight cannot be
42 // resubmitted with `since` wound back to 0 to rake in the whole buffer.
43 const ok = admission.verifyCollect({
44 nonce: params.nonce,
45 username: params.username,
46 since: params.since,
47 root: params.root,
48 proof: params.proof
49 })
50 if (!ok.ok) return { status: 403, body: { error: ok.error } }
51
52 const held = await registrations.get(params.username)
53 // ⛔ 404 RATHER THAN AN EMPTY PAGE. "nobody holds that name" and "you have no observations yet" are
54 // different states and a peer acts differently on each — the first means re-claim, the second means
55 // wait. An empty answer would read as the second and a device would sit there for ever.
56 if (!held) return { status: 404, body: { error: 'not registered' } }
57
58 // Question 2. `ok.root` is the root the PROOF attested, not the one the caller typed — comparing
59 // `params.root` here would be comparing the request against itself.
60 if (held.root !== String(ok.root).toLowerCase()) {
61 return { status: 403, body: { error: 'not yours to collect' } }
62 }
63
64 const page = observations.list({ recipient: held.username, since: params.since, limit: params.limit })
65 return { status: 200, body: { rows: page.rows, cursor: page.cursor, more: page.more } }
66}
67
68export default { admitCollect }
lib/register-rpc.js +20 -3
……@@ -6,9 +6,10 @@
66// speak the swarm, and this bridge is already a member of it — `feed-holder.js` runs one to hold
77// registered peers' feeds, and its own header says a bridge "is a service peers CONNECT TO".
88//
9// ⛔ THE SAME ROOM, A SECOND DOOR. Every rule lives in `admit.js`; this file is transport. A copy of
10// the claim flow here would be the failure `favela-markdown/lib/reach.js` names — written twice, and
11// the second one eventually written wrong.
9// ⛔ THE SAME ROOM, A SECOND DOOR. Every rule lives behind this file — admission in `admit.js`,
10// collection in `collect.js`; this file is transport and nothing else. A copy of the claim flow here
11// would be the failure `favela-markdown/lib/reach.js` names — written twice, and the second one
12// eventually written wrong.
1213//
1314// ⛔ AUTHORISATION IS THE CHAIN PROOF, NOT THE CONNECTION. hyper-proto-peer gates `apply-write` on
1415// `conn.remotePublicKey` against an allowlist because it relays for known callers. A bridge accepts
……@@ -49,6 +50,22 @@ export function registerHandlers (deps) {
4950 release: async (params = {}) => {
5051 const { admitRelease } = await import('./admit.js')
5152 return admitRelease(deps, params)
53 },
54
55 /**
56 * What the fediverse did to this peer's records while it was not looking.
57 *
58 * ⛔ THE REASON THIS DOOR HAS TO EXIST. A handset is not reachable — measured on this very
59 * bridge, `/ap/users/fleeky/outbox` hangs for the full timeout because the phone holding that
60 * feed does not answer. A push would have nowhere to go. So the peer PULLS, on its own schedule,
61 * over the connection it already opens to register.
62 *
63 * ⚠ Paged and cursored rather than "everything since I last asked": the bridge keeps no per-peer
64 * read position, because a peer's second device would silently move the first one's.
65 */
66 collect: async (params = {}) => {
67 const { admitCollect } = await import('./collect.js')
68 return admitCollect(deps, params)
5269 }
5370 }
5471}
package.json +1 -1
……@@ -1,6 +1,6 @@
11{
22 "name": "ap-bridge",
3 "version": "0.10.10",
3 "version": "0.10.11",
44 "description": "ActivityPub membrane \u2014 a second projection of the self-custody atproto peer",
55 "type": "module",
66 "main": "index.js",
test/collect.test.js +213 -0
……@@ -0,0 +1,213 @@
1// collect.test.js — a peer collecting the observations this bridge holds FOR IT.
2//
3// ⛔ THE HALF THAT IS EASY TO GET WRONG. Claim and release ask one question: does this caller control
4// the root it names. Collect asks TWO, and the second is the one a reviewer skips — does that root
5// HOLD THIS NAME. Roots are public and names are first-come, so a caller proving control of their own
6// root while naming somebody else's username would otherwise be handed that person's entire inbound
7// social graph. Control of a root is not authority over a name.
8//
9// ⛔ AND IT IS A READ, NOT A DRAIN. OBSERVED-INTERACTIONS.md §9 says the buffer is "bounded;
10// prunable" — bounded is the cap, prunable is an operator's act. Deleting rows as they are served
11// would lose them for a SECOND device holding the same name, which registrations.js explicitly
12// supports ("a person can hold one name across a replaced device").
13
14import test from 'brittle'
15import b4a from 'b4a'
16import fs from 'bare-fs'
17import IdentityKey from 'keet-identity-key'
18import crypto from 'hypercore-crypto'
19import { signAttested } from 'favela-identity/lib/attestation.js'
20import { MemoryRegistrations } from '../lib/registrations.js'
21import { Admission } from '../lib/admission.js'
22import { ObservationStore, KIND } from '../lib/observations.js'
23import { admitCollect } from '../lib/collect.js'
24import { registerHandlers } from '../lib/register-rpc.js'
25
26const FEED = 'f'.repeat(64)
27
28async function identity () {
29 const idk = await IdentityKey.from({ mnemonic: IdentityKey.generateMnemonic() })
30 const kp = crypto.keyPair()
31 const proof = await IdentityKey.bootstrap({ identity: idk.identityKeyPair }, kp.publicKey)
32 return { rootHex: b4a.toString(idk.identityKeyPair.publicKey, 'hex'), cred: { keyPair: kp, proof: b4a.toString(proof, 'base64') } }
33}
34
35// An in-memory store, so a test never touches a real bridge's buffer.
36function store (t) {
37 const path = '/tmp/ap-bridge-collect-' + Math.random().toString(16).slice(2) + '.jsonl'
38 const s = new ObservationStore({ path })
39 t.teardown(() => { try { fs.unlinkSync(path) } catch {} })
40 return s
41}
42
43async function bridge (t, { policy = 'open' } = {}) {
44 const d = {
45 registrations: new MemoryRegistrations({ policy }),
46 admission: new Admission(),
47 observations: store(t),
48 config: { host: 'ap.7rnx.net' }
49 }
50 return d
51}
52
53// Register `username` to a fresh identity, the way a real peer would have.
54async function held (d, username) {
55 const me = await identity()
56 await d.registrations.claim({ username, root: me.rootHex, feedKey: FEED })
57 return me
58}
59
60const sign = (d, me, { nonce, username, since = 0 }) =>
61 signAttested(d.admission.collectBytes({ nonce, username, since }), me.cred)
62
63test('★★ the whole ceremony: challenge, sign, collect what is held for me', async (t) => {
64 const d = await bridge(t)
65 const me = await held(d, 'alice')
66 d.observations.note({ recipient: 'alice', subject: 'https://ap.7rnx.net/x', actor: 'https://prsm.space/users/bob', kind: KIND.LIKE, id: 'l1' })
67 d.observations.note({ recipient: 'alice', subject: 'https://ap.7rnx.net/y', actor: 'https://prsm.space/users/bob', kind: KIND.REPLY, id: 'r1' })
68
69 const { nonce } = d.admission.challenge()
70 const r = await admitCollect(d, { username: 'alice', root: me.rootHex, since: 0, nonce, proof: sign(d, me, { nonce, username: 'alice' }) })
71
72 t.is(r.status, 200)
73 t.is(r.body.rows.length, 2)
74 t.is(r.body.rows[0].actor, 'https://prsm.space/users/bob')
75 t.is(r.body.cursor, r.body.rows[1].seq, 'the cursor names the last row actually handed over')
76 t.absent(r.body.more)
77})
78
79test('⛔ CONTROL OF A ROOT IS NOT AUTHORITY OVER A NAME', async (t) => {
80 const d = await bridge(t)
81 await held(d, 'alice')
82 const mallory = await identity() // a perfectly valid identity — just not alice's
83 d.observations.note({ recipient: 'alice', subject: 'https://ap.7rnx.net/x', actor: 'https://prsm.space/users/bob', id: 'l1' })
84
85 const { nonce } = d.admission.challenge()
86 // Mallory signs honestly, for her own root, naming alice. Every signature here verifies.
87 const r = await admitCollect(d, { username: 'alice', root: mallory.rootHex, since: 0, nonce, proof: sign(d, mallory, { nonce, username: 'alice' }) })
88
89 t.is(r.status, 403, 'refused')
90 t.is(r.body.error, 'not yours to collect')
91 t.absent(r.body.rows, 'and not one row leaked')
92})
93
94test('⛔ a proof from somebody else, under the right root, is refused', async (t) => {
95 const d = await bridge(t)
96 const me = await held(d, 'alice')
97 const other = await identity()
98
99 const { nonce } = d.admission.challenge()
100 const r = await admitCollect(d, { username: 'alice', root: me.rootHex, since: 0, nonce, proof: sign(d, other, { nonce, username: 'alice' }) })
101 t.is(r.status, 403)
102 t.is(r.body.error, 'proof does not attest that identity')
103})
104
105test('⛔ no proof at all is refused', async (t) => {
106 const d = await bridge(t)
107 const me = await held(d, 'alice')
108 const r = await admitCollect(d, { username: 'alice', root: me.rootHex, since: 0 })
109 t.is(r.status, 403)
110})
111
112test('⛔ a captured proof cannot be replayed with a different cursor', async (t) => {
113 const d = await bridge(t)
114 const me = await held(d, 'alice')
115 for (let i = 0; i < 3; i++) d.observations.note({ recipient: 'alice', actor: 'https://prsm.space/users/bob', id: 'l' + i })
116
117 const { nonce } = d.admission.challenge()
118 // Signed for since=2; submitted as since=0 to rake in everything from the start.
119 const proof = sign(d, me, { nonce, username: 'alice', since: 2 })
120 const r = await admitCollect(d, { username: 'alice', root: me.rootHex, since: 0, nonce, proof })
121 t.is(r.status, 403, 'the cursor is inside the signed bytes')
122})
123
124test('⛔ one peer can never collect another peer\'s rows', async (t) => {
125 const d = await bridge(t)
126 const me = await held(d, 'alice')
127 await held(d, 'bob')
128 d.observations.note({ recipient: 'bob', actor: 'https://prsm.space/users/x', id: 'b1' })
129 d.observations.note({ recipient: 'alice', actor: 'https://prsm.space/users/y', id: 'a1' })
130
131 const { nonce } = d.admission.challenge()
132 const r = await admitCollect(d, { username: 'alice', root: me.rootHex, since: 0, nonce, proof: sign(d, me, { nonce, username: 'alice' }) })
133 t.is(r.body.rows.length, 1)
134 t.is(r.body.rows[0].id, 'a1', 'bob\'s row is not in alice\'s answer')
135})
136
137test('★ collecting twice resumes from the cursor rather than re-reading', async (t) => {
138 const d = await bridge(t)
139 const me = await held(d, 'alice')
140 for (let i = 0; i < 5; i++) d.observations.note({ recipient: 'alice', actor: 'https://prsm.space/users/bob', id: 'l' + i })
141
142 const c1 = d.admission.challenge()
143 const r1 = await admitCollect(d, { username: 'alice', root: me.rootHex, since: 0, limit: 2, nonce: c1.nonce, proof: sign(d, me, { nonce: c1.nonce, username: 'alice' }) })
144 t.is(r1.body.rows.length, 2)
145 t.ok(r1.body.more, 'and says so')
146
147 const c2 = d.admission.challenge()
148 const r2 = await admitCollect(d, { username: 'alice', root: me.rootHex, since: r1.body.cursor, nonce: c2.nonce, proof: sign(d, me, { nonce: c2.nonce, username: 'alice', since: r1.body.cursor }) })
149 t.is(r2.body.rows.length, 3, 'the rest, and none of the first page')
150 t.absent(r2.body.more)
151})
152
153test('⛔ a read NEVER drains — a second device holding the same name still gets them', async (t) => {
154 const d = await bridge(t)
155 const me = await held(d, 'alice')
156 d.observations.note({ recipient: 'alice', actor: 'https://prsm.space/users/bob', id: 'l1' })
157
158 const c1 = d.admission.challenge()
159 await admitCollect(d, { username: 'alice', root: me.rootHex, since: 0, nonce: c1.nonce, proof: sign(d, me, { nonce: c1.nonce, username: 'alice' }) })
160
161 const c2 = d.admission.challenge()
162 const again = await admitCollect(d, { username: 'alice', root: me.rootHex, since: 0, nonce: c2.nonce, proof: sign(d, me, { nonce: c2.nonce, username: 'alice' }) })
163 t.is(again.body.rows.length, 1, 'still there')
164})
165
166test('⛔ an unregistered name is a 404, not an empty answer', async (t) => {
167 const d = await bridge(t)
168 const me = await identity()
169 const { nonce } = d.admission.challenge()
170 const r = await admitCollect(d, { username: 'nobody', root: me.rootHex, since: 0, nonce, proof: sign(d, me, { nonce, username: 'nobody' }) })
171 t.is(r.status, 404, 'an empty answer would read as "you have no likes"')
172})
173
174test('★★ the swarm door reaches the same room', async (t) => {
175 const d = await bridge(t)
176 const me = await held(d, 'alice')
177 d.observations.note({ recipient: 'alice', actor: 'https://prsm.space/users/bob', id: 'l1' })
178
179 const h = registerHandlers(d)
180 const { nonce } = await h.challenge()
181 const r = await h.collect({ username: 'alice', root: me.rootHex, since: 0, nonce, proof: sign(d, me, { nonce, username: 'alice' }) })
182 t.is(r.status, 200)
183 t.is(r.body.rows.length, 1)
184})
185
186// ⛔ A PROOF FOR ONE ACT MUST NOT VERIFY AS ANOTHER. This is the entire reason `collect` got its own
187// prefix rather than reusing CLAIM's. Releasing BURNS a name permanently and there is no way back, so
188// a routine poll whose proof could be repurposed as a release would be a peer handing strangers a
189// weapon every few minutes.
190test('⛔ a collect proof cannot be replayed as a release', async (t) => {
191 const d = await bridge(t)
192 const me = await held(d, 'alice')
193
194 const { nonce } = d.admission.challenge()
195 const proof = sign(d, me, { nonce, username: 'alice' })
196
197 // The nonce is still open — verifyCollect has not been called, so this is the best case for an
198 // attacker and it must still fail.
199 const r = d.admission.verifyRelease({ nonce, username: 'alice', root: me.rootHex, proof })
200 t.absent(r.ok, 'refused')
201 t.is(r.error, 'proof does not attest that identity', 'the bytes describe a different act')
202})
203
204// ⛔ THE WIRING, WHICH NO OTHER TEST CAN SEE. Every test above hands `observations` to the handlers
205// directly. If index.js forgets to, `admitCollect` returns 404 'not found' — the daemon boots, the
206// verb answers, the tests all pass, and every peer is told for ever that it has nothing. That is the
207// exact shape of the `held:false` defect registrations.js records, one door over.
208test('⛔ index.js hands the observation store to the swarm door', async (t) => {
209 const src = fs.readFileSync(new URL('../index.js', import.meta.url), 'utf8')
210 const call = /serveRegistration\(\{([^}]*)\}/.exec(src)
211 t.ok(call, 'the swarm door is opened at boot')
212 t.ok(/\bobservations\b/.test(call[1]), 'and it is given the buffer it is meant to serve')
213})