| … | … | @@ -1,9 +1,9 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "ap-bridge", |
| 3 | "version": "0.10.7", | |
| 3 | "version": "0.10.8", | |
| 4 | 4 | "runtime": "bare", |
| 5 | "rootTreeHash": "38ee12c24f843a8bb57fee2d1821ea985935b37a015214c1632d35361b101aa7", | |
| 6 | "publishedAt": "2026-09-15T16:24:17.618Z", | |
| 5 | "rootTreeHash": "a5f2dfc3a0fca68ec50c499d801ec68ccc8a7f6b158070d63826c29e44e55f9a", | |
| 6 | "publishedAt": "2026-09-15T16:28:31.063Z", | |
| 7 | 7 | "deps": [ |
| 8 | 8 | { |
| 9 | 9 | "name": "@hyperswarm/secret-stream", |
| … | … | @@ -4004,8 +4004,8 @@ |
| 4004 | 4004 | "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37" |
| 4005 | 4005 | } |
| 4006 | 4006 | ], |
| 4007 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4008 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4007 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4008 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4009 | 4009 | }, |
| 4010 | 4010 | { |
| 4011 | 4011 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4019,8 +4019,8 @@ |
| 4019 | 4019 | "tree": "ee91b6bd77101e3404ca571b2e5adf0f47029d174398a05f50294fc207e39d99" |
| 4020 | 4020 | } |
| 4021 | 4021 | ], |
| 4022 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4023 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4022 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4023 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4024 | 4024 | }, |
| 4025 | 4025 | { |
| 4026 | 4026 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4034,8 +4034,8 @@ |
| 4034 | 4034 | "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11" |
| 4035 | 4035 | } |
| 4036 | 4036 | ], |
| 4037 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4038 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4037 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4038 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4039 | 4039 | }, |
| 4040 | 4040 | { |
| 4041 | 4041 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4054,8 +4054,8 @@ |
| 4054 | 4054 | "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d" |
| 4055 | 4055 | } |
| 4056 | 4056 | ], |
| 4057 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4058 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4057 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4058 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4059 | 4059 | }, |
| 4060 | 4060 | { |
| 4061 | 4061 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4069,8 +4069,8 @@ |
| 4069 | 4069 | "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd" |
| 4070 | 4070 | } |
| 4071 | 4071 | ], |
| 4072 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4073 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4072 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4073 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4074 | 4074 | }, |
| 4075 | 4075 | { |
| 4076 | 4076 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4084,8 +4084,8 @@ |
| 4084 | 4084 | "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95" |
| 4085 | 4085 | } |
| 4086 | 4086 | ], |
| 4087 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4088 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4087 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4088 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4089 | 4089 | }, |
| 4090 | 4090 | { |
| 4091 | 4091 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4099,8 +4099,8 @@ |
| 4099 | 4099 | "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb" |
| 4100 | 4100 | } |
| 4101 | 4101 | ], |
| 4102 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4103 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4102 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4103 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4104 | 4104 | }, |
| 4105 | 4105 | { |
| 4106 | 4106 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4114,8 +4114,8 @@ |
| 4114 | 4114 | "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a" |
| 4115 | 4115 | } |
| 4116 | 4116 | ], |
| 4117 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4118 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4117 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4118 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4119 | 4119 | }, |
| 4120 | 4120 | { |
| 4121 | 4121 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4129,8 +4129,8 @@ |
| 4129 | 4129 | "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca" |
| 4130 | 4130 | } |
| 4131 | 4131 | ], |
| 4132 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4133 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4132 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4133 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4134 | 4134 | }, |
| 4135 | 4135 | { |
| 4136 | 4136 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4144,8 +4144,8 @@ |
| 4144 | 4144 | "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701" |
| 4145 | 4145 | } |
| 4146 | 4146 | ], |
| 4147 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4148 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4147 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4148 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4149 | 4149 | }, |
| 4150 | 4150 | { |
| 4151 | 4151 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4159,8 +4159,8 @@ |
| 4159 | 4159 | "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d" |
| 4160 | 4160 | } |
| 4161 | 4161 | ], |
| 4162 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4163 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4162 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4163 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4164 | 4164 | }, |
| 4165 | 4165 | { |
| 4166 | 4166 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4174,8 +4174,8 @@ |
| 4174 | 4174 | "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04" |
| 4175 | 4175 | } |
| 4176 | 4176 | ], |
| 4177 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4178 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4177 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4178 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4179 | 4179 | }, |
| 4180 | 4180 | { |
| 4181 | 4181 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4189,8 +4189,8 @@ |
| 4189 | 4189 | "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4" |
| 4190 | 4190 | } |
| 4191 | 4191 | ], |
| 4192 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4193 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4192 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4193 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4194 | 4194 | }, |
| 4195 | 4195 | { |
| 4196 | 4196 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4204,8 +4204,8 @@ |
| 4204 | 4204 | "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df" |
| 4205 | 4205 | } |
| 4206 | 4206 | ], |
| 4207 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4208 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4207 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4208 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4209 | 4209 | }, |
| 4210 | 4210 | { |
| 4211 | 4211 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4219,8 +4219,8 @@ |
| 4219 | 4219 | "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad" |
| 4220 | 4220 | } |
| 4221 | 4221 | ], |
| 4222 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4223 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4222 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4223 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4224 | 4224 | }, |
| 4225 | 4225 | { |
| 4226 | 4226 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4234,8 +4234,8 @@ |
| 4234 | 4234 | "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e" |
| 4235 | 4235 | } |
| 4236 | 4236 | ], |
| 4237 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4238 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4237 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4238 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4239 | 4239 | }, |
| 4240 | 4240 | { |
| 4241 | 4241 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4264,8 +4264,8 @@ |
| 4264 | 4264 | "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c" |
| 4265 | 4265 | } |
| 4266 | 4266 | ], |
| 4267 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4268 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4267 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4268 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4269 | 4269 | }, |
| 4270 | 4270 | { |
| 4271 | 4271 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4279,8 +4279,8 @@ |
| 4279 | 4279 | "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9" |
| 4280 | 4280 | } |
| 4281 | 4281 | ], |
| 4282 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4283 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4282 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4283 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4284 | 4284 | }, |
| 4285 | 4285 | { |
| 4286 | 4286 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4294,8 +4294,8 @@ |
| 4294 | 4294 | "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169" |
| 4295 | 4295 | } |
| 4296 | 4296 | ], |
| 4297 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4298 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4297 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4298 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4299 | 4299 | }, |
| 4300 | 4300 | { |
| 4301 | 4301 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4309,8 +4309,8 @@ |
| 4309 | 4309 | "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b" |
| 4310 | 4310 | } |
| 4311 | 4311 | ], |
| 4312 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4313 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4312 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4313 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4314 | 4314 | }, |
| 4315 | 4315 | { |
| 4316 | 4316 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4324,8 +4324,8 @@ |
| 4324 | 4324 | "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed" |
| 4325 | 4325 | } |
| 4326 | 4326 | ], |
| 4327 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4328 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4327 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4328 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4329 | 4329 | }, |
| 4330 | 4330 | { |
| 4331 | 4331 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4339,8 +4339,8 @@ |
| 4339 | 4339 | "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f" |
| 4340 | 4340 | } |
| 4341 | 4341 | ], |
| 4342 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4343 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4342 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4343 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4344 | 4344 | }, |
| 4345 | 4345 | { |
| 4346 | 4346 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4354,8 +4354,8 @@ |
| 4354 | 4354 | "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754" |
| 4355 | 4355 | } |
| 4356 | 4356 | ], |
| 4357 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4358 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4357 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4358 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4359 | 4359 | }, |
| 4360 | 4360 | { |
| 4361 | 4361 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4369,8 +4369,8 @@ |
| 4369 | 4369 | "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5" |
| 4370 | 4370 | } |
| 4371 | 4371 | ], |
| 4372 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4373 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4372 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4373 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4374 | 4374 | }, |
| 4375 | 4375 | { |
| 4376 | 4376 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4389,8 +4389,8 @@ |
| 4389 | 4389 | "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393" |
| 4390 | 4390 | } |
| 4391 | 4391 | ], |
| 4392 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4393 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4392 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4393 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4394 | 4394 | }, |
| 4395 | 4395 | { |
| 4396 | 4396 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4404,8 +4404,8 @@ |
| 4404 | 4404 | "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5" |
| 4405 | 4405 | } |
| 4406 | 4406 | ], |
| 4407 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4408 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4407 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4408 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4409 | 4409 | }, |
| 4410 | 4410 | { |
| 4411 | 4411 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4419,8 +4419,8 @@ |
| 4419 | 4419 | "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6" |
| 4420 | 4420 | } |
| 4421 | 4421 | ], |
| 4422 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4423 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4422 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4423 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4424 | 4424 | }, |
| 4425 | 4425 | { |
| 4426 | 4426 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4434,8 +4434,8 @@ |
| 4434 | 4434 | "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9" |
| 4435 | 4435 | } |
| 4436 | 4436 | ], |
| 4437 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4438 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4437 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4438 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4439 | 4439 | }, |
| 4440 | 4440 | { |
| 4441 | 4441 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4449,8 +4449,8 @@ |
| 4449 | 4449 | "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0" |
| 4450 | 4450 | } |
| 4451 | 4451 | ], |
| 4452 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4453 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4452 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4453 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4454 | 4454 | }, |
| 4455 | 4455 | { |
| 4456 | 4456 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4464,8 +4464,8 @@ |
| 4464 | 4464 | "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53" |
| 4465 | 4465 | } |
| 4466 | 4466 | ], |
| 4467 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4468 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4467 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4468 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4469 | 4469 | }, |
| 4470 | 4470 | { |
| 4471 | 4471 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4479,8 +4479,8 @@ |
| 4479 | 4479 | "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a" |
| 4480 | 4480 | } |
| 4481 | 4481 | ], |
| 4482 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4483 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4482 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4483 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4484 | 4484 | }, |
| 4485 | 4485 | { |
| 4486 | 4486 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4499,8 +4499,8 @@ |
| 4499 | 4499 | "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648" |
| 4500 | 4500 | } |
| 4501 | 4501 | ], |
| 4502 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4503 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4502 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4503 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4504 | 4504 | }, |
| 4505 | 4505 | { |
| 4506 | 4506 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4514,8 +4514,8 @@ |
| 4514 | 4514 | "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57" |
| 4515 | 4515 | } |
| 4516 | 4516 | ], |
| 4517 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4518 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4517 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4518 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4519 | 4519 | }, |
| 4520 | 4520 | { |
| 4521 | 4521 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4529,8 +4529,8 @@ |
| 4529 | 4529 | "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78" |
| 4530 | 4530 | } |
| 4531 | 4531 | ], |
| 4532 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4533 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4532 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4533 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4534 | 4534 | }, |
| 4535 | 4535 | { |
| 4536 | 4536 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4544,8 +4544,8 @@ |
| 4544 | 4544 | "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9" |
| 4545 | 4545 | } |
| 4546 | 4546 | ], |
| 4547 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4548 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4547 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4548 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4549 | 4549 | }, |
| 4550 | 4550 | { |
| 4551 | 4551 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4559,8 +4559,8 @@ |
| 4559 | 4559 | "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972" |
| 4560 | 4560 | } |
| 4561 | 4561 | ], |
| 4562 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4563 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4562 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4563 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4564 | 4564 | }, |
| 4565 | 4565 | { |
| 4566 | 4566 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4574,8 +4574,8 @@ |
| 4574 | 4574 | "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519" |
| 4575 | 4575 | } |
| 4576 | 4576 | ], |
| 4577 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4578 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4577 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4578 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4579 | 4579 | }, |
| 4580 | 4580 | { |
| 4581 | 4581 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4589,8 +4589,8 @@ |
| 4589 | 4589 | "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390" |
| 4590 | 4590 | } |
| 4591 | 4591 | ], |
| 4592 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4593 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4592 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4593 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4594 | 4594 | }, |
| 4595 | 4595 | { |
| 4596 | 4596 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4604,8 +4604,8 @@ |
| 4604 | 4604 | "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706" |
| 4605 | 4605 | } |
| 4606 | 4606 | ], |
| 4607 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4608 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4607 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4608 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4609 | 4609 | }, |
| 4610 | 4610 | { |
| 4611 | 4611 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4619,8 +4619,8 @@ |
| 4619 | 4619 | "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb" |
| 4620 | 4620 | } |
| 4621 | 4621 | ], |
| 4622 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4623 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4622 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4623 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4624 | 4624 | }, |
| 4625 | 4625 | { |
| 4626 | 4626 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4634,8 +4634,8 @@ |
| 4634 | 4634 | "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae" |
| 4635 | 4635 | } |
| 4636 | 4636 | ], |
| 4637 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4638 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4637 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4638 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4639 | 4639 | }, |
| 4640 | 4640 | { |
| 4641 | 4641 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4654,8 +4654,8 @@ |
| 4654 | 4654 | "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765" |
| 4655 | 4655 | } |
| 4656 | 4656 | ], |
| 4657 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4658 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4657 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4658 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4659 | 4659 | }, |
| 4660 | 4660 | { |
| 4661 | 4661 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4669,8 +4669,8 @@ |
| 4669 | 4669 | "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60" |
| 4670 | 4670 | } |
| 4671 | 4671 | ], |
| 4672 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4673 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4672 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4673 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4674 | 4674 | }, |
| 4675 | 4675 | { |
| 4676 | 4676 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4684,8 +4684,8 @@ |
| 4684 | 4684 | "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a" |
| 4685 | 4685 | } |
| 4686 | 4686 | ], |
| 4687 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4688 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4687 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4688 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4689 | 4689 | }, |
| 4690 | 4690 | { |
| 4691 | 4691 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4699,8 +4699,8 @@ |
| 4699 | 4699 | "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b" |
| 4700 | 4700 | } |
| 4701 | 4701 | ], |
| 4702 | "reason": "0.10.7: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4703 | "acceptedAt": "2026-09-15T16:24:17.637Z" | |
| 4702 | "reason": "0.10.8: same 45 version-splits/docs/packaging, 0 code-differs", | |
| 4703 | "acceptedAt": "2026-09-15T16:28:31.080Z" | |
| 4704 | 4704 | } |
| 4705 | 4705 | ] |
| 4706 | 4706 | } |
| … | … | @@ -16,6 +16,7 @@ import { createServer } from './lib/server.js' |
| 16 | 16 | import { actorDocFor } from './lib/router.js' |
| 17 | 17 | import { FollowerStore } from './lib/followers.js' |
| 18 | 18 | import { InboundStore } from './lib/inbound-store.js' |
| 19 | import { ObservationStore } from './lib/observations.js' | |
| 19 | 20 | import { RemoteKeyResolver } from './lib/remote-key.js' |
| 20 | 21 | import { createDeliverer } from './lib/deliver.js' |
| 21 | 22 | import { LocalSigner } from './lib/signer.js' |
| … | … | @@ -80,6 +81,11 @@ async function main () { |
| 80 | 81 | // ⛔ THE CAP IS THE OPERATOR'S. It was fixed at 500 here, and for the fediverse an eviction is not a |
| 81 | 82 | // cache miss — there is no backlink index to re-ask, so it can be the only copy. See config.js. |
| 82 | 83 | const inbound = new InboundStore({ file: join(dataDir, 'inbound.json'), cap: config.inboundCap }) |
| 84 | // ⛔ THE DURABLE HALF OF THE SAME ARRIVAL. `inbound` is a capped timeline for THIS bridge's pages; | |
| 85 | // an observation is what a registered PEER collects and keeps. Both are written from one inbox POST | |
| 86 | // and they have different lifetimes, which is why they are two stores rather than one with a flag — | |
| 87 | // see hyper-social/docs/OBSERVED-INTERACTIONS.md §9. | |
| 88 | const observations = new ObservationStore({ path: join(dataDir, 'observations.jsonl') }) | |
| 83 | 89 | const resolver = new RemoteKeyResolver() |
| 84 | 90 | |
| 85 | 91 | // ⛔ CUSTODY IS NAMED HERE, not implied by passing a key around. This is the primary actor and it |
| … | … | @@ -146,6 +152,7 @@ async function main () { |
| 146 | 152 | publicKeyPem, |
| 147 | 153 | followers, |
| 148 | 154 | inbound, |
| 155 | observations, | |
| 149 | 156 | resolvePublicKey: (kid) => resolver.resolve(kid), |
| 150 | 157 | deliver, |
| 151 | 158 | // Activity ids must be unique and stable enough that a receiver can dedup. Timestamp+counter |
| … | … | @@ -0,0 +1,138 @@ |
| 1 | // observations.js — what other people did to a peer's records, held until that peer collects it. | |
| 2 | // | |
| 3 | // ⛔ AN OBSERVATION IS NOT AN EDGE, and the distinction is the whole design. An edge a favela peer | |
| 4 | // authors is a record in THEIR feed and needs nothing here. A like from a Mastodon or Bluesky account | |
| 5 | // is not ours to author: writing one into somebody's feed would be authoring as them, which | |
| 6 | // hyper-social #61 §4 removed the room primitive to avoid. So what this holds is weaker and honest — | |
| 7 | // *we observed that this actor did this to this record of yours, at this time*. | |
| 8 | // | |
| 9 | // ⛔ THIS BRIDGE ACCUMULATES AND SERVES. THE PEER ASKS AND STORES. We hold a registered peer's feed | |
| 10 | // and are NOT a writer on it, so we never write their pointer or their segments. This is a buffer | |
| 11 | // that gets drained, not an archive — see OBSERVED-INTERACTIONS.md §9. | |
| 12 | // | |
| 13 | // ⚠ WHICH IS WHY THE CODEC IS NOT HERE. `favela-web/lib/interaction-codec.js` packs 50,000 | |
| 14 | // interactions into 0.50 MB by interning actors and delta-encoding time, and that earns its place | |
| 15 | // where data is kept for ever — on the PEER. Compressing a buffer that is drained and pruned would be | |
| 16 | // symmetry for its own sake, and it would make this file harder to read at a debugger. | |
| 17 | // | |
| 18 | // ⚠ M1 SCOPE: append-only JSONL, matching followers.js and inbound-store.js rather than inventing a | |
| 19 | // third storage pattern for the same lifetime of data. A row is ~120 bytes and this is a serving | |
| 20 | // buffer, so the size that matters is bounded by the cap below, not by the network's enthusiasm. | |
| 21 | // | |
| 22 | // ⛔ SCOPED BY RECIPIENT, ALWAYS. One store serves every registered identity on this bridge, and an | |
| 23 | // observation belongs to exactly one of them. `recipient` is the field added to the inbound store on | |
| 24 | // 2026-09-15 for precisely this reason: without it a per-peer read is the only thing standing between | |
| 25 | // one identity's social graph and another's, and that is too much weight for a transport check. | |
| 26 | ||
| 27 | import fs from 'bare-fs' | |
| 28 | import { dirname, join } from 'bare-path' | |
| 29 | ||
| 30 | // Kinds, as small integers because they are written once per row. Tombstones are their own kind | |
| 31 | // rather than a flag: `interaction.md` §4.3 makes reversal positive evidence everywhere else, because | |
| 32 | // absence is ambiguous in a replicated log and "they un-liked" must not read as "we never saw it". | |
| 33 | export const KIND = { LIKE: 0, REPOST: 1, FOLLOW: 2, REPLY: 3, UNDO: 4 } | |
| 34 | ||
| 35 | // Bounded, for the same reason inbound-store.js is: whatever we accept, somebody else decides how | |
| 36 | // much of it there is. A serving buffer that grows without limit is a disk-exhaustion bug with a | |
| 37 | // signature check in front of it. | |
| 38 | export const DEFAULT_CAP = 20_000 | |
| 39 | ||
| 40 | export class ObservationStore { | |
| 41 | constructor ({ path, cap = DEFAULT_CAP } = {}) { | |
| 42 | if (!path) throw new Error('ObservationStore: a path is required') | |
| 43 | this._path = path | |
| 44 | this._cap = Math.max(1, cap) | |
| 45 | this._rows = null | |
| 46 | this._seq = 0 | |
| 47 | } | |
| 48 | ||
| 49 | _load () { | |
| 50 | if (this._rows) return | |
| 51 | this._rows = [] | |
| 52 | let raw = '' | |
| 53 | try { raw = fs.readFileSync(this._path, 'utf8') } catch { raw = '' } | |
| 54 | for (const line of raw.split('\n')) { | |
| 55 | if (!line.trim()) continue | |
| 56 | // ⚠ A single corrupt line must not cost the whole file. An append-only log written by a process | |
| 57 | // that can be killed at any moment will eventually have a torn tail. | |
| 58 | try { | |
| 59 | const row = JSON.parse(line) | |
| 60 | if (row && typeof row.seq === 'number') { | |
| 61 | this._rows.push(row) | |
| 62 | if (row.seq > this._seq) this._seq = row.seq | |
| 63 | } | |
| 64 | } catch { /* torn or partial — skip it, keep the rest */ } | |
| 65 | } | |
| 66 | } | |
| 67 | ||
| 68 | _flush () { | |
| 69 | try { | |
| 70 | fs.mkdirSync(dirname(this._path), { recursive: true }) | |
| 71 | fs.writeFileSync(this._path, this._rows.map((r) => JSON.stringify(r)).join('\n') + '\n') | |
| 72 | } catch { /* a buffer that cannot be persisted still serves from memory this run */ } | |
| 73 | } | |
| 74 | ||
| 75 | /** | |
| 76 | * Record one observation. Returns the row, or null when it names nothing we can serve. | |
| 77 | * | |
| 78 | * ⛔ REFUSED RATHER THAN STORED when there is no recipient or no actor: a row nobody can be given | |
| 79 | * and nobody can be attributed to is not a weaker observation, it is noise that will be read later | |
| 80 | * as data. | |
| 81 | */ | |
| 82 | note ({ recipient, subject = null, actor, kind = KIND.LIKE, at = Date.now(), id = null } = {}) { | |
| 83 | if (typeof recipient !== 'string' || !recipient) return null | |
| 84 | if (typeof actor !== 'string' || !actor) return null | |
| 85 | this._load() | |
| 86 | ||
| 87 | // ⚠ DEDUPED BY id WHEN THERE IS ONE. A remote server retrying delivery is not a second like, and | |
| 88 | // the inbox already treats a repeat as a duplicate rather than an event. | |
| 89 | if (id && this._rows.some((r) => r.id === id)) return null | |
| 90 | ||
| 91 | const row = { | |
| 92 | seq: ++this._seq, | |
| 93 | recipient, | |
| 94 | subject, | |
| 95 | actor, | |
| 96 | kind: (kind | 0), | |
| 97 | at: Number(at) || Date.now(), | |
| 98 | id: id || null | |
| 99 | } | |
| 100 | this._rows.push(row) | |
| 101 | // Oldest first out: a peer that has been away for longer than the cap re-collects from whatever | |
| 102 | // survives, and the alternative — refusing new rows when full — loses the ones we just learned. | |
| 103 | if (this._rows.length > this._cap) this._rows = this._rows.slice(this._rows.length - this._cap) | |
| 104 | this._flush() | |
| 105 | return row | |
| 106 | } | |
| 107 | ||
| 108 | /** | |
| 109 | * What one peer has not collected yet. | |
| 110 | * | |
| 111 | * ⛔ `recipient` IS REQUIRED and there is deliberately no "everything" call. A store that could | |
| 112 | * return every identity's observations would make the door's authorisation the only thing standing | |
| 113 | * between one peer's social graph and another's. | |
| 114 | */ | |
| 115 | list ({ recipient, since = 0, limit = 500 } = {}) { | |
| 116 | if (typeof recipient !== 'string' || !recipient) return { rows: [], cursor: since || 0, more: false } | |
| 117 | this._load() | |
| 118 | const after = Number(since) || 0 | |
| 119 | const mine = this._rows.filter((r) => r.recipient === recipient && r.seq > after) | |
| 120 | const n = Math.max(1, Math.min(Number(limit) || 500, 1000)) | |
| 121 | const rows = mine.slice(0, n) | |
| 122 | return { | |
| 123 | rows, | |
| 124 | // ⚠ The cursor advances only over rows actually returned. Advancing it past a page we did not | |
| 125 | // send would silently skip observations, which is the failure this whole area keeps producing. | |
| 126 | cursor: rows.length ? rows[rows.length - 1].seq : after, | |
| 127 | more: mine.length > rows.length | |
| 128 | } | |
| 129 | } | |
| 130 | ||
| 131 | /** How many are held for one peer — the cheap answer, with no rows moved. */ | |
| 132 | count ({ recipient } = {}) { | |
| 133 | this._load() | |
| 134 | return this._rows.filter((r) => !recipient || r.recipient === recipient).length | |
| 135 | } | |
| 136 | } | |
| 137 | ||
| 138 | export default { ObservationStore, KIND, DEFAULT_CAP } |
| … | … | @@ -14,6 +14,7 @@ import { |
| 14 | 14 | } from './actor.js' |
| 15 | 15 | import { verifyRequest } from './http-signature.js' |
| 16 | 16 | import { normalize } from './inbound-store.js' |
| 17 | import { KIND as OBS } from './observations.js' | |
| 17 | 18 | |
| 18 | 19 | const json = (status, doc, type = 'application/json') => ({ |
| 19 | 20 | status, |
| … | … | @@ -520,6 +521,18 @@ async function onFollow (activity, actorUri, ctx) { |
| 520 | 521 | |
| 521 | 522 | const added = await ctx.followers.add(actorUri) |
| 522 | 523 | |
| 524 | // ⛔ AND IT IS AN OBSERVATION. A Follow is dispatched HERE, never through onInbound, so noting it | |
| 525 | // there alone recorded likes and boosts and silently lost every follow — caught by the test that | |
| 526 | // asserts all three land, which is why that test names all three rather than one. | |
| 527 | if (ctx.observations) { | |
| 528 | const recipient = (ctx.user && ctx.user.username) || (ctx.config && ctx.config.username) || null | |
| 529 | if (recipient) { | |
| 530 | try { | |
| 531 | ctx.observations.note({ recipient, subject: null, actor: actorUri, kind: OBS.FOLLOW, at: Date.now(), id: activity.id }) | |
| 532 | } catch (err) { ctx.log?.({ msg: 'inbox: follow not recorded', actor: actorUri, error: err.message }) } | |
| 533 | } | |
| 534 | } | |
| 535 | ||
| 523 | 536 | // ⛔ A NEW FOLLOWER IS A REASON TO DRAIN. Posts made while nobody was following are HELD rather |
| 524 | 537 | // than recorded as delivered (publisher.js `_send`), which only means something if somebody |
| 525 | 538 | // eventually goes and sends them. Without this the held posts sit until an unrelated future post |
| … | … | @@ -567,6 +580,13 @@ async function onInbound (activity, actorUri, ctx) { |
| 567 | 580 | const recipient = (ctx.user && ctx.user.username) || (ctx.config && ctx.config.username) || null |
| 568 | 581 | const item = normalize(activity, Date.now(), recipient) |
| 569 | 582 | const added = await ctx.inbound.add(item) |
| 583 | // ⛔ AND AS AN OBSERVATION, WHICH IS THE DURABLE HALF. The inbound store is a capped timeline for | |
| 584 | // this bridge's own pages; an observation is what a PEER collects and keeps. Same arrival, two | |
| 585 | // consumers with different lifetimes — see OBSERVED-INTERACTIONS.md §9. | |
| 586 | // | |
| 587 | // ⚠ NEVER FAILS THE POST. We owe a prompt 202; a storage problem is ours, and a 5xx here makes the | |
| 588 | // sender retry for ever and eventually back off delivery to us entirely. | |
| 589 | if (added) noteObservation(activity, actorUri, recipient, item, ctx) | |
| 570 | 590 | // A retry is not a second event; say which happened so a duplicate storm is visible. |
| 571 | 591 | ctx.log?.({ msg: added ? 'inbox: kept' : 'inbox: duplicate', type: activity.type, actor: actorUri, inReplyTo: item.inReplyTo || undefined }) |
| 572 | 592 | } catch (err) { |
| … | … | @@ -591,6 +611,29 @@ async function onDelete (activity, actorUri, ctx) { |
| 591 | 611 | return { status: 202, headers: {}, body: '' } |
| 592 | 612 | } |
| 593 | 613 | |
| 614 | // One inbound activity → one observation, or none. | |
| 615 | // | |
| 616 | // ⛔ ONLY WHAT WE HAVE WORDS FOR. An activity type with no kind is not recorded as the nearest | |
| 617 | // familiar one — a row read later as data has to mean what it says. | |
| 618 | function noteObservation (activity, actorUri, recipient, item, ctx) { | |
| 619 | if (!ctx.observations || !recipient || !actorUri) return | |
| 620 | const type = activity && activity.type | |
| 621 | let kind = null | |
| 622 | let subject = null | |
| 623 | ||
| 624 | if (type === 'Like') { kind = OBS.LIKE; subject = item.objectId } | |
| 625 | else if (type === 'Announce') { kind = OBS.REPOST; subject = item.objectId } | |
| 626 | else if (type === 'Follow') { kind = OBS.FOLLOW; subject = null } | |
| 627 | else if ((type === 'Create' || type === 'Update') && item.inReplyTo) { kind = OBS.REPLY; subject = item.inReplyTo } | |
| 628 | if (kind === null) return | |
| 629 | ||
| 630 | try { | |
| 631 | ctx.observations.note({ recipient, subject: subject || null, actor: actorUri, kind, at: Date.parse(item.published || '') || Date.now(), id: item.id }) | |
| 632 | } catch (err) { | |
| 633 | ctx.log?.({ msg: 'inbox: observation not recorded', type, actor: actorUri, error: err.message }) | |
| 634 | } | |
| 635 | } | |
| 636 | ||
| 594 | 637 | async function onUndo (activity, actorUri, ctx) { |
| 595 | 638 | const inner = activity.object |
| 596 | 639 | const innerType = typeof inner === 'object' ? inner?.type : null |
| … | … | @@ -598,6 +641,22 @@ async function onUndo (activity, actorUri, ctx) { |
| 598 | 641 | await ctx.followers.remove(actorUri) |
| 599 | 642 | ctx.log?.({ msg: 'inbox: unfollowed', actor: actorUri }) |
| 600 | 643 | } |
| 644 | // ⛔ A RETRACTION IS POSITIVE EVIDENCE, never an absence. interaction.md §4.3: absence is ambiguous | |
| 645 | // in a replicated log, so "they un-liked" must not be indistinguishable from "we never saw it". | |
| 646 | // The reader decides what a later UNDO means; the store does not silently forget the earlier row. | |
| 647 | const recipient = (ctx.user && ctx.user.username) || (ctx.config && ctx.config.username) || null | |
| 648 | if (ctx.observations && recipient && actorUri) { | |
| 649 | try { | |
| 650 | ctx.observations.note({ | |
| 651 | recipient, | |
| 652 | subject: typeof inner === 'object' ? (inner.object && (inner.object.id || inner.object)) || null : null, | |
| 653 | actor: actorUri, | |
| 654 | kind: OBS.UNDO, | |
| 655 | at: Date.now(), | |
| 656 | id: activity && activity.id ? activity.id : null | |
| 657 | }) | |
| 658 | } catch (err) { ctx.log?.({ msg: 'inbox: undo not recorded', actor: actorUri, error: err.message }) } | |
| 659 | } | |
| 601 | 660 | return { status: 202, headers: {}, body: '' } |
| 602 | 661 | } |
| 603 | 662 |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "ap-bridge", |
| 3 | "version": "0.10.8", | |
| 3 | "version": "0.10.9", | |
| 4 | 4 | "description": "ActivityPub membrane \u2014 a second projection of the self-custody atproto peer", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -0,0 +1,110 @@ |
| 1 | // Observations — held until the peer collects them, and never mixed between peers. | |
| 2 | // | |
| 3 | // ⛔ THE ISOLATION IS THE SAFETY PROPERTY, exactly as it is for the inbound store. One bridge serves | |
| 4 | // every registered identity, and an observation belongs to precisely one of them. A read that could | |
| 5 | // return another peer's rows would put the door's authorisation between one person's social graph and | |
| 6 | // another's, which is too much weight for a transport check to carry. | |
| 7 | ||
| 8 | import test from 'brittle' | |
| 9 | import fs from 'bare-fs' | |
| 10 | import os from 'bare-os' | |
| 11 | import { join } from 'bare-path' | |
| 12 | import { ObservationStore, KIND } from '../lib/observations.js' | |
| 13 | ||
| 14 | const tmp = (t) => { | |
| 15 | const d = fs.mkdtempSync(join(os.tmpdir(), 'obs-')) | |
| 16 | t.teardown(() => { try { fs.rmSync(d, { recursive: true, force: true }) } catch {} }) | |
| 17 | return join(d, 'observations.jsonl') | |
| 18 | } | |
| 19 | ||
| 20 | const ALICE = 'https://mastodon.social/users/alice' | |
| 21 | const BOB = 'did:plc:wj62ebbusln6gxo5azo6ogzo' | |
| 22 | ||
| 23 | test('an observation is recorded and read back by its recipient', (t) => { | |
| 24 | const s = new ObservationStore({ path: tmp(t) }) | |
| 25 | s.note({ recipient: 'fleeky', subject: 'https://ap.7rnx.net/ap/objects/1', actor: ALICE, kind: KIND.LIKE, at: 1000 }) | |
| 26 | ||
| 27 | const r = s.list({ recipient: 'fleeky' }) | |
| 28 | t.is(r.rows.length, 1) | |
| 29 | t.is(r.rows[0].actor, ALICE) | |
| 30 | t.is(r.rows[0].kind, KIND.LIKE) | |
| 31 | t.is(r.cursor, 1, 'the cursor is the seq of the last row returned') | |
| 32 | }) | |
| 33 | ||
| 34 | test('★★★ one peer never sees another peer\'s observations', (t) => { | |
| 35 | const s = new ObservationStore({ path: tmp(t) }) | |
| 36 | s.note({ recipient: 'fleeky', subject: 'o1', actor: ALICE, at: 1 }) | |
| 37 | s.note({ recipient: 'ashatron', subject: 'o2', actor: BOB, at: 2 }) | |
| 38 | ||
| 39 | t.is(s.list({ recipient: 'fleeky' }).rows.length, 1) | |
| 40 | t.is(s.list({ recipient: 'ashatron' }).rows[0].actor, BOB) | |
| 41 | // ⛔ And there is no call that returns both. | |
| 42 | t.is(s.list({}).rows.length, 0, 'a read with no recipient returns nothing rather than everything') | |
| 43 | }) | |
| 44 | ||
| 45 | test('★★ the cursor advances only over rows actually returned', (t) => { | |
| 46 | const s = new ObservationStore({ path: tmp(t) }) | |
| 47 | for (let i = 0; i < 5; i++) s.note({ recipient: 'fleeky', subject: 'o', actor: ALICE + i, at: i }) | |
| 48 | ||
| 49 | const first = s.list({ recipient: 'fleeky', limit: 2 }) | |
| 50 | t.is(first.rows.length, 2) | |
| 51 | t.ok(first.more, 'it says there is more') | |
| 52 | const second = s.list({ recipient: 'fleeky', since: first.cursor, limit: 2 }) | |
| 53 | t.is(second.rows[0].actor, ALICE + '2', 'resumes exactly where the last page ended — no gap, no repeat') | |
| 54 | const rest = s.list({ recipient: 'fleeky', since: second.cursor }) | |
| 55 | t.is(rest.rows.length, 1) | |
| 56 | t.absent(rest.more) | |
| 57 | }) | |
| 58 | ||
| 59 | test('⛔ a retry is not a second like', (t) => { | |
| 60 | const s = new ObservationStore({ path: tmp(t) }) | |
| 61 | const a = s.note({ recipient: 'fleeky', subject: 'o', actor: ALICE, id: 'act-1', at: 1 }) | |
| 62 | const b = s.note({ recipient: 'fleeky', subject: 'o', actor: ALICE, id: 'act-1', at: 1 }) | |
| 63 | t.ok(a) | |
| 64 | t.is(b, null, 'the duplicate is refused') | |
| 65 | t.is(s.count({ recipient: 'fleeky' }), 1) | |
| 66 | }) | |
| 67 | ||
| 68 | test('⛔ a row nobody can be given, or attributed to, is refused', (t) => { | |
| 69 | const s = new ObservationStore({ path: tmp(t) }) | |
| 70 | // Noise that will be read later as data is worse than a missing row. | |
| 71 | t.is(s.note({ subject: 'o', actor: ALICE }), null, 'no recipient') | |
| 72 | t.is(s.note({ recipient: 'fleeky', subject: 'o' }), null, 'no actor') | |
| 73 | t.is(s.count({}), 0) | |
| 74 | }) | |
| 75 | ||
| 76 | test('an undo is its own kind, not an absence', (t) => { | |
| 77 | const s = new ObservationStore({ path: tmp(t) }) | |
| 78 | s.note({ recipient: 'fleeky', subject: 'o', actor: ALICE, kind: KIND.LIKE, at: 1 }) | |
| 79 | s.note({ recipient: 'fleeky', subject: 'o', actor: ALICE, kind: KIND.UNDO, at: 2 }) | |
| 80 | const rows = s.list({ recipient: 'fleeky' }).rows | |
| 81 | // ⛔ Absence is ambiguous in a replicated log, so a retraction is positive evidence | |
| 82 | // (interaction.md §4.3) — the reader decides, the store does not silently forget. | |
| 83 | t.alike(rows.map((r) => r.kind), [KIND.LIKE, KIND.UNDO]) | |
| 84 | }) | |
| 85 | ||
| 86 | test('⚠ bounded, oldest out — a full buffer must not refuse what we just learned', (t) => { | |
| 87 | const s = new ObservationStore({ path: tmp(t), cap: 3 }) | |
| 88 | for (let i = 0; i < 5; i++) s.note({ recipient: 'fleeky', subject: 'o', actor: ALICE + i, at: i }) | |
| 89 | const rows = s.list({ recipient: 'fleeky' }).rows | |
| 90 | t.is(rows.length, 3) | |
| 91 | t.is(rows[0].actor, ALICE + '2', 'the oldest were dropped, not the newest refused') | |
| 92 | }) | |
| 93 | ||
| 94 | test('it survives a restart, and a torn tail does not cost the file', (t) => { | |
| 95 | const path = tmp(t) | |
| 96 | const s = new ObservationStore({ path }) | |
| 97 | s.note({ recipient: 'fleeky', subject: 'o', actor: ALICE, at: 1 }) | |
| 98 | s.note({ recipient: 'fleeky', subject: 'o', actor: BOB, at: 2 }) | |
| 99 | ||
| 100 | // A process killed mid-append leaves half a line. An append-only log will eventually have one. | |
| 101 | fs.appendFileSync(path, '{"seq":99,"recipient":"fle') | |
| 102 | ||
| 103 | const reopened = new ObservationStore({ path }) | |
| 104 | const rows = reopened.list({ recipient: 'fleeky' }).rows | |
| 105 | t.is(rows.length, 2, 'the intact rows survive') | |
| 106 | t.is(rows[1].actor, BOB) | |
| 107 | // And the next write continues the sequence rather than colliding with what was read. | |
| 108 | const next = reopened.note({ recipient: 'fleeky', subject: 'o', actor: 'x', at: 3 }) | |
| 109 | t.is(next.seq, 3) | |
| 110 | }) |
| … | … | @@ -495,3 +495,65 @@ test('★★ /timeline is the COMBINED view — every peer this bridge serves, i |
| 495 | 495 | t.ok(/from p2p/.test(mine.body)) |
| 496 | 496 | t.absent(/from fleeky/.test(mine.body), "p2p's page is p2p's alone") |
| 497 | 497 | }) |
| 498 | ||
| 499 | // ── inbound interactions become observations ────────────────────────────────────────────────────── | |
| 500 | // | |
| 501 | // ⛔ The inbound store is a CAPPED timeline for this bridge's own pages. An observation is what a PEER | |
| 502 | // collects and keeps. Same arrival, two consumers with different lifetimes — and the durable half is | |
| 503 | // the one that was missing, which is why a like from a stranger was invisible on a handset. | |
| 504 | ||
| 505 | test('★★★ a Like, an Announce and a Follow each land as an observation, scoped to the recipient', async (t) => { | |
| 506 | const { ObservationStore, KIND } = await import('../lib/observations.js') | |
| 507 | const fs = await import('bare-fs') | |
| 508 | const os = await import('bare-os') | |
| 509 | const { join } = await import('bare-path') | |
| 510 | const dir = fs.mkdtempSync(join(os.tmpdir(), 'obs-router-')) | |
| 511 | t.teardown(() => { try { fs.rmSync(dir, { recursive: true, force: true }) } catch {} }) | |
| 512 | ||
| 513 | const observations = new ObservationStore({ path: join(dir, 'o.jsonl') }) | |
| 514 | const ctx = makeCtx({ inbound: new MemoryInboundStore(), observations }) | |
| 515 | ||
| 516 | const act = (type, over = {}) => ({ | |
| 517 | '@context': 'https://www.w3.org/ns/activitystreams', | |
| 518 | id: `${REMOTE}/a/${type}`, type, actor: REMOTE, | |
| 519 | object: 'https://p2p.7rnx.net/ap/objects/1', ...over | |
| 520 | }) | |
| 521 | await handleRequest(signedActivity(act('Like')), ctx) | |
| 522 | await handleRequest(signedActivity(act('Announce', { id: `${REMOTE}/a/Announce2` })), ctx) | |
| 523 | await handleRequest(signedActivity(act('Follow', { id: `${REMOTE}/a/Follow2` })), ctx) | |
| 524 | ||
| 525 | const rows = observations.list({ recipient: 'p2p' }).rows | |
| 526 | t.alike(rows.map((r) => r.kind), [KIND.LIKE, KIND.REPOST, KIND.FOLLOW], 'all three, in words we have') | |
| 527 | t.is(rows[0].actor, REMOTE, 'attributed to who did it') | |
| 528 | t.is(rows[2].subject, null, 'a follow acts on you, not on a post') | |
| 529 | // ⛔ And nobody else's page can read them. | |
| 530 | t.is(observations.list({ recipient: 'someone-else' }).rows.length, 0) | |
| 531 | }) | |
| 532 | ||
| 533 | test('⛔ an activity type we have no kind for is NOT recorded as the nearest one', async (t) => { | |
| 534 | const { ObservationStore } = await import('../lib/observations.js') | |
| 535 | const fs = await import('bare-fs'); const os = await import('bare-os'); const { join } = await import('bare-path') | |
| 536 | const dir = fs.mkdtempSync(join(os.tmpdir(), 'obs-router2-')) | |
| 537 | t.teardown(() => { try { fs.rmSync(dir, { recursive: true, force: true }) } catch {} }) | |
| 538 | ||
| 539 | const observations = new ObservationStore({ path: join(dir, 'o.jsonl') }) | |
| 540 | const ctx = makeCtx({ inbound: new MemoryInboundStore(), observations }) | |
| 541 | await handleRequest(signedActivity({ | |
| 542 | '@context': 'https://www.w3.org/ns/activitystreams', | |
| 543 | id: `${REMOTE}/a/flag`, type: 'Flag', actor: REMOTE, object: 'https://p2p.7rnx.net/ap/objects/1' | |
| 544 | }), ctx) | |
| 545 | ||
| 546 | // A row read later as data has to mean what it says. | |
| 547 | t.is(observations.count({}), 0, 'dropped rather than guessed at') | |
| 548 | }) | |
| 549 | ||
| 550 | test('⚠ a bridge with no observation store behaves exactly as before', async (t) => { | |
| 551 | const inbound = new MemoryInboundStore() | |
| 552 | const ctx = makeCtx({ inbound }) | |
| 553 | const res = await handleRequest(signedActivity({ | |
| 554 | '@context': 'https://www.w3.org/ns/activitystreams', | |
| 555 | id: `${REMOTE}/a/like-noobs`, type: 'Like', actor: REMOTE, object: 'https://p2p.7rnx.net/ap/objects/1' | |
| 556 | }), ctx) | |
| 557 | t.is(res.status, 202, 'still acked promptly') | |
| 558 | t.is((await inbound.list({})).length, 1, 'and still kept in the inbound timeline') | |
| 559 | }) |