OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge commits / f3e3f52

0.9.95: reachable on a LAN name, because the client no longer needs a secure context
⛔ THE REAL BLOCKER WAS crypto.subtle, NOT THE BIND. static/anproto-client.js hashed with `crypto.subtle.digest`, which browsers expose only in a SECURE CONTEXT — HTTPS or localhost. Served at http://stortron.local:3401 the login page loaded a client whose hashing was undefined, and sign-in failed with nothing on the page or in the log to say why. Binding to the network without fixing that would have produced exactly that. The file had already made this move once, for signing: "Wire pure-JS SHA-512 into noble-ed25519 (replaces crypto.subtle dependency)". This finishes it. noble-sha256 is vendored from the same source and version as the sha512 beside it, and VERIFIED byte-identical to the server's `an.hash` for ascii, unicode and the empty string — if those ever diverge a signature verifies and then fails the hash check, which lands one layer from the cause. ⛔ AND THE SERVED MODULE GRAPH HAD A HOLE. The allowlist held the three files the client imports DIRECTLY, while two of those import ./noble-crypto.js — so the entry point served and its dependency 404'd. A missing transitive import and a missing crypto.subtle look identical from a browser: a sign-in page that does nothing. The test now WALKS the imports rather than restating a list, so adding a dependency without serving it fails here instead of in a browser. ⚠ One test measured the wrong thing first: it grepped raw text for `crypto.subtle` and failed on the comment explaining why the code no longer calls it. Comments are stripped now — the claim is about what the client CALLS. AP_ADMIN_BIND is its own knob, defaulting to loopback and NEVER inheriting config.bind: this deployment sets AP_BIND=0.0.0.0 for the fediverse listener, and inheriting it would publish the operator surface as a side effect of making the public one reachable. Widened to 0.0.0.0 here deliberately, and it is only safe because the surface is authenticated — a key proves itself, the first claim needs the bootstrap token from the service log, everything after needs an invite, and a revoke lands on the next request. 1029/1029 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
f3e3f52190f143b5accf2e2849f7bc29affb331e
by plankatron <fleeky@gmail.com> · 2026-09-11T23:53:25.000Z

Changed files (9) +225 -110

.pgh-lock.json +95 -95
……@@ -1,9 +1,9 @@
11{
22 "name": "ap-bridge",
3 "version": "0.9.93",
3 "version": "0.9.94",
44 "runtime": "bare",
5 "rootTreeHash": "6d10ca5949e4d7523e108ecbe1d6f04a4ece408318276e1492686c8cf5cd6c3f",
6 "publishedAt": "2026-09-11T23:30:52.409Z",
5 "rootTreeHash": "6fe6b3e99fd039bef4761d288db9df4948e96d97ef0354a6b5ccfb8eb2c1e60f",
6 "publishedAt": "2026-09-11T23:48:07.594Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -2396,8 +2396,8 @@
23962396 {
23972397 "name": "favela-peer",
23982398 "version": "0.0.56",
2399 "tree": "fb3415c5c01dcc8d31eb0b218ef450eb0bbfa85ff41de769763f4b82666af874",
2400 "normHash": "2a447b52145af4ac66941b9cc7f8bfe8852bb18ce031bbae2bd63df955f47756",
2399 "tree": "d42374919d053f6d95762c9f0cab06f295ee806239d05ff87cdb56eeef98c40c",
2400 "normHash": "332b4cf1dace21ba46cfd711602680d5f1f29a08b21592225ca674619fa6b336",
24012401 "workspace": true
24022402 },
24032403 {
……@@ -4004,8 +4004,8 @@
40044004 "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37"
40054005 }
40064006 ],
4007 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4008 "acceptedAt": "2026-09-11T23:30:52.432Z"
4007 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4008 "acceptedAt": "2026-09-11T23:48:07.627Z"
40094009 },
40104010 {
40114011 "type": "VERSION_CONFLICT",
……@@ -4019,8 +4019,8 @@
40194019 "tree": "ee91b6bd77101e3404ca571b2e5adf0f47029d174398a05f50294fc207e39d99"
40204020 }
40214021 ],
4022 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4023 "acceptedAt": "2026-09-11T23:30:52.432Z"
4022 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4023 "acceptedAt": "2026-09-11T23:48:07.627Z"
40244024 },
40254025 {
40264026 "type": "VERSION_CONFLICT",
……@@ -4034,8 +4034,8 @@
40344034 "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11"
40354035 }
40364036 ],
4037 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4038 "acceptedAt": "2026-09-11T23:30:52.432Z"
4037 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4038 "acceptedAt": "2026-09-11T23:48:07.627Z"
40394039 },
40404040 {
40414041 "type": "VERSION_CONFLICT",
……@@ -4054,8 +4054,8 @@
40544054 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
40554055 }
40564056 ],
4057 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4058 "acceptedAt": "2026-09-11T23:30:52.432Z"
4057 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4058 "acceptedAt": "2026-09-11T23:48:07.627Z"
40594059 },
40604060 {
40614061 "type": "VERSION_CONFLICT",
……@@ -4069,8 +4069,8 @@
40694069 "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd"
40704070 }
40714071 ],
4072 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4073 "acceptedAt": "2026-09-11T23:30:52.432Z"
4072 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4073 "acceptedAt": "2026-09-11T23:48:07.627Z"
40744074 },
40754075 {
40764076 "type": "VERSION_CONFLICT",
……@@ -4084,8 +4084,8 @@
40844084 "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95"
40854085 }
40864086 ],
4087 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4088 "acceptedAt": "2026-09-11T23:30:52.432Z"
4087 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4088 "acceptedAt": "2026-09-11T23:48:07.627Z"
40894089 },
40904090 {
40914091 "type": "VERSION_CONFLICT",
……@@ -4099,8 +4099,8 @@
40994099 "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb"
41004100 }
41014101 ],
4102 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4103 "acceptedAt": "2026-09-11T23:30:52.432Z"
4102 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4103 "acceptedAt": "2026-09-11T23:48:07.627Z"
41044104 },
41054105 {
41064106 "type": "VERSION_CONFLICT",
……@@ -4114,8 +4114,8 @@
41144114 "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a"
41154115 }
41164116 ],
4117 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4118 "acceptedAt": "2026-09-11T23:30:52.432Z"
4117 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4118 "acceptedAt": "2026-09-11T23:48:07.627Z"
41194119 },
41204120 {
41214121 "type": "VERSION_CONFLICT",
……@@ -4129,8 +4129,8 @@
41294129 "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca"
41304130 }
41314131 ],
4132 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4133 "acceptedAt": "2026-09-11T23:30:52.432Z"
4132 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4133 "acceptedAt": "2026-09-11T23:48:07.627Z"
41344134 },
41354135 {
41364136 "type": "VERSION_CONFLICT",
……@@ -4144,8 +4144,8 @@
41444144 "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701"
41454145 }
41464146 ],
4147 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4148 "acceptedAt": "2026-09-11T23:30:52.432Z"
4147 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4148 "acceptedAt": "2026-09-11T23:48:07.627Z"
41494149 },
41504150 {
41514151 "type": "VERSION_CONFLICT",
……@@ -4159,8 +4159,8 @@
41594159 "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d"
41604160 }
41614161 ],
4162 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4163 "acceptedAt": "2026-09-11T23:30:52.432Z"
4162 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4163 "acceptedAt": "2026-09-11T23:48:07.627Z"
41644164 },
41654165 {
41664166 "type": "VERSION_CONFLICT",
……@@ -4174,8 +4174,8 @@
41744174 "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04"
41754175 }
41764176 ],
4177 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4178 "acceptedAt": "2026-09-11T23:30:52.432Z"
4177 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4178 "acceptedAt": "2026-09-11T23:48:07.627Z"
41794179 },
41804180 {
41814181 "type": "VERSION_CONFLICT",
……@@ -4189,8 +4189,8 @@
41894189 "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4"
41904190 }
41914191 ],
4192 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4193 "acceptedAt": "2026-09-11T23:30:52.432Z"
4192 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4193 "acceptedAt": "2026-09-11T23:48:07.627Z"
41944194 },
41954195 {
41964196 "type": "VERSION_CONFLICT",
……@@ -4204,8 +4204,8 @@
42044204 "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df"
42054205 }
42064206 ],
4207 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4208 "acceptedAt": "2026-09-11T23:30:52.432Z"
4207 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4208 "acceptedAt": "2026-09-11T23:48:07.627Z"
42094209 },
42104210 {
42114211 "type": "VERSION_CONFLICT",
……@@ -4219,8 +4219,8 @@
42194219 "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad"
42204220 }
42214221 ],
4222 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4223 "acceptedAt": "2026-09-11T23:30:52.432Z"
4222 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4223 "acceptedAt": "2026-09-11T23:48:07.627Z"
42244224 },
42254225 {
42264226 "type": "VERSION_CONFLICT",
……@@ -4234,8 +4234,8 @@
42344234 "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e"
42354235 }
42364236 ],
4237 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4238 "acceptedAt": "2026-09-11T23:30:52.432Z"
4237 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4238 "acceptedAt": "2026-09-11T23:48:07.627Z"
42394239 },
42404240 {
42414241 "type": "VERSION_CONFLICT",
……@@ -4264,8 +4264,8 @@
42644264 "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c"
42654265 }
42664266 ],
4267 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4268 "acceptedAt": "2026-09-11T23:30:52.432Z"
4267 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4268 "acceptedAt": "2026-09-11T23:48:07.627Z"
42694269 },
42704270 {
42714271 "type": "VERSION_CONFLICT",
……@@ -4279,8 +4279,8 @@
42794279 "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9"
42804280 }
42814281 ],
4282 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4283 "acceptedAt": "2026-09-11T23:30:52.432Z"
4282 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4283 "acceptedAt": "2026-09-11T23:48:07.627Z"
42844284 },
42854285 {
42864286 "type": "VERSION_CONFLICT",
……@@ -4294,8 +4294,8 @@
42944294 "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169"
42954295 }
42964296 ],
4297 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4298 "acceptedAt": "2026-09-11T23:30:52.432Z"
4297 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4298 "acceptedAt": "2026-09-11T23:48:07.627Z"
42994299 },
43004300 {
43014301 "type": "VERSION_CONFLICT",
……@@ -4309,8 +4309,8 @@
43094309 "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b"
43104310 }
43114311 ],
4312 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4313 "acceptedAt": "2026-09-11T23:30:52.432Z"
4312 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4313 "acceptedAt": "2026-09-11T23:48:07.627Z"
43144314 },
43154315 {
43164316 "type": "VERSION_CONFLICT",
……@@ -4324,8 +4324,8 @@
43244324 "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed"
43254325 }
43264326 ],
4327 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4328 "acceptedAt": "2026-09-11T23:30:52.432Z"
4327 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4328 "acceptedAt": "2026-09-11T23:48:07.627Z"
43294329 },
43304330 {
43314331 "type": "VERSION_CONFLICT",
……@@ -4339,8 +4339,8 @@
43394339 "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f"
43404340 }
43414341 ],
4342 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4343 "acceptedAt": "2026-09-11T23:30:52.432Z"
4342 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4343 "acceptedAt": "2026-09-11T23:48:07.627Z"
43444344 },
43454345 {
43464346 "type": "VERSION_CONFLICT",
……@@ -4354,8 +4354,8 @@
43544354 "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754"
43554355 }
43564356 ],
4357 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4358 "acceptedAt": "2026-09-11T23:30:52.432Z"
4357 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4358 "acceptedAt": "2026-09-11T23:48:07.627Z"
43594359 },
43604360 {
43614361 "type": "VERSION_CONFLICT",
……@@ -4369,8 +4369,8 @@
43694369 "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5"
43704370 }
43714371 ],
4372 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4373 "acceptedAt": "2026-09-11T23:30:52.432Z"
4372 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4373 "acceptedAt": "2026-09-11T23:48:07.627Z"
43744374 },
43754375 {
43764376 "type": "VERSION_CONFLICT",
……@@ -4389,8 +4389,8 @@
43894389 "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393"
43904390 }
43914391 ],
4392 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4393 "acceptedAt": "2026-09-11T23:30:52.432Z"
4392 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4393 "acceptedAt": "2026-09-11T23:48:07.627Z"
43944394 },
43954395 {
43964396 "type": "VERSION_CONFLICT",
……@@ -4404,8 +4404,8 @@
44044404 "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5"
44054405 }
44064406 ],
4407 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4408 "acceptedAt": "2026-09-11T23:30:52.432Z"
4407 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4408 "acceptedAt": "2026-09-11T23:48:07.627Z"
44094409 },
44104410 {
44114411 "type": "VERSION_CONFLICT",
……@@ -4419,8 +4419,8 @@
44194419 "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6"
44204420 }
44214421 ],
4422 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4423 "acceptedAt": "2026-09-11T23:30:52.432Z"
4422 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4423 "acceptedAt": "2026-09-11T23:48:07.627Z"
44244424 },
44254425 {
44264426 "type": "VERSION_CONFLICT",
……@@ -4434,8 +4434,8 @@
44344434 "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9"
44354435 }
44364436 ],
4437 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4438 "acceptedAt": "2026-09-11T23:30:52.432Z"
4437 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4438 "acceptedAt": "2026-09-11T23:48:07.627Z"
44394439 },
44404440 {
44414441 "type": "VERSION_CONFLICT",
……@@ -4449,8 +4449,8 @@
44494449 "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0"
44504450 }
44514451 ],
4452 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4453 "acceptedAt": "2026-09-11T23:30:52.432Z"
4452 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4453 "acceptedAt": "2026-09-11T23:48:07.627Z"
44544454 },
44554455 {
44564456 "type": "VERSION_CONFLICT",
……@@ -4464,8 +4464,8 @@
44644464 "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53"
44654465 }
44664466 ],
4467 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4468 "acceptedAt": "2026-09-11T23:30:52.432Z"
4467 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4468 "acceptedAt": "2026-09-11T23:48:07.627Z"
44694469 },
44704470 {
44714471 "type": "VERSION_CONFLICT",
……@@ -4479,8 +4479,8 @@
44794479 "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a"
44804480 }
44814481 ],
4482 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4483 "acceptedAt": "2026-09-11T23:30:52.432Z"
4482 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4483 "acceptedAt": "2026-09-11T23:48:07.627Z"
44844484 },
44854485 {
44864486 "type": "VERSION_CONFLICT",
……@@ -4499,8 +4499,8 @@
44994499 "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648"
45004500 }
45014501 ],
4502 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4503 "acceptedAt": "2026-09-11T23:30:52.432Z"
4502 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4503 "acceptedAt": "2026-09-11T23:48:07.627Z"
45044504 },
45054505 {
45064506 "type": "VERSION_CONFLICT",
……@@ -4514,8 +4514,8 @@
45144514 "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57"
45154515 }
45164516 ],
4517 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4518 "acceptedAt": "2026-09-11T23:30:52.432Z"
4517 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4518 "acceptedAt": "2026-09-11T23:48:07.627Z"
45194519 },
45204520 {
45214521 "type": "VERSION_CONFLICT",
……@@ -4529,8 +4529,8 @@
45294529 "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78"
45304530 }
45314531 ],
4532 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4533 "acceptedAt": "2026-09-11T23:30:52.432Z"
4532 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4533 "acceptedAt": "2026-09-11T23:48:07.627Z"
45344534 },
45354535 {
45364536 "type": "VERSION_CONFLICT",
……@@ -4544,8 +4544,8 @@
45444544 "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9"
45454545 }
45464546 ],
4547 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4548 "acceptedAt": "2026-09-11T23:30:52.432Z"
4547 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4548 "acceptedAt": "2026-09-11T23:48:07.627Z"
45494549 },
45504550 {
45514551 "type": "VERSION_CONFLICT",
……@@ -4559,8 +4559,8 @@
45594559 "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972"
45604560 }
45614561 ],
4562 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4563 "acceptedAt": "2026-09-11T23:30:52.432Z"
4562 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4563 "acceptedAt": "2026-09-11T23:48:07.627Z"
45644564 },
45654565 {
45664566 "type": "VERSION_CONFLICT",
……@@ -4574,8 +4574,8 @@
45744574 "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519"
45754575 }
45764576 ],
4577 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4578 "acceptedAt": "2026-09-11T23:30:52.432Z"
4577 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4578 "acceptedAt": "2026-09-11T23:48:07.627Z"
45794579 },
45804580 {
45814581 "type": "VERSION_CONFLICT",
……@@ -4589,8 +4589,8 @@
45894589 "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390"
45904590 }
45914591 ],
4592 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4593 "acceptedAt": "2026-09-11T23:30:52.432Z"
4592 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4593 "acceptedAt": "2026-09-11T23:48:07.627Z"
45944594 },
45954595 {
45964596 "type": "VERSION_CONFLICT",
……@@ -4604,8 +4604,8 @@
46044604 "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706"
46054605 }
46064606 ],
4607 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4608 "acceptedAt": "2026-09-11T23:30:52.432Z"
4607 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4608 "acceptedAt": "2026-09-11T23:48:07.627Z"
46094609 },
46104610 {
46114611 "type": "VERSION_CONFLICT",
……@@ -4619,8 +4619,8 @@
46194619 "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb"
46204620 }
46214621 ],
4622 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4623 "acceptedAt": "2026-09-11T23:30:52.432Z"
4622 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4623 "acceptedAt": "2026-09-11T23:48:07.627Z"
46244624 },
46254625 {
46264626 "type": "VERSION_CONFLICT",
……@@ -4634,8 +4634,8 @@
46344634 "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae"
46354635 }
46364636 ],
4637 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4638 "acceptedAt": "2026-09-11T23:30:52.432Z"
4637 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4638 "acceptedAt": "2026-09-11T23:48:07.627Z"
46394639 },
46404640 {
46414641 "type": "VERSION_CONFLICT",
……@@ -4654,8 +4654,8 @@
46544654 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765"
46554655 }
46564656 ],
4657 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4658 "acceptedAt": "2026-09-11T23:30:52.432Z"
4657 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4658 "acceptedAt": "2026-09-11T23:48:07.627Z"
46594659 },
46604660 {
46614661 "type": "VERSION_CONFLICT",
……@@ -4669,8 +4669,8 @@
46694669 "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60"
46704670 }
46714671 ],
4672 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4673 "acceptedAt": "2026-09-11T23:30:52.432Z"
4672 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4673 "acceptedAt": "2026-09-11T23:48:07.627Z"
46744674 },
46754675 {
46764676 "type": "VERSION_CONFLICT",
……@@ -4684,8 +4684,8 @@
46844684 "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a"
46854685 }
46864686 ],
4687 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4688 "acceptedAt": "2026-09-11T23:30:52.432Z"
4687 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4688 "acceptedAt": "2026-09-11T23:48:07.627Z"
46894689 },
46904690 {
46914691 "type": "VERSION_CONFLICT",
……@@ -4699,8 +4699,8 @@
46994699 "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b"
47004700 }
47014701 ],
4702 "reason": "ap-bridge 0.9.93 — admin surface enabled on the deployment",
4703 "acceptedAt": "2026-09-11T23:30:52.432Z"
4702 "reason": "ap-bridge 0.9.94 — authenticated operator surface",
4703 "acceptedAt": "2026-09-11T23:48:07.627Z"
47044704 }
47054705 ]
47064706}
index.js +5 -4
……@@ -520,16 +520,17 @@ async function main () {
520520 })
521521 await new Promise((resolve, reject) => {
522522 adminServer.on('error', reject)
523 // ⛔ 127.0.0.1 HARDCODED, not config.bind. An operator who set AP_BIND to something routable for
524 // the public listener must not thereby publish the admin surface.
525 adminServer.listen(config.adminPort, '127.0.0.1', resolve)
523 // ⛔ ITS OWN BIND, never config.bind — this deployment sets that to 0.0.0.0 for the fediverse
524 // listener, and inheriting it would publish the operator surface as a side effect. Defaults to
525 // loopback; widening it is deliberate, and safe because the surface is authenticated.
526 adminServer.listen(config.adminPort, config.adminBind || '127.0.0.1', resolve)
526527 })
527528 // ⛔ STILL LOOPBACK EVEN THOUGH IT NOW HAS AUTH, and for a reason that is not caution:
528529 // static/anproto-client.js hashes with `crypto.subtle`, which browsers expose only in a SECURE
529530 // CONTEXT ("works on localhost + HTTPS" — its own comment). Bound to a public port over plain
530531 // HTTP this would serve a login page whose crypto is undefined in the browser that loaded it.
531532 // 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' })
533 log({ msg: 'admin surface (authenticated)', bind: (config.adminBind || '127.0.0.1') + ':' + config.adminPort })
533534 }
534535
535536 // ★ The first pass picks up whatever the last run left owed — which is the entire point of the
lib/admin-surface.js +7 -1
……@@ -17,7 +17,13 @@ const TYPES = { '.js': 'text/javascript; charset=utf-8' }
1717// The browser half of the identity layer. Vendored, and the same files favela-surface and pear-git
1818// serve — noble is pure JS so signing works without crypto.subtle; the SHA-256 in the client does use
1919// it, which is why this surface must be reached over HTTPS or localhost.
20const ASSETS = new Set(['anproto-client.js', 'noble-ed25519.js', 'noble-sha512.js'])
20// ⛔ THE TRANSITIVE SET, not just the entry. anproto-client.js imports noble-ed25519, noble-sha512 and
21// noble-sha256, and two of THOSE import noble-crypto — a list holding only the entry point serves a
22// module graph with a hole in it, and the browser reports a bare network error on an import rather than
23// anything about signing in. assets.test.js walks the imports and asserts this set is closed.
24const ASSETS = new Set([
25 'anproto-client.js', 'noble-ed25519.js', 'noble-sha512.js', 'noble-sha256.js', 'noble-crypto.js'
26])
2127
2228export function staticAsset (name, root) {
2329 if (!ASSETS.has(name)) return null
lib/config.js +11 -4
……@@ -39,9 +39,15 @@ const DEFAULTS = {
3939 // partially or not at all. So evicting here can destroy the only copy, and the cap should be
4040 // generous rather than tight.
4141 inboundCap: 500,
42 // The loopback admin UI. ABSENT means it is not served at all — an operator surface a bridge starts
43 // offering because nobody said not to is the same mistake as registration defaulting to open.
44 adminPort: null
42 // The admin UI. ABSENT means it is not served at all — an operator surface a bridge starts offering
43 // because nobody said not to is the same mistake as registration defaulting to open.
44 adminPort: null,
45 // ⛔ ITS OWN BIND, DEFAULTING TO LOOPBACK, AND NEVER config.bind. This deployment sets AP_BIND to
46 // 0.0.0.0 for the PUBLIC listener; inheriting that would publish the operator surface the moment
47 // somebody made the fediverse one reachable. Widening this is a separate, deliberate act — and it is
48 // only safe because the surface is authenticated: an ANProto key proves itself, the first claim needs
49 // the bootstrap token from the service log, and everything else needs an invite.
50 adminBind: '127.0.0.1'
4551}
4652
4753function expand (p) {
……@@ -70,7 +76,8 @@ export function loadConfig (env = process.env) {
7076 roomType: 'AP_ROOM_TYPE',
7177 // ⛔ 'open' or 'invite'. ABSENT means registration is off entirely and /ap/register 404s —
7278 // a bridge must not start handing out names on the operator's domain because nobody said not to.
73 registration: 'AP_REGISTRATION'
79 registration: 'AP_REGISTRATION',
80 adminBind: 'AP_ADMIN_BIND'
7481 }),
7582 ...pickInt(env, {
7683 port: 'AP_PORT',
package.json +3 -2
……@@ -1,6 +1,6 @@
11{
22 "name": "ap-bridge",
3 "version": "0.9.94",
3 "version": "0.9.95",
44 "description": "ActivityPub membrane — a second projection of the self-custody atproto peer",
55 "type": "module",
66 "main": "index.js",
……@@ -64,7 +64,8 @@
6464 "AP_DID": "did:plc:wj62ebbusln6gxo5azo6ogzo",
6565 "AP_BACKFILL": "20",
6666 "AP_REGISTRATION": "open",
67 "AP_ADMIN_PORT": "3401"
67 "AP_ADMIN_PORT": "3401",
68 "AP_ADMIN_BIND": "0.0.0.0"
6869 }
6970 },
7071 "readiness": {
static/anproto-client.js +11 -4
……@@ -10,6 +10,7 @@
1010
1111import * as ed from './noble-ed25519.js'
1212import { sha512 } from './noble-sha512.js'
13import { sha256 } from './noble-sha256.js'
1314
1415// Wire pure-JS SHA-512 into noble-ed25519 (replaces crypto.subtle dependency)
1516ed.etc.sha512Sync = (...msgs) => {
……@@ -56,14 +57,20 @@ export const ANProtoClient = {
5657 },
5758
5859 /**
59 * SHA-256 hash (uses crypto.subtle, works on localhost + HTTPS)
60 * SHA-256 hash — pure JS, so it works on plain HTTP as well as localhost and HTTPS
6061 * @param {string} data
6162 * @returns {Promise<string>} 44-char base64 hash
6263 */
6364 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))
65 // ⛔ PURE JS, NOT crypto.subtle. `crypto.subtle` exists only in a SECURE CONTEXT — HTTPS or
66 // localhost — so an operator page served over plain HTTP at http://stortron.local:3401 loaded a
67 // client whose hashing was undefined, and sign-in failed with no way to tell why from the page.
68 // The same file already made this move for ed25519 ("replaces crypto.subtle dependency"); this
69 // finishes it, and the surface no longer needs TLS to be reachable on a LAN name.
70 //
71 // ⚠ Verified byte-identical to the server's `an.hash` for ascii, unicode and the empty string —
72 // it must be, or every signature verifies and then fails the hash check.
73 return bytesToBase64(sha256(new TextEncoder().encode(data)))
6774 },
6875
6976 /**
static/noble-crypto.js +8 -0
……@@ -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/crypto.js
4 *
5 * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6 */
7const o="object"==typeof globalThis&&"crypto"in globalThis?globalThis.crypto:void 0;export{o as crypto};export default null;
8//# sourceMappingURL=/sm/c89c3eead1f3ff4004cc3626795fb3273c573384fdf3d96f44a89e5143a967f7.map
static/noble-sha256.js +8 -0
……@@ -0,0 +1,8 @@
1/**
2 * Bundled by jsDelivr using Rollup v4.62.2 and esbuild v0.28.1.
3 * Original file: /npm/@noble/hashes@1.3.0/esm/sha256.js
4 *
5 * Do NOT use SRI with dynamically generated files! More information: https://www.jsdelivr.com/using-sri-with-dynamic-files
6 */
7import"./noble-crypto.js";function w(t){if(!Number.isSafeInteger(t)||t<0)throw new Error(`Wrong positive integer: ${t}`)}function m(t){if(typeof t!="boolean")throw new Error(`Expected boolean, not ${t}`)}function U(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}`)}function C(t){if(typeof t!="function"||typeof t.create!="function")throw new Error("Hash should be wrapped by utils.wrapConstructor");w(t.outputLen),w(t.blockLen)}function I(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")}function T(t,e){U(t);const n=e.outputLen;if(t.length<n)throw new Error(`digestInto() expects output buffer of length at least ${n}`)}const y={number:w,bool:m,bytes:U,hash:C,exists:I,output:T},A=t=>new DataView(t.buffer,t.byteOffset,t.byteLength),a=(t,e)=>t<<32-e|t>>>e,_=new Uint8Array(new Uint32Array([287454020]).buffer)[0]===68;if(!_)throw new Error("Non little-endian hardware is not supported");Array.from({length:256},(t,e)=>e.toString(16).padStart(2,"0"));function S(t){if(typeof t!="string")throw new TypeError(`utf8ToBytes expected string, got ${typeof t}`);return new TextEncoder().encode(t)}function L(t){if(typeof t=="string"&&(t=S(t)),!(t instanceof Uint8Array))throw new TypeError(`Expected input type is Uint8Array (got ${typeof t})`);return t}class k{clone(){return this._cloneInto()}}function B(t){const e=s=>t().update(L(s)).digest(),n=t();return e.outputLen=n.outputLen,e.blockLen=n.blockLen,e.create=()=>t(),e}function D(t,e,n,s){if(typeof t.setBigUint64=="function")return t.setBigUint64(e,n,s);const o=BigInt(32),f=BigInt(4294967295),i=Number(n>>o&f),c=Number(n&f),h=s?4:0,x=s?0:4;t.setUint32(e+h,i,s),t.setUint32(e+x,c,s)}class $ extends k{constructor(e,n,s,o){super(),this.blockLen=e,this.outputLen=n,this.padOffset=s,this.isLE=o,this.finished=!1,this.length=0,this.pos=0,this.destroyed=!1,this.buffer=new Uint8Array(e),this.view=A(this.buffer)}update(e){y.exists(this);const{view:n,buffer:s,blockLen:o}=this;e=L(e);const f=e.length;for(let i=0;i<f;){const c=Math.min(o-this.pos,f-i);if(c===o){const h=A(e);for(;o<=f-i;i+=o)this.process(h,i);continue}s.set(e.subarray(i,i+c),this.pos),this.pos+=c,i+=c,this.pos===o&&(this.process(n,0),this.pos=0)}return this.length+=e.length,this.roundClean(),this}digestInto(e){y.exists(this),y.output(e,this),this.finished=!0;const{buffer:n,view:s,blockLen:o,isLE:f}=this;let{pos:i}=this;n[i++]=128,this.buffer.subarray(i).fill(0),this.padOffset>o-i&&(this.process(s,0),i=0);for(let r=i;r<o;r++)n[r]=0;D(s,o-8,BigInt(this.length*8),f),this.process(s,0);const c=A(e),h=this.outputLen;if(h%4)throw new Error("_sha2: outputLen should be aligned to 32bit");const x=h/4,u=this.get();if(x>u.length)throw new Error("_sha2: outputLen bigger than state");for(let r=0;r<x;r++)c.setUint32(4*r,u[r],f)}digest(){const{buffer:e,outputLen:n}=this;this.digestInto(e);const s=e.slice(0,n);return this.destroy(),s}_cloneInto(e){e||(e=new this.constructor),e.set(...this.get());const{blockLen:n,buffer:s,length:o,finished:f,destroyed:i,pos:c}=this;return e.length=o,e.pos=c,e.finished=f,e.destroyed=i,o%n&&e.buffer.set(s),e}}const F=(t,e,n)=>t&e^~t&n,G=(t,e,n)=>t&e^t&n^e&n,N=new Uint32Array([1116352408,1899447441,3049323471,3921009573,961987163,1508970993,2453635748,2870763221,3624381080,310598401,607225278,1426881987,1925078388,2162078206,2614888103,3248222580,3835390401,4022224774,264347078,604807628,770255983,1249150122,1555081692,1996064986,2554220882,2821834349,2952996808,3210313671,3336571891,3584528711,113926993,338241895,666307205,773529912,1294757372,1396182291,1695183700,1986661051,2177026350,2456956037,2730485921,2820302411,3259730800,3345764771,3516065817,3600352804,4094571909,275423344,430227734,506948616,659060556,883997877,958139571,1322822218,1537002063,1747873779,1955562222,2024104815,2227730452,2361852424,2428436474,2756734187,3204031479,3329325298]),b=new Uint32Array([1779033703,3144134277,1013904242,2773480762,1359893119,2600822924,528734635,1541459225]),p=new Uint32Array(64);class H extends ${constructor(){super(64,32,8,!1),this.A=b[0]|0,this.B=b[1]|0,this.C=b[2]|0,this.D=b[3]|0,this.E=b[4]|0,this.F=b[5]|0,this.G=b[6]|0,this.H=b[7]|0}get(){const{A:e,B:n,C:s,D:o,E:f,F:i,G:c,H:h}=this;return[e,n,s,o,f,i,c,h]}set(e,n,s,o,f,i,c,h){this.A=e|0,this.B=n|0,this.C=s|0,this.D=o|0,this.E=f|0,this.F=i|0,this.G=c|0,this.H=h|0}process(e,n){for(let r=0;r<16;r++,n+=4)p[r]=e.getUint32(n,!1);for(let r=16;r<64;r++){const l=p[r-15],d=p[r-2],E=a(l,7)^a(l,18)^l>>>3,g=a(d,17)^a(d,19)^d>>>10;p[r]=g+p[r-7]+E+p[r-16]|0}let{A:s,B:o,C:f,D:i,E:c,F:h,G:x,H:u}=this;for(let r=0;r<64;r++){const l=a(c,6)^a(c,11)^a(c,25),d=u+l+F(c,h,x)+N[r]+p[r]|0,g=(a(s,2)^a(s,13)^a(s,22))+G(s,o,f)|0;u=x,x=h,h=c,c=i+d|0,i=f,f=o,o=s,s=d+g|0}s=s+this.A|0,o=o+this.B|0,f=f+this.C|0,i=i+this.D|0,c=c+this.E|0,h=h+this.F|0,x=x+this.G|0,u=u+this.H|0,this.set(s,o,f,i,c,h,x,u)}roundClean(){p.fill(0)}destroy(){this.set(0,0,0,0,0,0,0,0),this.buffer.fill(0)}}class V extends H{constructor(){super(),this.A=-1056596264,this.B=914150663,this.C=812702999,this.D=-150054599,this.E=-4191439,this.F=1750603025,this.G=1694076839,this.H=-1090891868,this.outputLen=28}}const W=B(()=>new H),M=B(()=>new V);export{M as sha224,W as sha256};
8//# sourceMappingURL=/sm/ca62abd53b825446bc284d916083e5a679dcf43aa71d5d6019708a75e9cf6ebd.map
test/admin-assets.test.js +77 -0
……@@ -0,0 +1,77 @@
1// The served module graph must be CLOSED, and the hashing must need no secure context.
2//
3// ⛔ TWO FAILURES THAT LOOK THE SAME FROM A BROWSER. A missing transitive import and a missing
4// `crypto.subtle` both surface as a sign-in page that does nothing — no server log, no useful console
5// line, nothing the operator can act on. Both were real here:
6//
7// 1. The allowlist held only the three files anproto-client.js imports DIRECTLY, while two of those
8// import ./noble-crypto.js. The entry point served fine and its dependency 404'd.
9// 2. hash() used `crypto.subtle`, which exists only on HTTPS or localhost. Over plain HTTP at
10// http://stortron.local:3401 — which is the whole point of this surface — it is undefined.
11
12// ⛔ FIRST — ap-bridge's own globals shim, for the same reason index.js imports it first: deps touch
13// TextEncoder at module load and bare does not define it.
14import '../lib/bare-globals.js'
15import test from 'brittle'
16import fs from 'bare-fs'
17import { join } from 'bare-path'
18import { staticAsset } from '../lib/admin-surface.js'
19
20const ROOT = new URL('..', import.meta.url).pathname
21const ENTRY = 'anproto-client.js'
22
23// Every `import ... from './x.js'` / `import "./x.js"` in a served file.
24function localImports (src) {
25 const out = new Set()
26 for (const m of src.matchAll(/import\s*(?:[^'"]*?from\s*)?['"]\.\/([^'"]+)['"]/g)) out.add(m[1])
27 return out
28}
29
30test('★★★ every module the client pulls in is itself served', (t) => {
31 // ⛔ WALKED, not listed. A hand-kept list of "the files it needs" is the thing that went wrong; this
32 // follows the imports so adding a dependency without serving it fails here rather than in a browser.
33 const seen = new Set()
34 const queue = [ENTRY]
35 while (queue.length) {
36 const name = queue.pop()
37 if (seen.has(name)) continue
38 seen.add(name)
39
40 const served = staticAsset(name, ROOT)
41 t.ok(served, name + ' is served')
42 if (!served) continue
43 for (const dep of localImports(String(served.body))) queue.push(dep)
44 }
45 t.ok(seen.size >= 4, 'the graph has real depth (' + seen.size + ' modules), so this is not vacuous')
46 t.ok(seen.has('noble-crypto.js'), 'including the transitive one the first list forgot')
47})
48
49// ⚠ Comments are stripped first. The claim is about what the client CALLS, and the file quite
50// deliberately explains in prose why it no longer calls it — a grep over raw text fails on its own
51// explanation, which is a test measuring the wrong thing rather than a real finding.
52const codeOf = (src) => src.replace(/\/\*[\s\S]*?\*\//g, '').replace(/^\s*\/\/.*$/gm, '').replace(/\/\/.*$/gm, '')
53
54test('★★★ the client needs no secure context to hash', (t) => {
55 const code = codeOf(String(staticAsset(ENTRY, ROOT).body))
56 t.absent(/crypto\s*\.\s*subtle/.test(code),
57 'crypto.subtle exists only on HTTPS or localhost — this surface is reached on a LAN name over HTTP')
58 t.ok(/sha256\(/.test(code), 'it hashes with the vendored pure-JS implementation instead')
59})
60
61test('★★ nothing outside the graph is reachable', (t) => {
62 for (const evil of ['../lib/admin.js', '../package.json', '../../favela-peer/lib/browsers.js', 'index.js']) {
63 t.absent(staticAsset(evil, ROOT), evil + ' is not served')
64 }
65})
66
67test('★★ the vendored hash is the one the server verifies against', async (t) => {
68 // ⚠ If these ever diverge the signature VERIFIES and then fails the hash check — the failure lands
69 // one layer away from the cause, which is why it is asserted rather than assumed.
70 const { sha256 } = await import('../static/noble-sha256.js')
71 const { an } = await import('anproto')
72 const b4a = (await import('b4a')).default
73 for (const s of ['hello', '', 'a challenge string']) {
74 const mine = b4a.toString(b4a.from(sha256(new TextEncoder().encode(s))), 'base64')
75 t.is(mine, await an.hash(s), 'sha256 matches an.hash for ' + JSON.stringify(s))
76 }
77})