| … | … | @@ -1,9 +1,9 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "ap-bridge", |
| 3 | "version": "0.9.91", | |
| 3 | "version": "0.9.93", | |
| 4 | 4 | "runtime": "bare", |
| 5 | "rootTreeHash": "4373422f9b5b8c299b8a34e7104da5dae1e608935ac858b423df236dc2474db4", | |
| 6 | "publishedAt": "2026-09-11T22:03:58.277Z", | |
| 5 | "rootTreeHash": "6d10ca5949e4d7523e108ecbe1d6f04a4ece408318276e1492686c8cf5cd6c3f", | |
| 6 | "publishedAt": "2026-09-11T23:30:52.409Z", | |
| 7 | 7 | "deps": [ |
| 8 | 8 | { |
| 9 | 9 | "name": "@hyperswarm/secret-stream", |
| … | … | @@ -2396,8 +2396,8 @@ |
| 2396 | 2396 | { |
| 2397 | 2397 | "name": "favela-peer", |
| 2398 | 2398 | "version": "0.0.56", |
| 2399 | "tree": "8ea15d633438bd0f3d4dc69babecd0069ba33c3e7e63de8dd5d428f9530062b7", | |
| 2400 | "normHash": "42db39cc01ba6f0424f2302b28fe5a523e082501ae708961cd16f2a025faada2", | |
| 2399 | "tree": "fb3415c5c01dcc8d31eb0b218ef450eb0bbfa85ff41de769763f4b82666af874", | |
| 2400 | "normHash": "2a447b52145af4ac66941b9cc7f8bfe8852bb18ce031bbae2bd63df955f47756", | |
| 2401 | 2401 | "workspace": true |
| 2402 | 2402 | }, |
| 2403 | 2403 | { |
| … | … | @@ -4004,8 +4004,8 @@ |
| 4004 | 4004 | "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37" |
| 4005 | 4005 | } |
| 4006 | 4006 | ], |
| 4007 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4008 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4007 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4008 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4009 | 4009 | }, |
| 4010 | 4010 | { |
| 4011 | 4011 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4019,8 +4019,8 @@ |
| 4019 | 4019 | "tree": "ee91b6bd77101e3404ca571b2e5adf0f47029d174398a05f50294fc207e39d99" |
| 4020 | 4020 | } |
| 4021 | 4021 | ], |
| 4022 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4023 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4022 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4023 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4024 | 4024 | }, |
| 4025 | 4025 | { |
| 4026 | 4026 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4034,8 +4034,8 @@ |
| 4034 | 4034 | "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11" |
| 4035 | 4035 | } |
| 4036 | 4036 | ], |
| 4037 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4038 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4037 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4038 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4039 | 4039 | }, |
| 4040 | 4040 | { |
| 4041 | 4041 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4054,8 +4054,8 @@ |
| 4054 | 4054 | "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d" |
| 4055 | 4055 | } |
| 4056 | 4056 | ], |
| 4057 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4058 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4057 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4058 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4059 | 4059 | }, |
| 4060 | 4060 | { |
| 4061 | 4061 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4069,8 +4069,8 @@ |
| 4069 | 4069 | "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd" |
| 4070 | 4070 | } |
| 4071 | 4071 | ], |
| 4072 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4073 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4072 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4073 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4074 | 4074 | }, |
| 4075 | 4075 | { |
| 4076 | 4076 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4084,8 +4084,8 @@ |
| 4084 | 4084 | "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95" |
| 4085 | 4085 | } |
| 4086 | 4086 | ], |
| 4087 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4088 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4087 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4088 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4089 | 4089 | }, |
| 4090 | 4090 | { |
| 4091 | 4091 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4099,8 +4099,8 @@ |
| 4099 | 4099 | "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb" |
| 4100 | 4100 | } |
| 4101 | 4101 | ], |
| 4102 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4103 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4102 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4103 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4104 | 4104 | }, |
| 4105 | 4105 | { |
| 4106 | 4106 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4114,8 +4114,8 @@ |
| 4114 | 4114 | "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a" |
| 4115 | 4115 | } |
| 4116 | 4116 | ], |
| 4117 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4118 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4117 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4118 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4119 | 4119 | }, |
| 4120 | 4120 | { |
| 4121 | 4121 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4129,8 +4129,8 @@ |
| 4129 | 4129 | "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca" |
| 4130 | 4130 | } |
| 4131 | 4131 | ], |
| 4132 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4133 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4132 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4133 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4134 | 4134 | }, |
| 4135 | 4135 | { |
| 4136 | 4136 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4144,8 +4144,8 @@ |
| 4144 | 4144 | "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701" |
| 4145 | 4145 | } |
| 4146 | 4146 | ], |
| 4147 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4148 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4147 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4148 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4149 | 4149 | }, |
| 4150 | 4150 | { |
| 4151 | 4151 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4159,8 +4159,8 @@ |
| 4159 | 4159 | "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d" |
| 4160 | 4160 | } |
| 4161 | 4161 | ], |
| 4162 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4163 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4162 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4163 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4164 | 4164 | }, |
| 4165 | 4165 | { |
| 4166 | 4166 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4174,8 +4174,8 @@ |
| 4174 | 4174 | "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04" |
| 4175 | 4175 | } |
| 4176 | 4176 | ], |
| 4177 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4178 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4177 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4178 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4179 | 4179 | }, |
| 4180 | 4180 | { |
| 4181 | 4181 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4189,8 +4189,8 @@ |
| 4189 | 4189 | "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4" |
| 4190 | 4190 | } |
| 4191 | 4191 | ], |
| 4192 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4193 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4192 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4193 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4194 | 4194 | }, |
| 4195 | 4195 | { |
| 4196 | 4196 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4204,8 +4204,8 @@ |
| 4204 | 4204 | "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df" |
| 4205 | 4205 | } |
| 4206 | 4206 | ], |
| 4207 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4208 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4207 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4208 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4209 | 4209 | }, |
| 4210 | 4210 | { |
| 4211 | 4211 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4219,8 +4219,8 @@ |
| 4219 | 4219 | "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad" |
| 4220 | 4220 | } |
| 4221 | 4221 | ], |
| 4222 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4223 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4222 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4223 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4224 | 4224 | }, |
| 4225 | 4225 | { |
| 4226 | 4226 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4234,8 +4234,8 @@ |
| 4234 | 4234 | "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e" |
| 4235 | 4235 | } |
| 4236 | 4236 | ], |
| 4237 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4238 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4237 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4238 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4239 | 4239 | }, |
| 4240 | 4240 | { |
| 4241 | 4241 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4264,8 +4264,8 @@ |
| 4264 | 4264 | "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c" |
| 4265 | 4265 | } |
| 4266 | 4266 | ], |
| 4267 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4268 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4267 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4268 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4269 | 4269 | }, |
| 4270 | 4270 | { |
| 4271 | 4271 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4279,8 +4279,8 @@ |
| 4279 | 4279 | "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9" |
| 4280 | 4280 | } |
| 4281 | 4281 | ], |
| 4282 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4283 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4282 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4283 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4284 | 4284 | }, |
| 4285 | 4285 | { |
| 4286 | 4286 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4294,8 +4294,8 @@ |
| 4294 | 4294 | "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169" |
| 4295 | 4295 | } |
| 4296 | 4296 | ], |
| 4297 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4298 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4297 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4298 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4299 | 4299 | }, |
| 4300 | 4300 | { |
| 4301 | 4301 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4309,8 +4309,8 @@ |
| 4309 | 4309 | "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b" |
| 4310 | 4310 | } |
| 4311 | 4311 | ], |
| 4312 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4313 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4312 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4313 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4314 | 4314 | }, |
| 4315 | 4315 | { |
| 4316 | 4316 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4324,8 +4324,8 @@ |
| 4324 | 4324 | "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed" |
| 4325 | 4325 | } |
| 4326 | 4326 | ], |
| 4327 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4328 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4327 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4328 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4329 | 4329 | }, |
| 4330 | 4330 | { |
| 4331 | 4331 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4339,8 +4339,8 @@ |
| 4339 | 4339 | "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f" |
| 4340 | 4340 | } |
| 4341 | 4341 | ], |
| 4342 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4343 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4342 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4343 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4344 | 4344 | }, |
| 4345 | 4345 | { |
| 4346 | 4346 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4354,8 +4354,8 @@ |
| 4354 | 4354 | "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754" |
| 4355 | 4355 | } |
| 4356 | 4356 | ], |
| 4357 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4358 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4357 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4358 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4359 | 4359 | }, |
| 4360 | 4360 | { |
| 4361 | 4361 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4369,8 +4369,8 @@ |
| 4369 | 4369 | "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5" |
| 4370 | 4370 | } |
| 4371 | 4371 | ], |
| 4372 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4373 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4372 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4373 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4374 | 4374 | }, |
| 4375 | 4375 | { |
| 4376 | 4376 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4389,8 +4389,8 @@ |
| 4389 | 4389 | "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393" |
| 4390 | 4390 | } |
| 4391 | 4391 | ], |
| 4392 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4393 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4392 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4393 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4394 | 4394 | }, |
| 4395 | 4395 | { |
| 4396 | 4396 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4404,8 +4404,8 @@ |
| 4404 | 4404 | "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5" |
| 4405 | 4405 | } |
| 4406 | 4406 | ], |
| 4407 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4408 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4407 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4408 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4409 | 4409 | }, |
| 4410 | 4410 | { |
| 4411 | 4411 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4419,8 +4419,8 @@ |
| 4419 | 4419 | "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6" |
| 4420 | 4420 | } |
| 4421 | 4421 | ], |
| 4422 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4423 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4422 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4423 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4424 | 4424 | }, |
| 4425 | 4425 | { |
| 4426 | 4426 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4434,8 +4434,8 @@ |
| 4434 | 4434 | "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9" |
| 4435 | 4435 | } |
| 4436 | 4436 | ], |
| 4437 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4438 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4437 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4438 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4439 | 4439 | }, |
| 4440 | 4440 | { |
| 4441 | 4441 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4449,8 +4449,8 @@ |
| 4449 | 4449 | "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0" |
| 4450 | 4450 | } |
| 4451 | 4451 | ], |
| 4452 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4453 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4452 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4453 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4454 | 4454 | }, |
| 4455 | 4455 | { |
| 4456 | 4456 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4464,8 +4464,8 @@ |
| 4464 | 4464 | "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53" |
| 4465 | 4465 | } |
| 4466 | 4466 | ], |
| 4467 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4468 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4467 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4468 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4469 | 4469 | }, |
| 4470 | 4470 | { |
| 4471 | 4471 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4479,8 +4479,8 @@ |
| 4479 | 4479 | "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a" |
| 4480 | 4480 | } |
| 4481 | 4481 | ], |
| 4482 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4483 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4482 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4483 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4484 | 4484 | }, |
| 4485 | 4485 | { |
| 4486 | 4486 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4499,8 +4499,8 @@ |
| 4499 | 4499 | "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648" |
| 4500 | 4500 | } |
| 4501 | 4501 | ], |
| 4502 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4503 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4502 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4503 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4504 | 4504 | }, |
| 4505 | 4505 | { |
| 4506 | 4506 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4514,8 +4514,8 @@ |
| 4514 | 4514 | "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57" |
| 4515 | 4515 | } |
| 4516 | 4516 | ], |
| 4517 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4518 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4517 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4518 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4519 | 4519 | }, |
| 4520 | 4520 | { |
| 4521 | 4521 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4529,8 +4529,8 @@ |
| 4529 | 4529 | "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78" |
| 4530 | 4530 | } |
| 4531 | 4531 | ], |
| 4532 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4533 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4532 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4533 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4534 | 4534 | }, |
| 4535 | 4535 | { |
| 4536 | 4536 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4544,8 +4544,8 @@ |
| 4544 | 4544 | "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9" |
| 4545 | 4545 | } |
| 4546 | 4546 | ], |
| 4547 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4548 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4547 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4548 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4549 | 4549 | }, |
| 4550 | 4550 | { |
| 4551 | 4551 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4559,8 +4559,8 @@ |
| 4559 | 4559 | "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972" |
| 4560 | 4560 | } |
| 4561 | 4561 | ], |
| 4562 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4563 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4562 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4563 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4564 | 4564 | }, |
| 4565 | 4565 | { |
| 4566 | 4566 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4574,8 +4574,8 @@ |
| 4574 | 4574 | "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519" |
| 4575 | 4575 | } |
| 4576 | 4576 | ], |
| 4577 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4578 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4577 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4578 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4579 | 4579 | }, |
| 4580 | 4580 | { |
| 4581 | 4581 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4589,8 +4589,8 @@ |
| 4589 | 4589 | "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390" |
| 4590 | 4590 | } |
| 4591 | 4591 | ], |
| 4592 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4593 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4592 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4593 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4594 | 4594 | }, |
| 4595 | 4595 | { |
| 4596 | 4596 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4604,8 +4604,8 @@ |
| 4604 | 4604 | "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706" |
| 4605 | 4605 | } |
| 4606 | 4606 | ], |
| 4607 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4608 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4607 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4608 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4609 | 4609 | }, |
| 4610 | 4610 | { |
| 4611 | 4611 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4619,8 +4619,8 @@ |
| 4619 | 4619 | "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb" |
| 4620 | 4620 | } |
| 4621 | 4621 | ], |
| 4622 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4623 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4622 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4623 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4624 | 4624 | }, |
| 4625 | 4625 | { |
| 4626 | 4626 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4634,8 +4634,8 @@ |
| 4634 | 4634 | "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae" |
| 4635 | 4635 | } |
| 4636 | 4636 | ], |
| 4637 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4638 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4637 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4638 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4639 | 4639 | }, |
| 4640 | 4640 | { |
| 4641 | 4641 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4654,8 +4654,8 @@ |
| 4654 | 4654 | "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765" |
| 4655 | 4655 | } |
| 4656 | 4656 | ], |
| 4657 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4658 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4657 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4658 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4659 | 4659 | }, |
| 4660 | 4660 | { |
| 4661 | 4661 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4669,8 +4669,8 @@ |
| 4669 | 4669 | "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60" |
| 4670 | 4670 | } |
| 4671 | 4671 | ], |
| 4672 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4673 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4672 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4673 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4674 | 4674 | }, |
| 4675 | 4675 | { |
| 4676 | 4676 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4684,8 +4684,8 @@ |
| 4684 | 4684 | "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a" |
| 4685 | 4685 | } |
| 4686 | 4686 | ], |
| 4687 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4688 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4687 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4688 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4689 | 4689 | }, |
| 4690 | 4690 | { |
| 4691 | 4691 | "type": "VERSION_CONFLICT", |
| … | … | @@ -4699,8 +4699,8 @@ |
| 4699 | 4699 | "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b" |
| 4700 | 4700 | } |
| 4701 | 4701 | ], |
| 4702 | "reason": "ap-bridge 0.9.91 — holder re-claim is admitted; a pending claim names the acct", | |
| 4703 | "acceptedAt": "2026-09-11T22:03:58.310Z" | |
| 4702 | "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment", | |
| 4703 | "acceptedAt": "2026-09-11T23:30:52.432Z" | |
| 4704 | 4704 | } |
| 4705 | 4705 | ] |
| 4706 | 4706 | } |
| … | … | @@ -10,6 +10,7 @@ import fs from 'bare-fs' |
| 10 | 10 | import process from 'bare-process' |
| 11 | 11 | import { join } from 'bare-path' |
| 12 | 12 | import { loadConfig, assertUsable } from './lib/config.js' |
| 13 | import crypto from 'bare-crypto' | |
| 13 | 14 | import http from 'bare-http1' |
| 14 | 15 | import { createServer } from './lib/server.js' |
| 15 | 16 | import { actorDocFor } from './lib/router.js' |
| … | … | @@ -490,11 +491,30 @@ async function main () { |
| 490 | 491 | // it must be a decision, never a default." It mints nothing; every action on it is a flow that |
| 491 | 492 | // already exists (invite, list, release). |
| 492 | 493 | if (config.adminPort) { |
| 493 | const { adminRoutes } = await import('./lib/admin.js') | |
| 494 | const { serveAdmin } = await import('./lib/admin-surface.js') | |
| 495 | const { Sessions } = await import('./lib/admin-auth.js') | |
| 494 | 496 | const { parseForm } = await import('./lib/admin-serve.js') |
| 497 | const { Browsers } = await import('favela-peer/lib/browsers.js') | |
| 498 | ||
| 499 | // ⛔ A TOKEN PER BOOT, HELD ONLY IN MEMORY AND ONLY UNTIL CLAIMED. Writing it to disk would make a | |
| 500 | // standing secret out of something needed exactly once; regenerating it every start costs nothing | |
| 501 | // because it is meaningless the moment the bridge is claimed. | |
| 502 | const bootstrap = crypto.randomBytes(16).toString('hex') | |
| 503 | // GOTCHA, same as the record store above: openCorestore, never `new Corestore` from a separately | |
| 504 | // installed copy — `instanceof Corestore` fails across duplicated module copies. | |
| 505 | const browsers = new Browsers(openCorestore(join(dataDir, 'admin-identity')), { bootstrap }) | |
| 506 | await browsers.ready() | |
| 507 | ||
| 508 | // ⛔ PRINTED ONLY WHILE UNCLAIMED, because after that it grants nothing and logging a dead secret | |
| 509 | // every boot teaches an operator to ignore the line that matters. | |
| 510 | if (await browsers.unclaimed()) { | |
| 511 | log({ msg: 'admin surface UNCLAIMED — claim it with this bootstrap token, then it closes for good', bootstrap }) | |
| 512 | } | |
| 513 | ||
| 514 | const adminCtx = { browsers, sessions: new Sessions(), registrations, inbound, config, root: new URL('.', import.meta.url).pathname } | |
| 495 | 515 | const adminServer = http.createServer((req, res) => { |
| 496 | parseForm(req).then((form) => adminRoutes({ method: req.method, path: (req.url || '/').split('?')[0], form }, | |
| 497 | { registrations, inbound, config })) | |
| 516 | parseForm(req).then((form) => serveAdmin( | |
| 517 | { method: req.method, path: (req.url || '/').split('?')[0], form, headers: req.headers || {} }, adminCtx)) | |
| 498 | 518 | .then((out) => { res.writeHead(out.status, out.headers); res.end(out.body) }) |
| 499 | 519 | .catch((err) => { res.writeHead(500, { 'content-type': 'text/plain' }); res.end(String((err && err.message) || err)) }) |
| 500 | 520 | }) |
| … | … | @@ -504,7 +524,12 @@ async function main () { |
| 504 | 524 | // the public listener must not thereby publish the admin surface. |
| 505 | 525 | adminServer.listen(config.adminPort, '127.0.0.1', resolve) |
| 506 | 526 | }) |
| 507 | log({ msg: 'admin surface (loopback only, no auth — reach it over SSH)', url: 'http://127.0.0.1:' + config.adminPort + '/admin' }) | |
| 527 | // ⛔ STILL LOOPBACK EVEN THOUGH IT NOW HAS AUTH, and for a reason that is not caution: | |
| 528 | // static/anproto-client.js hashes with `crypto.subtle`, which browsers expose only in a SECURE | |
| 529 | // CONTEXT ("works on localhost + HTTPS" — its own comment). Bound to a public port over plain | |
| 530 | // HTTP this would serve a login page whose crypto is undefined in the browser that loaded it. | |
| 531 | // Publish it through the proxy that already terminates TLS for this host. | |
| 532 | log({ msg: 'admin surface (authenticated; loopback — publish it over HTTPS via the proxy)', url: 'http://127.0.0.1:' + config.adminPort + '/admin' }) | |
| 508 | 533 | } |
| 509 | 534 | |
| 510 | 535 | // ★ The first pass picks up whatever the last run left owed — which is the entire point of the |
| … | … | @@ -0,0 +1,185 @@ |
| 1 | // admin-auth.js — who may operate this bridge. | |
| 2 | // | |
| 3 | // ⛔ THE SAME IDENTITY LAYER THE REST OF THE STACK USES, not a password. `Browsers` (favela-peer) | |
| 4 | // holds the registry: an ANProto keypair proves itself against a single-use challenge, the FIRST one | |
| 5 | // claims the instance, and every one after needs an invite. A bearer password would have been fewer | |
| 6 | // moving parts and a seventh way to authenticate in one monorepo. | |
| 7 | // | |
| 8 | // ⛔ AND THE FIRST CLAIM IS GATED BY A BOOTSTRAP TOKEN. Plain first-claim TOFU is safe only while the | |
| 9 | // surface is bound to loopback — browsers.js says so in its own header — and an operator page has to be | |
| 10 | // reachable. The token is printed to the service log at startup, so it is a secret only whoever can | |
| 11 | // read the log holds. See browsers.js for the gate and the claim latch. | |
| 12 | // | |
| 13 | // ⛔ HTTPS OR LOCALHOST, NOT PLAIN HTTP ON A PUBLIC PORT. static/anproto-client.js hashes with | |
| 14 | // `crypto.subtle`, which browsers expose only in a secure context — its own comment says "works on | |
| 15 | // localhost + HTTPS". So this listener stays on loopback and is published through the proxy that | |
| 16 | // already terminates TLS for this host. Binding it to the network directly would serve a login page | |
| 17 | // whose crypto is undefined in the browser that loaded it. | |
| 18 | ||
| 19 | import crypto from 'bare-crypto' | |
| 20 | ||
| 21 | export const COOKIE = 'ap_admin' | |
| 22 | ||
| 23 | /** | |
| 24 | * Cookie -> the browser public key that proved itself. That is ALL it is. | |
| 25 | * | |
| 26 | * ⛔ EVERY REQUEST RE-ASKS whether that key is still allowed, so a revoke lands on the next request | |
| 27 | * rather than whenever something lapses. That is why there is no expiry to configure: the check is | |
| 28 | * live, and an expiry would only add a way to be logged out for no reason. Same shape favela-surface | |
| 29 | * uses, and for the same reasons. | |
| 30 | * | |
| 31 | * ⚠ In memory, so a restart means logging in again. Persisting them would make a bridge own durable | |
| 32 | * security state it has no other reason to hold. | |
| 33 | */ | |
| 34 | export class Sessions { | |
| 35 | constructor () { this._byId = new Map() } | |
| 36 | ||
| 37 | create (publicKey) { | |
| 38 | const id = crypto.randomBytes(32).toString('hex') | |
| 39 | this._byId.set(id, publicKey) | |
| 40 | return id | |
| 41 | } | |
| 42 | ||
| 43 | get (id) { return (id && this._byId.get(id)) || null } | |
| 44 | destroy (id) { if (id) this._byId.delete(id) } | |
| 45 | get size () { return this._byId.size } | |
| 46 | } | |
| 47 | ||
| 48 | export function parseCookies (header) { | |
| 49 | const out = {} | |
| 50 | for (const part of String(header || '').split(';')) { | |
| 51 | const i = part.indexOf('=') | |
| 52 | if (i === -1) continue | |
| 53 | out[part.slice(0, i).trim()] = part.slice(i + 1).trim() | |
| 54 | } | |
| 55 | return out | |
| 56 | } | |
| 57 | ||
| 58 | /** | |
| 59 | * The public key on this request, if it is still allowed to operate the bridge. | |
| 60 | * | |
| 61 | * ⛔ `browsers.status()` IS THE AUTHORITY, not the cookie. A cookie that maps to a revoked key must | |
| 62 | * stop working immediately — holding a session must never outlive the grant behind it. | |
| 63 | */ | |
| 64 | export async function currentAdmin (req, { sessions, browsers }) { | |
| 65 | const id = parseCookies(req.headers && req.headers.cookie)[COOKIE] | |
| 66 | const publicKey = sessions.get(id) | |
| 67 | if (!publicKey) return null | |
| 68 | try { | |
| 69 | const st = await browsers.status(publicKey) | |
| 70 | if (!st || !st.known || st.revoked) { sessions.destroy(id); return null } | |
| 71 | } catch { return null } | |
| 72 | return publicKey | |
| 73 | } | |
| 74 | ||
| 75 | const esc = (s) => String(s == null ? '' : s) | |
| 76 | .replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"') | |
| 77 | ||
| 78 | export function loginPage ({ unclaimed = false, message = null } = {}) { | |
| 79 | const warn = message ? '<p class="warn">' + esc(message) + '</p>' : '' | |
| 80 | const intro = unclaimed | |
| 81 | ? '<p>Nobody operates this bridge yet. Claim it with the <strong>bootstrap token</strong> printed ' + | |
| 82 | 'in the service log when it started.</p>' + | |
| 83 | '<p><label>Bootstrap token<br><input id="bootstrap" type="password" autocomplete="off" ' + | |
| 84 | 'placeholder="32 hex characters"></label></p>' | |
| 85 | : '<p>This browser signs in with the key it generated the first time you used it. If this is a new ' + | |
| 86 | 'browser it needs an invite from one that is already signed in.</p>' | |
| 87 | ||
| 88 | return `<!doctype html><meta charset="utf-8"><title>bridge · sign in</title> | |
| 89 | <style>body{font:14px/1.5 system-ui,sans-serif;margin:0;padding:40px;max-width:520px;color:#111} | |
| 90 | h1{font-size:20px}.warn{color:#a00}.who{color:#666}input{font:inherit;padding:5px;width:100%;box-sizing:border-box} | |
| 91 | button{font:inherit;padding:6px 14px;margin-top:12px}code{font:12px ui-monospace,monospace;background:#f5f5f5;padding:1px 4px}</style> | |
| 92 | <h1>Sign in to this bridge</h1>${warn}${intro} | |
| 93 | <button id="go">sign in</button> | |
| 94 | <p class="who" id="msg"></p> | |
| 95 | <p class="who">Your key: <code id="pk">…</code></p> | |
| 96 | <script type="module"> | |
| 97 | import { ANProtoClient as an } from '/static/anproto-client.js' | |
| 98 | ||
| 99 | // ⛔ THE KEY LIVES IN THIS BROWSER AND NOWHERE ELSE. It is not a device key and never enters the | |
| 100 | // attestation chain — a browser cannot hold a writer core or verify a chain. It authenticates; the | |
| 101 | // daemon writes. | |
| 102 | const KEY = 'ap-admin-keypair' | |
| 103 | let kp = localStorage.getItem(KEY) | |
| 104 | if (!kp) { kp = await an.gen(); localStorage.setItem(KEY, kp) } | |
| 105 | const pk = an.getPublicKey(kp) | |
| 106 | document.getElementById('pk').textContent = pk | |
| 107 | ||
| 108 | const msg = document.getElementById('msg') | |
| 109 | const post = (path, body) => fetch(path, { | |
| 110 | method: 'POST', | |
| 111 | headers: { 'content-type': 'application/x-www-form-urlencoded' }, | |
| 112 | body: new URLSearchParams(body).toString() | |
| 113 | }) | |
| 114 | ||
| 115 | document.getElementById('go').onclick = async () => { | |
| 116 | msg.textContent = 'signing…' | |
| 117 | try { | |
| 118 | const r = await post('/admin/challenge', {}) | |
| 119 | const { challenge } = await r.json() | |
| 120 | // ⚠ The raw challenge — this client hashes it internally and emits the canonical wire format. | |
| 121 | // Hashing it here first would sign the wrong thing, verify, and then fail the hash check. | |
| 122 | const signature = await an.sign(challenge, kp) | |
| 123 | const el = document.getElementById('bootstrap') | |
| 124 | const out = await post('/admin/session', { | |
| 125 | publicKey: pk, signature, challenge, bootstrap: el ? el.value : '' | |
| 126 | }) | |
| 127 | if (out.ok) { location.href = '/admin'; return } | |
| 128 | msg.textContent = await out.text() | |
| 129 | } catch (e) { | |
| 130 | msg.textContent = (e && e.message) || String(e) | |
| 131 | } | |
| 132 | } | |
| 133 | </script>` | |
| 134 | } | |
| 135 | ||
| 136 | const text = (status, body) => ({ status, headers: { 'content-type': 'text/plain; charset=utf-8' }, body }) | |
| 137 | const html = (status, body) => ({ status, headers: { 'content-type': 'text/html; charset=utf-8' }, body }) | |
| 138 | ||
| 139 | /** | |
| 140 | * The sign-in routes. Returns null when the path is not one of ours, so the caller can route on. | |
| 141 | */ | |
| 142 | export async function authRoutes (req, { browsers, sessions }) { | |
| 143 | const path = req.path || '/' | |
| 144 | ||
| 145 | if (req.method === 'GET' && path === '/admin/login') { | |
| 146 | return html(200, loginPage({ unclaimed: await browsers.unclaimed() })) | |
| 147 | } | |
| 148 | ||
| 149 | if (req.method === 'POST' && path === '/admin/challenge') { | |
| 150 | const { challenge } = browsers.challenge() | |
| 151 | return { status: 200, headers: { 'content-type': 'application/json' }, body: JSON.stringify({ challenge }) } | |
| 152 | } | |
| 153 | ||
| 154 | if (req.method === 'POST' && path === '/admin/session') { | |
| 155 | const f = req.form || {} | |
| 156 | const proof = { publicKey: f.publicKey, signature: f.signature, challenge: f.challenge } | |
| 157 | try { | |
| 158 | // ⛔ CLAIM OR AUTHENTICATE — never both, and the instance decides which. Offering a "claim" | |
| 159 | // button after the first one would be a second door onto an instance that is already owned. | |
| 160 | if (await browsers.unclaimed()) await browsers.claim({ ...proof, bootstrap: f.bootstrap || null }) | |
| 161 | else await browsers.authenticate(proof) | |
| 162 | } catch (err) { | |
| 163 | // ⚠ The daemon's own words: it knows whether the token was wrong, the key unknown, the | |
| 164 | // challenge expired, or the instance already claimed. A generic "sign-in failed" would make | |
| 165 | // every one of those look like a typo. | |
| 166 | return text(403, (err && err.message) || String(err)) | |
| 167 | } | |
| 168 | const id = sessions.create(f.publicKey) | |
| 169 | return { | |
| 170 | status: 200, | |
| 171 | // ⚠ HttpOnly so a page cannot read it; SameSite=Strict so it never rides a cross-site request. | |
| 172 | headers: { 'set-cookie': COOKIE + '=' + id + '; HttpOnly; SameSite=Strict; Path=/' }, | |
| 173 | body: 'ok' | |
| 174 | } | |
| 175 | } | |
| 176 | ||
| 177 | if (req.method === 'POST' && path === '/admin/logout') { | |
| 178 | sessions.destroy(parseCookies(req.headers && req.headers.cookie)[COOKIE]) | |
| 179 | return { status: 303, headers: { location: '/admin/login', 'set-cookie': COOKIE + '=; Max-Age=0; Path=/' }, body: '' } | |
| 180 | } | |
| 181 | ||
| 182 | return null | |
| 183 | } | |
| 184 | ||
| 185 | export default { Sessions, authRoutes, currentAdmin, loginPage, parseCookies, COOKIE } |
| … | … | @@ -0,0 +1,51 @@ |
| 1 | // The operator surface, assembled: sign-in, then the pages, with the gate between them. | |
| 2 | // | |
| 3 | // ⛔ THE GATE IS HERE AND NOWHERE ELSE, so there is one place to read to know what is protected. Every | |
| 4 | // path that is not sign-in or a static asset requires a live session, and "live" is re-checked against | |
| 5 | // the identity registry per request rather than trusted from the cookie. | |
| 6 | // | |
| 7 | // ⛔ STATIC IS SERVED FROM A FIXED LIST, not from a directory join. A path taken off the wire and | |
| 8 | // joined to a root is a traversal bug; three files that never change do not need a filesystem walk. | |
| 9 | ||
| 10 | import fs from 'bare-fs' | |
| 11 | import { join } from 'bare-path' | |
| 12 | import { authRoutes, currentAdmin } from './admin-auth.js' | |
| 13 | import { adminRoutes } from './admin.js' | |
| 14 | ||
| 15 | const TYPES = { '.js': 'text/javascript; charset=utf-8' } | |
| 16 | ||
| 17 | // The browser half of the identity layer. Vendored, and the same files favela-surface and pear-git | |
| 18 | // serve — noble is pure JS so signing works without crypto.subtle; the SHA-256 in the client does use | |
| 19 | // it, which is why this surface must be reached over HTTPS or localhost. | |
| 20 | const ASSETS = new Set(['anproto-client.js', 'noble-ed25519.js', 'noble-sha512.js']) | |
| 21 | ||
| 22 | export function staticAsset (name, root) { | |
| 23 | if (!ASSETS.has(name)) return null | |
| 24 | try { | |
| 25 | return { status: 200, headers: { 'content-type': TYPES['.js'] }, body: fs.readFileSync(join(root, 'static', name)) } | |
| 26 | } catch { | |
| 27 | return null | |
| 28 | } | |
| 29 | } | |
| 30 | ||
| 31 | export async function serveAdmin (req, ctx) { | |
| 32 | const path = req.path || '/' | |
| 33 | ||
| 34 | if (path.startsWith('/static/')) { | |
| 35 | const asset = staticAsset(path.slice('/static/'.length), ctx.root) | |
| 36 | return asset || { status: 404, headers: {}, body: 'not found' } | |
| 37 | } | |
| 38 | ||
| 39 | // Sign-in first: these are the only routes reachable without a session, by definition. | |
| 40 | const auth = await authRoutes(req, ctx) | |
| 41 | if (auth) return auth | |
| 42 | ||
| 43 | // ⛔ NO SESSION, NO SURFACE. A redirect rather than a 403 because the operator's next act is always | |
| 44 | // to sign in, and a bare 403 on a page they navigated to reads as "this is broken". | |
| 45 | const admin = await currentAdmin(req, ctx) | |
| 46 | if (!admin) return { status: 303, headers: { location: '/admin/login' }, body: '' } | |
| 47 | ||
| 48 | return adminRoutes(req, ctx) | |
| 49 | } | |
| 50 | ||
| 51 | export default { serveAdmin, staticAsset } |
| … | … | @@ -1,6 +1,6 @@ |
| 1 | 1 | { |
| 2 | 2 | "name": "ap-bridge", |
| 3 | "version": "0.9.93", | |
| 3 | "version": "0.9.94", | |
| 4 | 4 | "description": "ActivityPub membrane — a second projection of the self-custody atproto peer", |
| 5 | 5 | "type": "module", |
| 6 | 6 | "main": "index.js", |
| … | … | @@ -0,0 +1,161 @@ |
| 1 | /** | |
| 2 | * ANProto Browser Client — Ed25519 identity for browser contexts | |
| 3 | * | |
| 4 | * Uses vendored @noble/ed25519 (pure JS, no native deps, works on HTTP). | |
| 5 | * Same protocol as server-side ANProto — keypairs, challenge-response signing. | |
| 6 | * | |
| 7 | * Keypair format: 132-char base64 string (44 pubkey + 88 secretkey) | |
| 8 | * Signature format: 88-char base64 (64 bytes) | |
| 9 | */ | |
| 10 | ||
| 11 | import * as ed from './noble-ed25519.js' | |
| 12 | import { sha512 } from './noble-sha512.js' | |
| 13 | ||
| 14 | // Wire pure-JS SHA-512 into noble-ed25519 (replaces crypto.subtle dependency) | |
| 15 | ed.etc.sha512Sync = (...msgs) => { | |
| 16 | const h = sha512.create() | |
| 17 | for (const m of msgs) h.update(m) | |
| 18 | return h.digest() | |
| 19 | } | |
| 20 | ed.etc.sha512Async = async (...msgs) => ed.etc.sha512Sync(...msgs) | |
| 21 | ||
| 22 | export const ANProtoClient = { | |
| 23 | /** | |
| 24 | * Generate a new Ed25519 keypair | |
| 25 | * @returns {Promise<string>} 132-char base64 keypair (44 pub + 88 secret) | |
| 26 | */ | |
| 27 | async gen () { | |
| 28 | const privateKey = ed.utils.randomPrivateKey() | |
| 29 | const publicKey = await ed.getPublicKeyAsync(privateKey) | |
| 30 | ||
| 31 | // Build 64-byte secret key (32 seed + 32 pubkey) to match sodium format | |
| 32 | const secretKey = new Uint8Array(64) | |
| 33 | secretKey.set(privateKey, 0) | |
| 34 | secretKey.set(publicKey, 32) | |
| 35 | ||
| 36 | return bytesToBase64(publicKey) + bytesToBase64(secretKey) | |
| 37 | }, | |
| 38 | ||
| 39 | /** | |
| 40 | * Get public key from keypair string | |
| 41 | * @param {string} keypair — 132-char base64 | |
| 42 | * @returns {string} 44-char base64 public key | |
| 43 | */ | |
| 44 | getPublicKey (keypair) { | |
| 45 | return keypair.substring(0, 44) | |
| 46 | }, | |
| 47 | ||
| 48 | /** | |
| 49 | * Get public key as hex string | |
| 50 | * @param {string} keypair — 132-char base64 | |
| 51 | * @returns {string} 64-char hex public key | |
| 52 | */ | |
| 53 | getPublicKeyHex (keypair) { | |
| 54 | const bytes = base64ToBytes(keypair.substring(0, 44)) | |
| 55 | return bytesToHex(bytes) | |
| 56 | }, | |
| 57 | ||
| 58 | /** | |
| 59 | * SHA-256 hash (uses crypto.subtle, works on localhost + HTTPS) | |
| 60 | * @param {string} data | |
| 61 | * @returns {Promise<string>} 44-char base64 hash | |
| 62 | */ | |
| 63 | async hash (data) { | |
| 64 | const bytes = new TextEncoder().encode(data) | |
| 65 | const hashBuf = await crypto.subtle.digest('SHA-256', bytes) | |
| 66 | return bytesToBase64(new Uint8Array(hashBuf)) | |
| 67 | }, | |
| 68 | ||
| 69 | /** | |
| 70 | * Sign a challenge using canonical ANProto wire format. | |
| 71 | * | |
| 72 | * Produces: pubkey(44 base64) + base64(signature(64) || timestamp + hash) | |
| 73 | * Wire-compatible with server-side ANProto.verify() and an.open(). | |
| 74 | * | |
| 75 | * @param {string} challenge — challenge string (from server) | |
| 76 | * @param {string} keypair — 132-char base64 keypair | |
| 77 | * @returns {Promise<string>} canonical wire-format signature | |
| 78 | */ | |
| 79 | async sign (challenge, keypair) { | |
| 80 | const pubKeyB64 = keypair.substring(0, 44) | |
| 81 | const secretKeyB64 = keypair.substring(44, 132) | |
| 82 | const seed = base64ToBytes(secretKeyB64).slice(0, 32) | |
| 83 | ||
| 84 | // Canonical: hash the challenge, prepend timestamp, sign the combined message | |
| 85 | const contentHash = await this.hash(challenge) | |
| 86 | const ts = Date.now().toString() | |
| 87 | const message = ts + contentHash | |
| 88 | const messageBytes = new TextEncoder().encode(message) | |
| 89 | ||
| 90 | // Detached signature of the message | |
| 91 | const sig = await ed.signAsync(messageBytes, seed) | |
| 92 | ||
| 93 | // Combined-mode envelope: signature(64 bytes) || message | |
| 94 | const combined = new Uint8Array(sig.length + messageBytes.length) | |
| 95 | combined.set(sig, 0) | |
| 96 | combined.set(messageBytes, sig.length) | |
| 97 | ||
| 98 | return pubKeyB64 + bytesToBase64(combined) | |
| 99 | }, | |
| 100 | ||
| 101 | /** | |
| 102 | * Verify a canonical ANProto signature and extract the signed data. | |
| 103 | * @param {string} message — wire-format signature | |
| 104 | * @returns {Promise<{ valid: boolean, publicKey: string, timestamp: number, hash: string }>} | |
| 105 | */ | |
| 106 | async open (message) { | |
| 107 | try { | |
| 108 | if (typeof message !== 'string' || message.length < 44) { | |
| 109 | return { valid: false, publicKey: null, timestamp: null, hash: null } | |
| 110 | } | |
| 111 | const publicKeyB64 = message.substring(0, 44) | |
| 112 | const publicKey = base64ToBytes(publicKeyB64) | |
| 113 | const combined = base64ToBytes(message.substring(44)) | |
| 114 | ||
| 115 | if (combined.length <= 64) { | |
| 116 | return { valid: false, publicKey: publicKeyB64, timestamp: null, hash: null } | |
| 117 | } | |
| 118 | ||
| 119 | const sig = combined.slice(0, 64) | |
| 120 | const body = combined.slice(64) | |
| 121 | ||
| 122 | const valid = await ed.verifyAsync(sig, body, publicKey) | |
| 123 | if (!valid) { | |
| 124 | return { valid: false, publicKey: publicKeyB64, timestamp: null, hash: null } | |
| 125 | } | |
| 126 | ||
| 127 | const bodyStr = new TextDecoder().decode(body) | |
| 128 | // ⛔ 13 digits exactly — see lib/anproto.js. A greedy match eats the hash's leading character | |
| 129 | // whenever it is a digit, which is ~1 in 6 messages. | |
| 130 | const tsMatch = bodyStr.match(/^(\d{13})/) | |
| 131 | const timestamp = tsMatch ? parseInt(tsMatch[0], 10) : null | |
| 132 | const hash = tsMatch ? bodyStr.substring(tsMatch[0].length) : bodyStr | |
| 133 | ||
| 134 | return { valid: true, publicKey: publicKeyB64, timestamp, hash } | |
| 135 | } catch { | |
| 136 | return { valid: false, publicKey: null, timestamp: null, hash: null } | |
| 137 | } | |
| 138 | } | |
| 139 | } | |
| 140 | ||
| 141 | // ── Encoding helpers ───────────────────────────────────────── | |
| 142 | ||
| 143 | function base64ToBytes (base64) { | |
| 144 | const binary = atob(base64) | |
| 145 | const bytes = new Uint8Array(binary.length) | |
| 146 | for (let i = 0; i < binary.length; i++) { | |
| 147 | bytes[i] = binary.charCodeAt(i) | |
| 148 | } | |
| 149 | return bytes | |
| 150 | } | |
| 151 | ||
| 152 | function bytesToBase64 (bytes) { | |
| 153 | const binary = String.fromCharCode(...bytes) | |
| 154 | return btoa(binary) | |
| 155 | } | |
| 156 | ||
| 157 | function bytesToHex (bytes) { | |
| 158 | return Array.from(bytes).map(b => b.toString(16).padStart(2, '0')).join('') | |
| 159 | } | |
| 160 | ||
| 161 | export default ANProtoClient |
| … | … | @@ -0,0 +1,9 @@ |
| 1 | /** | |
| 2 | * Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0. | |
| 3 | * Original file: /npm/@noble/ed25519@2.0.0/index.js | |
| 4 | * | |
| 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files | |
| 6 | */ | |
| 7 | /*! noble-ed25519 - MIT License (c) 2019 Paul Miller (paulmillr.com) */ | |
| 8 | const t=2n**255n-19n,e=2n**252n+27742317777372353535851937790883648493n,n=0x216936d3cd6e53fec0a4e231fdd6dc5c692cc7609525a7b2c9562d608f25d51an,r=0x6666666666666666666666666666666666666666666666666666666666666658n,s={a:-1n,d:37095705934669439343138083508754565189542113879843219016388785533085940283555n,p:t,n:e,h:8,Gx:n,Gy:r},o=(t="")=>{throw new Error(t)},i=t=>"string"==typeof t,a=(t,e)=>!(t instanceof Uint8Array)||"number"==typeof e&&e>0&&t.length!==e?o("Uint8Array expected"):t,c=t=>new Uint8Array(t),l=(t,e)=>a(i(t)?m(t):c(t),e),u=(e,n=t)=>{let r=e%n;return r>=0n?r:n+r},d=t=>t instanceof y?t:o("Point expected");let h;class y{constructor(t,e,n,r){this.ex=t,this.ey=e,this.ez=n,this.et=r}static fromAffine(t){return new y(t.x,t.y,1n,u(t.x*t.y))}static fromHex(e,n=!0){const{d:r}=s,i=(e=l(e,32)).slice();i[31]=-129&e[31];const a=w(i);0n===a||(!n||0n<a&&a<t||o("bad y coord 1"),n||0n<a&&a<2n**256n||o("bad y coord 2"));const c=u(a*a),d=u(c-1n),h=u(r*c+1n);let{isValid:f,value:p}=E(d,h);f||o("bad y coordinate 3");const x=1n===(1n&p);return!!(128&e[31])!==x&&(p=u(-p)),new y(p,a,1n,u(p*a))}get x(){return this.toAffine().x}get y(){return this.toAffine().y}equals(t){const{ex:e,ey:n,ez:r}=this,{ex:s,ey:o,ez:i}=d(t),a=u(e*i),c=u(s*r),l=u(n*i),h=u(o*r);return a===c&&l===h}is0(){return this.equals(p)}negate(){return new y(u(-this.ex),this.ey,this.ez,u(-this.et))}double(){const{ex:t,ey:e,ez:n}=this,{a:r}=s,o=u(t*t),i=u(e*e),a=u(2n*u(n*n)),c=u(r*o),l=t+e,d=u(u(l*l)-o-i),h=c+i,f=h-a,p=c-i,x=u(d*f),b=u(h*p),m=u(d*p),g=u(f*h);return new y(x,b,g,m)}add(t){const{ex:e,ey:n,ez:r,et:o}=this,{ex:i,ey:a,ez:c,et:l}=d(t),{a:h,d:f}=s,p=u(e*i),x=u(n*a),b=u(o*f*l),m=u(r*c),g=u((e+n)*(i+a)-p-x),w=u(m-b),B=u(m+b),A=u(x-h*p),v=u(g*w),R=u(B*A),E=u(g*A),S=u(w*B);return new y(v,R,S,E)}mul(t,n=!0){if(0n===t)return!0===n?o("cannot multiply by 0"):p;if("bigint"==typeof t&&0n<t&&t<e||o("invalid scalar, must be < L"),!n&&this.is0()||1n===t)return this;if(this.equals(f))return M(t).p;let r=p,s=f;for(let e=this;t>0n;e=e.double(),t>>=1n)1n&t?r=r.add(e):n&&(s=s.add(e));return r}multiply(t){return this.mul(t)}clearCofactor(){return this.mul(BigInt(s.h),!1)}isSmallOrder(){return this.clearCofactor().is0()}isTorsionFree(){let t=this.mul(e/2n,!1).double();return t=t.add(this),t.is0()}toAffine(){const{ex:t,ey:e,ez:n}=this;if(this.is0())return{x:0n,y:0n};const r=A(n);return 1n!==u(n*r)&&o("invalid inverse"),{x:u(t*r),y:u(e*r)}}toRawBytes(){const{x:t,y:e}=this.toAffine(),n=g(e);return n[31]|=1n&t?128:0,n}toHex(){return b(this.toRawBytes())}}y.BASE=new y(n,r,1n,u(n*r)),y.ZERO=new y(0n,1n,1n,0n);const{BASE:f,ZERO:p}=y,x=(t,e)=>t.toString(16).padStart(e,"0"),b=t=>Array.from(t).map((t=>x(t,2))).join(""),m=t=>{const e=t.length;(!i(t)||e%2)&&o("hex invalid 1");const n=c(e/2);for(let e=0;e<n.length;e++){const r=2*e,s=t.slice(r,r+2),i=Number.parseInt(s,16);(Number.isNaN(i)||i<0)&&o("hex invalid 2"),n[e]=i}return n},g=t=>m(x(t,64)).reverse(),w=t=>BigInt("0x"+b(c(a(t)).reverse())),B=(...t)=>{const e=c(t.reduce(((t,e)=>t+a(e).length),0));let n=0;return t.forEach((t=>{e.set(t,n),n+=t.length})),e},A=(e,n=t)=>{(0n===e||n<=0n)&&o("no inverse n="+e+" mod="+n);let r=u(e,n),s=n,i=0n,a=1n;for(;0n!==r;){const t=s%r,e=i-a*(s/r);s=r,r=t,i=a,a=e}return 1n===s?u(i,n):o("no inverse")},v=(e,n)=>{let r=e;for(;n-- >0n;)r*=r,r%=t;return r},R=19681161376707505956807079304988542015446066515923890162744021073123829784752n,E=(e,n)=>{const r=u(n*n*n),s=(e=>{const n=e*e%t*e%t,r=v(n,2n)*n%t,s=v(r,1n)*e%t,o=v(s,5n)*s%t,i=v(o,10n)*o%t,a=v(i,20n)*i%t,c=v(a,40n)*a%t,l=v(c,80n)*c%t,u=v(l,80n)*c%t,d=v(u,10n)*o%t;return{pow_p_5_8:v(d,2n)*e%t,b2:n}})(e*u(r*r*n)).pow_p_5_8;let o=u(e*r*s);const i=u(n*o*o),a=o,c=u(o*R),l=i===e,d=i===u(-e),h=i===u(-e*R);return l&&(o=a),(d||h)&&(o=c),1n===(1n&u(o))&&(o=u(-o)),{isValid:l||d,value:o}},S=t=>u(w(t),e);let z;const H=(...t)=>F.sha512Async(...t),P=(...t)=>"function"==typeof z?z(...t):o("etc.sha512Sync not set"),T=t=>{const e=t.slice(0,32);e[0]&=248,e[31]&=127,e[31]|=64;const n=t.slice(32,64),r=S(e),s=f.mul(r),o=s.toRawBytes();return{head:e,prefix:n,scalar:r,point:s,pointBytes:o}},_=t=>H(l(t,32)).then(T),I=t=>T(P(l(t,32))),N=t=>_(t).then((t=>t.pointBytes)),O=t=>I(t).pointBytes;function V(t,e){return t?H(e.hashable).then(e.finish):e.finish(P(e.hashable))}const j=(t,n,r)=>{const{pointBytes:s,scalar:o}=t,i=S(n),c=f.mul(i).toRawBytes();return{hashable:B(c,s,r),finish:t=>{const n=u(i+S(t)*o,e);return a(B(c,g(n)),64)}}},q=async(t,e)=>{const n=l(t),r=await _(e),s=await H(r.prefix,n);return V(!0,j(r,s,n))},C=(t,e)=>{const n=l(t),r=I(e),s=P(r.prefix,n);return V(!1,j(r,s,n))},K=(t,e,n)=>{e=l(e),t=l(t,64);const r=y.fromHex(n,!1),s=y.fromHex(t.slice(0,32),!1),o=w(t.slice(32,64)),i=f.mul(o,!1);return{hashable:B(s.toRawBytes(),r.toRawBytes(),e),finish:t=>{const e=S(t);return s.add(r.mul(e,!1)).add(i.negate()).clearCofactor().is0()}}},U=async(t,e,n)=>V(!0,K(t,e,n)),G=(t,e,n)=>V(!1,K(t,e,n)),Z=()=>"object"==typeof globalThis&&"crypto"in globalThis?globalThis.crypto:void 0,F={bytesToHex:b,hexToBytes:m,concatBytes:B,mod:u,invert:A,randomBytes:t=>{const e=Z();return e||o("crypto.getRandomValues must be defined"),e.getRandomValues(c(t))},sha512Async:async(...t)=>{const e=Z();e||o("crypto.subtle or etc.sha512Async must be defined");const n=B(...t);return c(await e.subtle.digest("SHA-512",n.buffer))},sha512Sync:void 0};Object.defineProperties(F,{sha512Sync:{configurable:!1,get:()=>z,set(t){z||(z=t)}}});const L={getExtendedPublicKeyAsync:_,getExtendedPublicKey:I,randomPrivateKey:()=>F.randomBytes(32),precompute:(t=8,e=f)=>(e.multiply(3n),e)},M=t=>{const e=h||(h=(()=>{const t=[];let e=f,n=e;for(let r=0;r<33;r++){n=e,t.push(n);for(let r=1;r<128;r++)n=n.add(e),t.push(n);e=n.double()}return t})()),n=(t,e)=>{let n=e.negate();return t?n:e};let r=p,s=f;const o=BigInt(255),i=BigInt(8);for(let a=0;a<33;a++){const c=128*a;let l=Number(t&o);t>>=i,l>128&&(l-=256,t+=1n);const u=c,d=c+Math.abs(l)-1,h=a%2!=0,y=l<0;0===l?s=s.add(n(h,e[u])):r=r.add(n(y,e[d]))}return{p:r,f:s}};export{s as CURVE,y as ExtendedPoint,F as etc,O as getPublicKey,N as getPublicKeyAsync,C as sign,q as signAsync,L as utils,G as verify,U as verifyAsync};export default null; | |
| 9 | //# sourceMappingURL=/sm/cd01a92102818c7c46dbe0b9ea67492dd1f0856266ce83a45f80684e6e019347.map |
| … | … | @@ -0,0 +1,8 @@ |
| 1 | /** | |
| 2 | * Bundled by jsDelivr using Rollup v2.79.2 and Terser v5.39.0. | |
| 3 | * Original file: /npm/@noble/hashes@1.3.0/esm/sha512.js | |
| 4 | * | |
| 5 | * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files | |
| 6 | */ | |
| 7 | import"./noble-crypto.js";function t(t){if(!Number.isSafeInteger(t)||t<0)throw new Error(`Wrong positive integer: ${t}`)}function e(t,...e){if(!(t instanceof Uint8Array))throw new TypeError("Expected Uint8Array");if(e.length>0&&!e.includes(t.length))throw new TypeError(`Expected Uint8Array of length ${e}, not of length=${t.length}`)}const s={number:t,bool:function(t){if("boolean"!=typeof t)throw new Error(`Expected boolean, not ${t}`)},bytes:e,hash:function(e){if("function"!=typeof e||"function"!=typeof e.create)throw new Error("Hash should be wrapped by utils.wrapConstructor");t(e.outputLen),t(e.blockLen)},exists:function(t,e=!0){if(t.destroyed)throw new Error("Hash instance has been destroyed");if(e&&t.finished)throw new Error("Hash#digest() has already been called")},output:function(t,s){e(t);const h=s.outputLen;if(t.length<h)throw new Error(`digestInto() expects output buffer of length at least ${h}`)}},h=t=>new DataView(t.buffer,t.byteOffset,t.byteLength);if(!(68===new Uint8Array(new Uint32Array([287454020]).buffer)[0]))throw new Error("Non little-endian hardware is not supported");function i(t){if("string"==typeof t&&(t=function(t){if("string"!=typeof t)throw new TypeError("utf8ToBytes expected string, got "+typeof t);return(new TextEncoder).encode(t)}(t)),!(t instanceof Uint8Array))throw new TypeError(`Expected input type is Uint8Array (got ${typeof t})`);return t}Array.from({length:256},((t,e)=>e.toString(16).padStart(2,"0")));class r{clone(){return this._cloneInto()}}function n(t){const e=e=>t().update(i(e)).digest(),s=t();return e.outputLen=s.outputLen,e.blockLen=s.blockLen,e.create=()=>t(),e}class o extends r{constructor(t,e,s,i){super(),this.blockLen=t,this.outputLen=e,this.padOffset=s,this.isLE=i,this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.buffer=new Uint8Array(t),this.view=h(this.buffer)}update(t){s.exists(this);const{view:e,buffer:r,blockLen:n}=this,o=(t=i(t)).length;for(let s=0;s<o;){const i=Math.min(n-this.pos,o-s);if(i!==n)r.set(t.subarray(s,s+i),this.pos),this.pos+=i,s+=i,this.pos===n&&(this.process(e,0),this.pos=0);else{const e=h(t);for(;n<=o-s;s+=n)this.process(e,s)}}return this.length+=t.length,this.roundClean(),this}digestInto(t){s.exists(this),s.output(t,this),this.finished=!0;const{buffer:e,view:i,blockLen:r,isLE:n}=this;let{pos:o}=this;e[o++]=128,this.buffer.subarray(o).fill(0),this.padOffset>r-o&&(this.process(i,0),o=0);for(let t=o;t<r;t++)e[t]=0;!function(t,e,s,h){if("function"==typeof t.setBigUint64)return t.setBigUint64(e,s,h);const i=BigInt(32),r=BigInt(4294967295),n=Number(s>>i&r),o=Number(s&r),c=h?4:0,a=h?0:4;t.setUint32(e+c,n,h),t.setUint32(e+a,o,h)}(i,r-8,BigInt(8*this.length),n),this.process(i,0);const c=h(t),a=this.outputLen;if(a%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const f=a/4,d=this.get();if(f>d.length)throw new Error("_sha2: outputLen bigger than state");for(let t=0;t<f;t++)c.setUint32(4*t,d[t],n)}digest(){const{buffer:t,outputLen:e}=this;this.digestInto(t);const s=t.slice(0,e);return this.destroy(),s}_cloneInto(t){t||(t=new this.constructor),t.set(...this.get());const{blockLen:e,buffer:s,length:h,finished:i,destroyed:r,pos:n}=this;return t.length=h,t.pos=n,t.finished=i,t.destroyed=r,h%e&&t.buffer.set(s),t}}const c=BigInt(2**32-1),a=BigInt(32);function f(t,e=!1){return e?{h:Number(t&c),l:Number(t>>a&c)}:{h:0|Number(t>>a&c),l:0|Number(t&c)}}const d={fromBig:f,split:function(t,e=!1){let s=new Uint32Array(t.length),h=new Uint32Array(t.length);for(let i=0;i<t.length;i++){const{h:r,l:n}=f(t[i],e);[s[i],h[i]]=[r,n]}return[s,h]},toBig:(t,e)=>BigInt(t>>>0)<<a|BigInt(e>>>0),shrSH:(t,e,s)=>t>>>s,shrSL:(t,e,s)=>t<<32-s|e>>>s,rotrSH:(t,e,s)=>t>>>s|e<<32-s,rotrSL:(t,e,s)=>t<<32-s|e>>>s,rotrBH:(t,e,s)=>t<<64-s|e>>>s-32,rotrBL:(t,e,s)=>t>>>s-32|e<<64-s,rotr32H:(t,e)=>e,rotr32L:(t,e)=>t,rotlSH:(t,e,s)=>t<<s|e>>>32-s,rotlSL:(t,e,s)=>e<<s|t>>>32-s,rotlBH:(t,e,s)=>e<<s-32|t>>>64-s,rotlBL:(t,e,s)=>t<<s-32|e>>>64-s,add:function(t,e,s,h){const i=(e>>>0)+(h>>>0);return{h:t+s+(i/2**32|0)|0,l:0|i}},add3L:(t,e,s)=>(t>>>0)+(e>>>0)+(s>>>0),add3H:(t,e,s,h)=>e+s+h+(t/2**32|0)|0,add4L:(t,e,s,h)=>(t>>>0)+(e>>>0)+(s>>>0)+(h>>>0),add4H:(t,e,s,h,i)=>e+s+h+i+(t/2**32|0)|0,add5H:(t,e,s,h,i,r)=>e+s+h+i+r+(t/2**32|0)|0,add5L:(t,e,s,h,i)=>(t>>>0)+(e>>>0)+(s>>>0)+(h>>>0)+(i>>>0)},[l,b]=d.split(["0x428a2f98d728ae22","0x7137449123ef65cd","0xb5c0fbcfec4d3b2f","0xe9b5dba58189dbbc","0x3956c25bf348b538","0x59f111f1b605d019","0x923f82a4af194f9b","0xab1c5ed5da6d8118","0xd807aa98a3030242","0x12835b0145706fbe","0x243185be4ee4b28c","0x550c7dc3d5ffb4e2","0x72be5d74f27b896f","0x80deb1fe3b1696b1","0x9bdc06a725c71235","0xc19bf174cf692694","0xe49b69c19ef14ad2","0xefbe4786384f25e3","0x0fc19dc68b8cd5b5","0x240ca1cc77ac9c65","0x2de92c6f592b0275","0x4a7484aa6ea6e483","0x5cb0a9dcbd41fbd4","0x76f988da831153b5","0x983e5152ee66dfab","0xa831c66d2db43210","0xb00327c898fb213f","0xbf597fc7beef0ee4","0xc6e00bf33da88fc2","0xd5a79147930aa725","0x06ca6351e003826f","0x142929670a0e6e70","0x27b70a8546d22ffc","0x2e1b21385c26c926","0x4d2c6dfc5ac42aed","0x53380d139d95b3df","0x650a73548baf63de","0x766a0abb3c77b2a8","0x81c2c92e47edaee6","0x92722c851482353b","0xa2bfe8a14cf10364","0xa81a664bbc423001","0xc24b8b70d0f89791","0xc76c51a30654be30","0xd192e819d6ef5218","0xd69906245565a910","0xf40e35855771202a","0x106aa07032bbd1b8","0x19a4c116b8d2d0c8","0x1e376c085141ab53","0x2748774cdf8eeb99","0x34b0bcb5e19b48a8","0x391c0cb3c5c95a63","0x4ed8aa4ae3418acb","0x5b9cca4f7763e373","0x682e6ff3d6b2b8a3","0x748f82ee5defb2fc","0x78a5636f43172f60","0x84c87814a1f0ab72","0x8cc702081a6439ec","0x90befffa23631e28","0xa4506cebde82bde9","0xbef9a3f7b2c67915","0xc67178f2e372532b","0xca273eceea26619c","0xd186b8c721c0c207","0xeada7dd6cde0eb1e","0xf57d4f7fee6ed178","0x06f067aa72176fba","0x0a637dc5a2c898a6","0x113f9804bef90dae","0x1b710b35131c471b","0x28db77f523047d84","0x32caab7b40c72493","0x3c9ebe0a15c9bebc","0x431d67c49c100d4c","0x4cc5d4becb3e42b6","0x597f299cfc657e2a","0x5fcb6fab3ad6faec","0x6c44198c4a475817"].map((t=>BigInt(t)))),u=new Uint32Array(80),x=new Uint32Array(80);class p extends o{constructor(){super(128,64,16,!1),this.Ah=1779033703,this.Al=-205731576,this.Bh=-1150833019,this.Bl=-2067093701,this.Ch=1013904242,this.Cl=-23791573,this.Dh=-1521486534,this.Dl=1595750129,this.Eh=1359893119,this.El=-1377402159,this.Fh=-1694144372,this.Fl=725511199,this.Gh=528734635,this.Gl=-79577749,this.Hh=1541459225,this.Hl=327033209}get(){const{Ah:t,Al:e,Bh:s,Bl:h,Ch:i,Cl:r,Dh:n,Dl:o,Eh:c,El:a,Fh:f,Fl:d,Gh:l,Gl:b,Hh:u,Hl:x}=this;return[t,e,s,h,i,r,n,o,c,a,f,d,l,b,u,x]}set(t,e,s,h,i,r,n,o,c,a,f,d,l,b,u,x){this.Ah=0|t,this.Al=0|e,this.Bh=0|s,this.Bl=0|h,this.Ch=0|i,this.Cl=0|r,this.Dh=0|n,this.Dl=0|o,this.Eh=0|c,this.El=0|a,this.Fh=0|f,this.Fl=0|d,this.Gh=0|l,this.Gl=0|b,this.Hh=0|u,this.Hl=0|x}process(t,e){for(let s=0;s<16;s++,e+=4)u[s]=t.getUint32(e),x[s]=t.getUint32(e+=4);for(let t=16;t<80;t++){const e=0|u[t-15],s=0|x[t-15],h=d.rotrSH(e,s,1)^d.rotrSH(e,s,8)^d.shrSH(e,s,7),i=d.rotrSL(e,s,1)^d.rotrSL(e,s,8)^d.shrSL(e,s,7),r=0|u[t-2],n=0|x[t-2],o=d.rotrSH(r,n,19)^d.rotrBH(r,n,61)^d.shrSH(r,n,6),c=d.rotrSL(r,n,19)^d.rotrBL(r,n,61)^d.shrSL(r,n,6),a=d.add4L(i,c,x[t-7],x[t-16]),f=d.add4H(a,h,o,u[t-7],u[t-16]);u[t]=0|f,x[t]=0|a}let{Ah:s,Al:h,Bh:i,Bl:r,Ch:n,Cl:o,Dh:c,Dl:a,Eh:f,El:p,Fh:g,Fl:w,Gh:L,Gl:H,Hh:y,Hl:B}=this;for(let t=0;t<80;t++){const e=d.rotrSH(f,p,14)^d.rotrSH(f,p,18)^d.rotrBH(f,p,41),E=d.rotrSL(f,p,14)^d.rotrSL(f,p,18)^d.rotrBL(f,p,41),A=f&g^~f&L,S=p&w^~p&H,C=d.add5L(B,E,S,b[t],x[t]),U=d.add5H(C,y,e,A,l[t],u[t]),D=0|C,F=d.rotrSH(s,h,28)^d.rotrBH(s,h,34)^d.rotrBH(s,h,39),G=d.rotrSL(s,h,28)^d.rotrBL(s,h,34)^d.rotrBL(s,h,39),m=s&i^s&n^i&n,I=h&r^h&o^r&o;y=0|L,B=0|H,L=0|g,H=0|w,g=0|f,w=0|p,({h:f,l:p}=d.add(0|c,0|a,0|U,0|D)),c=0|n,a=0|o,n=0|i,o=0|r,i=0|s,r=0|h;const N=d.add3L(D,G,I);s=d.add3H(N,U,F,m),h=0|N}({h:s,l:h}=d.add(0|this.Ah,0|this.Al,0|s,0|h)),({h:i,l:r}=d.add(0|this.Bh,0|this.Bl,0|i,0|r)),({h:n,l:o}=d.add(0|this.Ch,0|this.Cl,0|n,0|o)),({h:c,l:a}=d.add(0|this.Dh,0|this.Dl,0|c,0|a)),({h:f,l:p}=d.add(0|this.Eh,0|this.El,0|f,0|p)),({h:g,l:w}=d.add(0|this.Fh,0|this.Fl,0|g,0|w)),({h:L,l:H}=d.add(0|this.Gh,0|this.Gl,0|L,0|H)),({h:y,l:B}=d.add(0|this.Hh,0|this.Hl,0|y,0|B)),this.set(s,h,i,r,n,o,c,a,f,p,g,w,L,H,y,B)}roundClean(){u.fill(0),x.fill(0)}destroy(){this.buffer.fill(0),this.set(0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0)}}class g extends p{constructor(){super(),this.Ah=-1942145080,this.Al=424955298,this.Bh=1944164710,this.Bl=-1982016298,this.Ch=502970286,this.Cl=855612546,this.Dh=1738396948,this.Dl=1479516111,this.Eh=258812777,this.El=2077511080,this.Fh=2011393907,this.Fl=79989058,this.Gh=1067287976,this.Gl=1780299464,this.Hh=286451373,this.Hl=-1848208735,this.outputLen=28}}class w extends p{constructor(){super(),this.Ah=573645204,this.Al=-64227540,this.Bh=-1621794909,this.Bl=-934517566,this.Ch=596883563,this.Cl=1867755857,this.Dh=-1774684391,this.Dl=1497426621,this.Eh=-1775747358,this.El=-1467023389,this.Fh=-1101128155,this.Fl=1401305490,this.Gh=721525244,this.Gl=746961066,this.Hh=246885852,this.Hl=-2117784414,this.outputLen=32}}class L extends p{constructor(){super(),this.Ah=-876896931,this.Al=-1056596264,this.Bh=1654270250,this.Bl=914150663,this.Ch=-1856437926,this.Cl=812702999,this.Dh=355462360,this.Dl=-150054599,this.Eh=1731405415,this.El=-4191439,this.Fh=-1900787065,this.Fl=1750603025,this.Gh=-619958771,this.Gl=1694076839,this.Hh=1203062813,this.Hl=-1090891868,this.outputLen=48}}const H=n((()=>new p)),y=n((()=>new g)),B=n((()=>new w)),E=n((()=>new L));export{p as SHA512,E as sha384,H as sha512,y as sha512_224,B as sha512_256};export default null; | |
| 8 | //# sourceMappingURL=/sm/25ef7ec32d52ab376d05edb8d663f6f9885235f7c4d8ebc8cb0aab306fd143bb.map |
| … | … | @@ -0,0 +1,123 @@ |
| 1 | // The gate on the operator surface. | |
| 2 | // | |
| 3 | // ⛔ WHY THIS EXISTS AT ALL. The surface has to be reachable from the network, and browsers.js is | |
| 4 | // explicit that plain first-claim TOFU is safe only while the surface binds loopback — "exposing it | |
| 5 | // before claiming it hands it to whoever is on the network." So the first claim is bound to a bootstrap | |
| 6 | // token printed in the service log, and everything else is bound to a live session. | |
| 7 | ||
| 8 | import test from 'brittle' | |
| 9 | import Corestore from 'corestore' | |
| 10 | import fs from 'bare-fs' | |
| 11 | import os from 'bare-os' | |
| 12 | import { join } from 'bare-path' | |
| 13 | import { an } from 'anproto' | |
| 14 | import { Browsers } from 'favela-peer/lib/browsers.js' | |
| 15 | import { MemoryRegistrations } from '../lib/registrations.js' | |
| 16 | import { serveAdmin } from '../lib/admin-surface.js' | |
| 17 | import { Sessions, COOKIE } from '../lib/admin-auth.js' | |
| 18 | ||
| 19 | const ROOT = new URL('..', import.meta.url).pathname | |
| 20 | const TOKEN = 'b7f4c1a9e2d05638b7f4c1a9e2d05638' | |
| 21 | const tmp = () => fs.mkdtempSync(join(os.tmpdir(), 'ap-admin-')) | |
| 22 | ||
| 23 | async function ctx ({ bootstrap = TOKEN } = {}) { | |
| 24 | const browsers = new Browsers(new Corestore(tmp()), { bootstrap }) | |
| 25 | await browsers.ready() | |
| 26 | const registrations = new MemoryRegistrations({ policy: 'open' }) | |
| 27 | await registrations.claim({ username: 'fleeky', root: 'a'.repeat(64) }) | |
| 28 | return { | |
| 29 | browsers, | |
| 30 | registrations, | |
| 31 | sessions: new Sessions(), | |
| 32 | inbound: null, | |
| 33 | root: ROOT, | |
| 34 | config: { host: 'ap.7rnx.net', registration: 'open', inboundCap: 500 } | |
| 35 | } | |
| 36 | } | |
| 37 | ||
| 38 | async function browser () { | |
| 39 | const keypair = await an.gen() | |
| 40 | return { | |
| 41 | publicKey: an.publicKey(keypair), | |
| 42 | async proof (c) { | |
| 43 | const { challenge } = c.browsers.challenge() | |
| 44 | return { publicKey: an.publicKey(keypair), challenge, signature: await an.sign(await an.hash(challenge), keypair) } | |
| 45 | } | |
| 46 | } | |
| 47 | } | |
| 48 | ||
| 49 | const req = (method, path, extra = {}) => ({ method, path, headers: {}, ...extra }) | |
| 50 | ||
| 51 | test('★★★ every page redirects to sign-in without a session', async (t) => { | |
| 52 | const c = await ctx() | |
| 53 | for (const path of ['/admin', '/admin/', '/']) { | |
| 54 | const res = await serveAdmin(req('GET', path), c) | |
| 55 | t.is(res.status, 303, path + ' is gated') | |
| 56 | t.is(res.headers.location, '/admin/login') | |
| 57 | } | |
| 58 | }) | |
| 59 | ||
| 60 | test('★★★ a proved key alone cannot claim the bridge — the token is required', async (t) => { | |
| 61 | const c = await ctx() | |
| 62 | const who = await browser() | |
| 63 | const res = await serveAdmin(req('POST', '/admin/session', { form: await who.proof(c) }), c) | |
| 64 | t.is(res.status, 403) | |
| 65 | t.ok(/bootstrap token/.test(res.body), res.body) | |
| 66 | t.absent(res.headers['set-cookie'], 'and no session was handed out') | |
| 67 | }) | |
| 68 | ||
| 69 | test('★★★ the token claims it, and the cookie is HttpOnly + SameSite=Strict', async (t) => { | |
| 70 | const c = await ctx() | |
| 71 | const who = await browser() | |
| 72 | const res = await serveAdmin(req('POST', '/admin/session', { form: { ...(await who.proof(c)), bootstrap: TOKEN } }), c) | |
| 73 | t.is(res.status, 200) | |
| 74 | const cookie = res.headers['set-cookie'] | |
| 75 | t.ok(cookie && cookie.startsWith(COOKIE + '=')) | |
| 76 | t.ok(/HttpOnly/.test(cookie), 'a page cannot read it') | |
| 77 | t.ok(/SameSite=Strict/.test(cookie), 'and it does not ride cross-site requests') | |
| 78 | }) | |
| 79 | ||
| 80 | test('★★★ with the session the surface opens, and shows the operator their users', async (t) => { | |
| 81 | const c = await ctx() | |
| 82 | const who = await browser() | |
| 83 | const login = await serveAdmin(req('POST', '/admin/session', { form: { ...(await who.proof(c)), bootstrap: TOKEN } }), c) | |
| 84 | const id = login.headers['set-cookie'].split(';')[0].split('=')[1] | |
| 85 | ||
| 86 | const res = await serveAdmin(req('GET', '/admin', { headers: { cookie: COOKIE + '=' + id } }), c) | |
| 87 | t.is(res.status, 200) | |
| 88 | t.ok(/fleeky@ap\.7rnx\.net/.test(res.body), 'the sanity check the surface exists for') | |
| 89 | }) | |
| 90 | ||
| 91 | test('★★★ a revoked browser loses the surface on its NEXT request', async (t) => { | |
| 92 | // ⛔ The cookie is not the authority. Holding a session must never outlive the grant behind it, so | |
| 93 | // the registry is re-asked per request rather than trusted at login. | |
| 94 | const c = await ctx() | |
| 95 | const who = await browser() | |
| 96 | const login = await serveAdmin(req('POST', '/admin/session', { form: { ...(await who.proof(c)), bootstrap: TOKEN } }), c) | |
| 97 | const id = login.headers['set-cookie'].split(';')[0].split('=')[1] | |
| 98 | const cookie = { cookie: COOKIE + '=' + id } | |
| 99 | ||
| 100 | t.is((await serveAdmin(req('GET', '/admin', { headers: cookie }), c)).status, 200, 'in') | |
| 101 | await c.browsers.revoke(who.publicKey) | |
| 102 | t.is((await serveAdmin(req('GET', '/admin', { headers: cookie }), c)).status, 303, 'and out again immediately') | |
| 103 | }) | |
| 104 | ||
| 105 | test('★★ the browser identity client is served, and nothing else is', async (t) => { | |
| 106 | const c = await ctx() | |
| 107 | const ok = await serveAdmin(req('GET', '/static/anproto-client.js'), c) | |
| 108 | t.is(ok.status, 200) | |
| 109 | t.ok(/ANProtoClient/.test(String(ok.body))) | |
| 110 | ||
| 111 | // ⛔ A fixed list, not a directory join — a path off the wire joined to a root is a traversal bug. | |
| 112 | for (const evil of ['/static/../lib/admin.js', '/static/../../../etc/passwd', '/static/index.js']) { | |
| 113 | t.is((await serveAdmin(req('GET', evil), c)).status, 404, evil + ' is refused') | |
| 114 | } | |
| 115 | }) | |
| 116 | ||
| 117 | test('★★ sign-in is reachable without a session, and says what it needs', async (t) => { | |
| 118 | const c = await ctx() | |
| 119 | const res = await serveAdmin(req('GET', '/admin/login'), c) | |
| 120 | t.is(res.status, 200) | |
| 121 | t.ok(/bootstrap token/i.test(res.body), 'an unclaimed bridge asks for the token') | |
| 122 | t.ok(/anproto-client\.js/.test(res.body), 'and loads the same identity client the other apps use') | |
| 123 | }) |