OverviewFilesCommitsIssuesDocsReleasesPeers

ap-bridge commits / c920ea6

0.9.98: `ap-bridge admin` — a CLI surface that can actually be installed
⛔ MY FIRST VERSION SHIPPED A FILE NOBODY COULD RUN. bin/ap-bridge-admin.js existed in the tree, was absent from PATH, and the command I documented silently did not exist — the clumsy invocation I gave was the symptom, not the fix. pgh installs exactly ONE launcher per app (`pgh.bin` is a single name; schema.js rejects anything else), and that launcher is `exec … index.js "$@"`. So arguments already arrive at the entry: the surface is a SUBCOMMAND. ap-bridge admin status | invite | revoke <pubkey> ⛔ DISPATCHED BEFORE ANYTHING BINDS, and it exits rather than falling through — running the CLI must not also bring up the fediverse listener of a bridge that is already running. Asserted by position in the source, because the consequence is a second daemon rather than an error. ⛔ THE SAME dataDir DERIVATION AS THE DAEMON, via loadConfig (which expands ~ and ${HOME}) and the identical fallback. Disagreeing by one character would make the CLI report "no control socket" on a bridge that is running and serving it. ⚠ loadConfig, NOT assertUsable: the CLI must work on a bridge whose keys or host are misconfigured. Refusing to hand out a recovery invite because the thing needing recovery is broken would be exactly backwards. `invite` puts the token alone on stdout and everything else on stderr, so `| pbcopy` copies a token and not a paragraph. An unreachable socket names the path it tried AND that AP_ADMIN_PORT gates the control socket — the likeliest cause, and a bare "connection refused" would send an operator looking at the wrong thing. Revoked operators are listed, never filtered: still registered is why revoking the claimant does not reopen the instance. 1047/1047 pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
c920ea6038ccf4526b149a9fa0c8557d18d4db42
by plankatron <fleeky@gmail.com> · 2026-09-12T00:25:31.000Z

Changed files (6) +293 -160

.pgh-lock.json +93 -93
……@@ -1,9 +1,9 @@
11{
22 "name": "ap-bridge",
3 "version": "0.9.96",
3 "version": "0.9.97",
44 "runtime": "bare",
5 "rootTreeHash": "3db8ca6d8191db17fd7047baf444777c3efa9b793110fc9fef1857bc8b9ec30c",
6 "publishedAt": "2026-09-11T23:59:28.794Z",
5 "rootTreeHash": "ca0bba4ddbe4fbc98dea11b202aa9dee4d63b1f90c646a3a705772968b3432d0",
6 "publishedAt": "2026-09-12T00:16:58.238Z",
77 "deps": [
88 {
99 "name": "@hyperswarm/secret-stream",
……@@ -4004,8 +4004,8 @@
40044004 "tree": "38f44af48b2d59b46f1b5db31b81d02f3ca8b8bad6bc0fb9fffff698241f5e37"
40054005 }
40064006 ],
4007 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4008 "acceptedAt": "2026-09-11T23:59:28.817Z"
4007 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4008 "acceptedAt": "2026-09-12T00:16:58.261Z"
40094009 },
40104010 {
40114011 "type": "VERSION_CONFLICT",
……@@ -4019,8 +4019,8 @@
40194019 "tree": "ee91b6bd77101e3404ca571b2e5adf0f47029d174398a05f50294fc207e39d99"
40204020 }
40214021 ],
4022 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4023 "acceptedAt": "2026-09-11T23:59:28.817Z"
4022 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4023 "acceptedAt": "2026-09-12T00:16:58.261Z"
40244024 },
40254025 {
40264026 "type": "VERSION_CONFLICT",
……@@ -4034,8 +4034,8 @@
40344034 "tree": "3bb78d8f06263a549bd1077f37a7df257b1d557e44608631435af625f05a9c11"
40354035 }
40364036 ],
4037 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4038 "acceptedAt": "2026-09-11T23:59:28.817Z"
4037 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4038 "acceptedAt": "2026-09-12T00:16:58.261Z"
40394039 },
40404040 {
40414041 "type": "VERSION_CONFLICT",
……@@ -4054,8 +4054,8 @@
40544054 "tree": "58c0bf8698af19464d0ee8354a77ed1ac96d3a5fa18aa4e794686cfbbf080e5d"
40554055 }
40564056 ],
4057 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4058 "acceptedAt": "2026-09-11T23:59:28.817Z"
4057 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4058 "acceptedAt": "2026-09-12T00:16:58.261Z"
40594059 },
40604060 {
40614061 "type": "VERSION_CONFLICT",
……@@ -4069,8 +4069,8 @@
40694069 "tree": "da4c6b2654e9d66f10b0faa989311cf182878c24ab51b1c41458cfbf37ad29cd"
40704070 }
40714071 ],
4072 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4073 "acceptedAt": "2026-09-11T23:59:28.817Z"
4072 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4073 "acceptedAt": "2026-09-12T00:16:58.261Z"
40744074 },
40754075 {
40764076 "type": "VERSION_CONFLICT",
……@@ -4084,8 +4084,8 @@
40844084 "tree": "40be8f559a13b54ebf0689e63e6c70f0d218035359e17c497ed7bc8a85c3cc95"
40854085 }
40864086 ],
4087 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4088 "acceptedAt": "2026-09-11T23:59:28.817Z"
4087 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4088 "acceptedAt": "2026-09-12T00:16:58.261Z"
40894089 },
40904090 {
40914091 "type": "VERSION_CONFLICT",
……@@ -4099,8 +4099,8 @@
40994099 "tree": "3eae85efce82299769898c8151b5ed0a94e5a5ebdbcdee2105b815ca733a61cb"
41004100 }
41014101 ],
4102 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4103 "acceptedAt": "2026-09-11T23:59:28.817Z"
4102 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4103 "acceptedAt": "2026-09-12T00:16:58.261Z"
41044104 },
41054105 {
41064106 "type": "VERSION_CONFLICT",
……@@ -4114,8 +4114,8 @@
41144114 "tree": "8d9e01d443a127781773598d2e77c3f372334fad09e20623a819dabe49b22e1a"
41154115 }
41164116 ],
4117 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4118 "acceptedAt": "2026-09-11T23:59:28.817Z"
4117 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4118 "acceptedAt": "2026-09-12T00:16:58.261Z"
41194119 },
41204120 {
41214121 "type": "VERSION_CONFLICT",
……@@ -4129,8 +4129,8 @@
41294129 "tree": "dc1ad6c999fe90d946ba0ef34698ea9af451b639be2ad1c5b61eabb08337a6ca"
41304130 }
41314131 ],
4132 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4133 "acceptedAt": "2026-09-11T23:59:28.817Z"
4132 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4133 "acceptedAt": "2026-09-12T00:16:58.262Z"
41344134 },
41354135 {
41364136 "type": "VERSION_CONFLICT",
……@@ -4144,8 +4144,8 @@
41444144 "tree": "88b4779570e5113aa21b204989ac018d1628b387681f7e53428a7c99114fb701"
41454145 }
41464146 ],
4147 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4148 "acceptedAt": "2026-09-11T23:59:28.817Z"
4147 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4148 "acceptedAt": "2026-09-12T00:16:58.262Z"
41494149 },
41504150 {
41514151 "type": "VERSION_CONFLICT",
……@@ -4159,8 +4159,8 @@
41594159 "tree": "720b867ec4300a3caa2325eb1449499f9940bfb88341ecceb5992fa0fdf5f37d"
41604160 }
41614161 ],
4162 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4163 "acceptedAt": "2026-09-11T23:59:28.817Z"
4162 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4163 "acceptedAt": "2026-09-12T00:16:58.262Z"
41644164 },
41654165 {
41664166 "type": "VERSION_CONFLICT",
……@@ -4174,8 +4174,8 @@
41744174 "tree": "b697fe174482a708b49d0764cd4786291845f0034a31e0699381ca7e7b269d04"
41754175 }
41764176 ],
4177 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4178 "acceptedAt": "2026-09-11T23:59:28.817Z"
4177 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4178 "acceptedAt": "2026-09-12T00:16:58.262Z"
41794179 },
41804180 {
41814181 "type": "VERSION_CONFLICT",
……@@ -4189,8 +4189,8 @@
41894189 "tree": "767f07242277ae80bd395de5a4969fcdbf956c216158ae517fb2b562558ac1a4"
41904190 }
41914191 ],
4192 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4193 "acceptedAt": "2026-09-11T23:59:28.817Z"
4192 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4193 "acceptedAt": "2026-09-12T00:16:58.262Z"
41944194 },
41954195 {
41964196 "type": "VERSION_CONFLICT",
……@@ -4204,8 +4204,8 @@
42044204 "tree": "de7deed21f587ffc8bde4971abe00fca77bb6c71c4e7effb31ad34ee330815df"
42054205 }
42064206 ],
4207 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4208 "acceptedAt": "2026-09-11T23:59:28.817Z"
4207 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4208 "acceptedAt": "2026-09-12T00:16:58.262Z"
42094209 },
42104210 {
42114211 "type": "VERSION_CONFLICT",
……@@ -4219,8 +4219,8 @@
42194219 "tree": "2df42517ebf73cd9c11f2f652f75e87401fd4ed7b856c0adc15c33baf1d4f1ad"
42204220 }
42214221 ],
4222 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4223 "acceptedAt": "2026-09-11T23:59:28.817Z"
4222 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4223 "acceptedAt": "2026-09-12T00:16:58.262Z"
42244224 },
42254225 {
42264226 "type": "VERSION_CONFLICT",
……@@ -4234,8 +4234,8 @@
42344234 "tree": "325aca171cbff560487177584078c8066133e91176639ca47c98d70e195aa92e"
42354235 }
42364236 ],
4237 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4238 "acceptedAt": "2026-09-11T23:59:28.817Z"
4237 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4238 "acceptedAt": "2026-09-12T00:16:58.262Z"
42394239 },
42404240 {
42414241 "type": "VERSION_CONFLICT",
……@@ -4264,8 +4264,8 @@
42644264 "tree": "89a40400106bf4fef95fb5c44a66ee29aada6f76c7740bfba09d8aeeb841f39c"
42654265 }
42664266 ],
4267 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4268 "acceptedAt": "2026-09-11T23:59:28.817Z"
4267 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4268 "acceptedAt": "2026-09-12T00:16:58.262Z"
42694269 },
42704270 {
42714271 "type": "VERSION_CONFLICT",
……@@ -4279,8 +4279,8 @@
42794279 "tree": "1bd0eb382789c62cd4622d47bffb60984fdd766260c79f73a2fc7a136f3edbe9"
42804280 }
42814281 ],
4282 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4283 "acceptedAt": "2026-09-11T23:59:28.817Z"
4282 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4283 "acceptedAt": "2026-09-12T00:16:58.262Z"
42844284 },
42854285 {
42864286 "type": "VERSION_CONFLICT",
……@@ -4294,8 +4294,8 @@
42944294 "tree": "4526fe9dc5468264c7171ec16e9303726674b7b44ca0b3e09cb8b9a6a1be6169"
42954295 }
42964296 ],
4297 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4298 "acceptedAt": "2026-09-11T23:59:28.817Z"
4297 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4298 "acceptedAt": "2026-09-12T00:16:58.262Z"
42994299 },
43004300 {
43014301 "type": "VERSION_CONFLICT",
……@@ -4309,8 +4309,8 @@
43094309 "tree": "32b291cd756e142da18a6ed01c351e024fcedc3e2b1ecfbfb85f3106ada4419b"
43104310 }
43114311 ],
4312 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4313 "acceptedAt": "2026-09-11T23:59:28.817Z"
4312 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4313 "acceptedAt": "2026-09-12T00:16:58.262Z"
43144314 },
43154315 {
43164316 "type": "VERSION_CONFLICT",
……@@ -4324,8 +4324,8 @@
43244324 "tree": "4a8cd3b0093092b33088617dc0415e8179882d7f2f13b7649962ca20b08beeed"
43254325 }
43264326 ],
4327 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4328 "acceptedAt": "2026-09-11T23:59:28.817Z"
4327 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4328 "acceptedAt": "2026-09-12T00:16:58.262Z"
43294329 },
43304330 {
43314331 "type": "VERSION_CONFLICT",
……@@ -4339,8 +4339,8 @@
43394339 "tree": "4f63f165ee6a581f689b40c4a40df4df01ab6fba17d46bd29e40ef5bbdd9884f"
43404340 }
43414341 ],
4342 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4343 "acceptedAt": "2026-09-11T23:59:28.817Z"
4342 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4343 "acceptedAt": "2026-09-12T00:16:58.262Z"
43444344 },
43454345 {
43464346 "type": "VERSION_CONFLICT",
……@@ -4354,8 +4354,8 @@
43544354 "tree": "735109c736c0bd3413ab47414fd8b6ea9a99c401fe8e6d608459c79293806754"
43554355 }
43564356 ],
4357 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4358 "acceptedAt": "2026-09-11T23:59:28.817Z"
4357 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4358 "acceptedAt": "2026-09-12T00:16:58.262Z"
43594359 },
43604360 {
43614361 "type": "VERSION_CONFLICT",
……@@ -4369,8 +4369,8 @@
43694369 "tree": "b32b75bf1b2acdd18ed0cc370d01d477fba42ba09fd1f2f78af3ebad925e72d5"
43704370 }
43714371 ],
4372 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4373 "acceptedAt": "2026-09-11T23:59:28.817Z"
4372 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4373 "acceptedAt": "2026-09-12T00:16:58.262Z"
43744374 },
43754375 {
43764376 "type": "VERSION_CONFLICT",
……@@ -4389,8 +4389,8 @@
43894389 "tree": "189397178121e9a18430943345dab820691648a324229071ceb980bbb77a7393"
43904390 }
43914391 ],
4392 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4393 "acceptedAt": "2026-09-11T23:59:28.817Z"
4392 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4393 "acceptedAt": "2026-09-12T00:16:58.262Z"
43944394 },
43954395 {
43964396 "type": "VERSION_CONFLICT",
……@@ -4404,8 +4404,8 @@
44044404 "tree": "3bedca5287a9041fc2fd5ec5ace4df1fa86240e6afec0a105ba67398bafc09a5"
44054405 }
44064406 ],
4407 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4408 "acceptedAt": "2026-09-11T23:59:28.817Z"
4407 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4408 "acceptedAt": "2026-09-12T00:16:58.262Z"
44094409 },
44104410 {
44114411 "type": "VERSION_CONFLICT",
……@@ -4419,8 +4419,8 @@
44194419 "tree": "b9ca9b272008bd67672a1b252390e8b527095c46b46cf6b6725a7a187eb038b6"
44204420 }
44214421 ],
4422 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4423 "acceptedAt": "2026-09-11T23:59:28.817Z"
4422 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4423 "acceptedAt": "2026-09-12T00:16:58.262Z"
44244424 },
44254425 {
44264426 "type": "VERSION_CONFLICT",
……@@ -4434,8 +4434,8 @@
44344434 "tree": "7793645d7d4248d6d6533e3cbcd4bf40bed4f38c678b0ca0606c15640ba5e7b9"
44354435 }
44364436 ],
4437 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4438 "acceptedAt": "2026-09-11T23:59:28.817Z"
4437 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4438 "acceptedAt": "2026-09-12T00:16:58.262Z"
44394439 },
44404440 {
44414441 "type": "VERSION_CONFLICT",
……@@ -4449,8 +4449,8 @@
44494449 "tree": "79447513d4ac0357c8d60bdc7b81b3c5143d761b26700340cba266c4d1cb77b0"
44504450 }
44514451 ],
4452 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4453 "acceptedAt": "2026-09-11T23:59:28.817Z"
4452 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4453 "acceptedAt": "2026-09-12T00:16:58.262Z"
44544454 },
44554455 {
44564456 "type": "VERSION_CONFLICT",
……@@ -4464,8 +4464,8 @@
44644464 "tree": "279d4e0447eb79fd75816864ba7f41f2df03080c9da69ea4603fe2900c8d9e53"
44654465 }
44664466 ],
4467 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4468 "acceptedAt": "2026-09-11T23:59:28.817Z"
4467 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4468 "acceptedAt": "2026-09-12T00:16:58.262Z"
44694469 },
44704470 {
44714471 "type": "VERSION_CONFLICT",
……@@ -4479,8 +4479,8 @@
44794479 "tree": "ed4e0b480417610e195ff0b59e5b9a453abbf2db6d6ce72d7cd522269f835e9a"
44804480 }
44814481 ],
4482 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4483 "acceptedAt": "2026-09-11T23:59:28.817Z"
4482 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4483 "acceptedAt": "2026-09-12T00:16:58.262Z"
44844484 },
44854485 {
44864486 "type": "VERSION_CONFLICT",
……@@ -4499,8 +4499,8 @@
44994499 "tree": "a9e07d54e526c838dfe5e27df33936682aefb218f61a45b6c15289243999f648"
45004500 }
45014501 ],
4502 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4503 "acceptedAt": "2026-09-11T23:59:28.817Z"
4502 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4503 "acceptedAt": "2026-09-12T00:16:58.262Z"
45044504 },
45054505 {
45064506 "type": "VERSION_CONFLICT",
……@@ -4514,8 +4514,8 @@
45144514 "tree": "9d84c86441fe138b227c4fd01d1e948aa83bf8d13cfb1aa957838feea05fbb57"
45154515 }
45164516 ],
4517 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4518 "acceptedAt": "2026-09-11T23:59:28.817Z"
4517 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4518 "acceptedAt": "2026-09-12T00:16:58.262Z"
45194519 },
45204520 {
45214521 "type": "VERSION_CONFLICT",
……@@ -4529,8 +4529,8 @@
45294529 "tree": "4a3afbe2fc7a882e24454eb2e7e7cfe6816811ee5d73ae0f92e8b9ea9cfb8c78"
45304530 }
45314531 ],
4532 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4533 "acceptedAt": "2026-09-11T23:59:28.817Z"
4532 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4533 "acceptedAt": "2026-09-12T00:16:58.262Z"
45344534 },
45354535 {
45364536 "type": "VERSION_CONFLICT",
……@@ -4544,8 +4544,8 @@
45444544 "tree": "1e5c5810d8971a3c27fde7fb0e4d633dcabbe2cf98d8df86b473f8d67eead8b9"
45454545 }
45464546 ],
4547 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4548 "acceptedAt": "2026-09-11T23:59:28.817Z"
4547 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4548 "acceptedAt": "2026-09-12T00:16:58.262Z"
45494549 },
45504550 {
45514551 "type": "VERSION_CONFLICT",
……@@ -4559,8 +4559,8 @@
45594559 "tree": "e3478f50e85ba0ac288efed83e84c992483904897dc39ccf243d20a4ea25d972"
45604560 }
45614561 ],
4562 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4563 "acceptedAt": "2026-09-11T23:59:28.817Z"
4562 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4563 "acceptedAt": "2026-09-12T00:16:58.262Z"
45644564 },
45654565 {
45664566 "type": "VERSION_CONFLICT",
……@@ -4574,8 +4574,8 @@
45744574 "tree": "c05d8bf7eab5024ea98140f78c84d12fb494b4b7b10c7eedeb50b2be842fa519"
45754575 }
45764576 ],
4577 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4578 "acceptedAt": "2026-09-11T23:59:28.817Z"
4577 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4578 "acceptedAt": "2026-09-12T00:16:58.262Z"
45794579 },
45804580 {
45814581 "type": "VERSION_CONFLICT",
……@@ -4589,8 +4589,8 @@
45894589 "tree": "0697bb338bbcec517353cffcdb2345a8232b9b90cc627a549a306f0323576390"
45904590 }
45914591 ],
4592 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4593 "acceptedAt": "2026-09-11T23:59:28.817Z"
4592 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4593 "acceptedAt": "2026-09-12T00:16:58.262Z"
45944594 },
45954595 {
45964596 "type": "VERSION_CONFLICT",
……@@ -4604,8 +4604,8 @@
46044604 "tree": "a366a73e74f8d80f106465058083b62bb70b833c721047124e99c89880e5c706"
46054605 }
46064606 ],
4607 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4608 "acceptedAt": "2026-09-11T23:59:28.817Z"
4607 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4608 "acceptedAt": "2026-09-12T00:16:58.262Z"
46094609 },
46104610 {
46114611 "type": "VERSION_CONFLICT",
……@@ -4619,8 +4619,8 @@
46194619 "tree": "7912e790c9e87e9829a6daa037c5f5da0cc4355214f51d334f96e57678dfddeb"
46204620 }
46214621 ],
4622 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4623 "acceptedAt": "2026-09-11T23:59:28.817Z"
4622 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4623 "acceptedAt": "2026-09-12T00:16:58.262Z"
46244624 },
46254625 {
46264626 "type": "VERSION_CONFLICT",
……@@ -4634,8 +4634,8 @@
46344634 "tree": "003ac841449fcdda1abc3c87fee291a5d9596d245925a7298bd8ef5f8e423eae"
46354635 }
46364636 ],
4637 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4638 "acceptedAt": "2026-09-11T23:59:28.817Z"
4637 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4638 "acceptedAt": "2026-09-12T00:16:58.262Z"
46394639 },
46404640 {
46414641 "type": "VERSION_CONFLICT",
……@@ -4654,8 +4654,8 @@
46544654 "tree": "7b3f314cf92558edbc90295c573885276a6add925318998c829c03d335edc765"
46554655 }
46564656 ],
4657 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4658 "acceptedAt": "2026-09-11T23:59:28.817Z"
4657 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4658 "acceptedAt": "2026-09-12T00:16:58.262Z"
46594659 },
46604660 {
46614661 "type": "VERSION_CONFLICT",
……@@ -4669,8 +4669,8 @@
46694669 "tree": "c486fe73805bf013e9c5de91b5780d1aa5d2b2295306f411f7863757aaa05e60"
46704670 }
46714671 ],
4672 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4673 "acceptedAt": "2026-09-11T23:59:28.817Z"
4672 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4673 "acceptedAt": "2026-09-12T00:16:58.262Z"
46744674 },
46754675 {
46764676 "type": "VERSION_CONFLICT",
……@@ -4684,8 +4684,8 @@
46844684 "tree": "10193a86e2c6f80c2d4377379a0aae4a959f8a40fd9e852b85fee081bfe6675a"
46854685 }
46864686 ],
4687 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4688 "acceptedAt": "2026-09-11T23:59:28.817Z"
4687 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4688 "acceptedAt": "2026-09-12T00:16:58.262Z"
46894689 },
46904690 {
46914691 "type": "VERSION_CONFLICT",
……@@ -4699,8 +4699,8 @@
46994699 "tree": "20b1ecd2937f3e35102892288234b86a8ec6deb3711016d96ff3509474ac465b"
47004700 }
47014701 ],
4702 "reason": "ap-bridge 0.9.96 — operators page, invites, primary actor",
4703 "acceptedAt": "2026-09-11T23:59:28.817Z"
4702 "reason": "ap-bridge 0.9.97 — operator recovery over the local control socket",
4703 "acceptedAt": "2026-09-12T00:16:58.262Z"
47044704 }
47054705 ]
47064706}
bin/ap-bridge-admin.js +0 -62
……@@ -1,62 +0,0 @@
1#!/usr/bin/env bare
2// ap-bridge-admin — the operator's way back in, from the machine running the bridge.
3//
4// ap-bridge-admin status who operates this bridge
5// ap-bridge-admin invite mint a single-use invite for another browser
6// ap-bridge-admin revoke <pubkey> retire an operator browser
7//
8// ⛔ OVER THE UNIX SOCKET, which is the gate. The filesystem decides who may call this: only a local
9// process that can open the socket path connects. That is the fleet's pattern — pgv3's handlers gate on
10// `ctx.transport` and hyper-proto-peer's UDS surface is "TRUSTED WITHOUT the writer-allowlist" for the
11// same reason — and it is strictly stronger than gating on a loopback address, which any process or
12// user on the box can present.
13//
14// ⛔ WHY IT EXISTS. Lose every operator browser — cleared storage, a dead laptop — and the web surface
15// is shut for good: the bootstrap token gates the FIRST claim only, and the instance never reopens, not
16// even when the claimant is revoked. The alternative recovery is deleting the identity store, which
17// throws away every operator grant in order to get one back.
18import '../lib/bare-globals.js'
19import process from 'bare-process'
20import os from 'bare-os'
21import { join } from 'bare-path'
22import { Client } from 'hyper-svc'
23
24const argv = process.argv.slice(2)
25const cmd = argv[0] || 'status'
26const dataDir = process.env.AP_DATA_DIR
27 ? process.env.AP_DATA_DIR.replace(/^~/, process.env.HOME || '').replace(/\$\{HOME\}/g, process.env.HOME || '')
28 : join(os.homedir(), '.config', 'ap-bridge')
29const uds = process.env.AP_ADMIN_SOCKET || join(dataDir, 'admin.sock')
30
31function die (msg) { console.error(msg); process.exit(1) }
32
33const client = await Client.discover({ uds, prefer: 'uds' }).catch(() => null)
34if (!client) {
35 die('no control socket at ' + uds + '\n' +
36 'Is the bridge running, and is AP_ADMIN_PORT set? (the control socket comes up with the admin surface)')
37}
38
39try {
40 if (cmd === 'status') {
41 const s = await client.call('admin-status', {})
42 console.log(s.unclaimed ? 'UNCLAIMED — claim it with the bootstrap token from the service log' : 'claimed')
43 if (!s.operators.length) console.log(' no operator browsers')
44 for (const o of s.operators) console.log(' ' + o.publicKey + (o.revoked ? ' (revoked)' : ''))
45 } else if (cmd === 'invite') {
46 const { token, expiresAt } = await client.call('admin-invite', {})
47 console.log(token)
48 // ⚠ On stderr so `ap-bridge-admin invite | pbcopy` pipes the token alone.
49 console.error('single-use' + (expiresAt ? ', expires ' + new Date(expiresAt).toISOString() : '') +
50 ' — paste it into the Invite field on the sign-in page')
51 } else if (cmd === 'revoke') {
52 if (!argv[1]) die('usage: ap-bridge-admin revoke <publicKey>')
53 await client.call('admin-revoke', { publicKey: argv[1] })
54 console.log('revoked ' + argv[1])
55 } else {
56 die('usage: ap-bridge-admin [status|invite|revoke <publicKey>]')
57 }
58} catch (err) {
59 die((err && err.message) || String(err))
60} finally {
61 try { await client.close() } catch {}
62}
index.js +20 -0
……@@ -48,6 +48,26 @@ function pkgVersion () {
4848}
4949
5050async function main () {
51 // ⛔ THE CLI IS A SUBCOMMAND OF THIS ENTRY, checked before anything is opened. pgh installs one
52 // launcher per app (`pgh.bin` is a single name) and it execs this file with "$@", so `ap-bridge
53 // admin invite` arrives here. A second bin/ would have been a file nobody could install.
54 //
55 // ⚠ Before the daemon boots, and it RETURNS rather than falling through — running the CLI must not
56 // also bind the fediverse listener of a bridge that is already running.
57 if (process.argv[2] === 'admin') {
58 const { runAdminCli } = await import('./lib/admin-cli.js')
59 // ⛔ THE SAME DERIVATION THE DAEMON USES, via loadConfig (which expands ~ and ${HOME}) and the
60 // identical fallback below. If the CLI and the daemon disagreed about dataDir by one character the
61 // CLI would report "no control socket" on a bridge that is running and serving it.
62 //
63 // ⚠ loadConfig, NOT assertUsable — the CLI must work on a bridge whose keys or host are
64 // misconfigured. Refusing to hand out a recovery invite because the thing needing recovery is
65 // broken would be exactly backwards.
66 const cfg = loadConfig()
67 const dir = cfg.dataDir || join(process.env.HOME || '.', '.config', 'ap-bridge')
68 process.exit(await runAdminCli(process.argv.slice(3), { uds: join(dir, 'admin.sock') }))
69 }
70
5171 const config = assertUsable(loadConfig())
5272
5373 const privateKey = fs.readFileSync(config.privateKeyPath, 'utf8')
lib/admin-cli.js +85 -0
……@@ -0,0 +1,85 @@
1// The operator's CLI, reached as `ap-bridge admin …`.
2//
3// ⛔ A SUBCOMMAND, NOT A SECOND BINARY. pgh installs exactly one launcher per app — `pgh.bin` is a
4// single name, and schema.js rejects anything else — and that launcher is `exec … index.js "$@"`, so
5// arguments already pass through. Shipping a second bin/ would have been a file nobody could install:
6// it exists in the tree, is absent from PATH, and the documented command silently is not there.
7//
8// ⛔ OVER THE UNIX SOCKET, which is the gate. Only a local process that can open the socket path may
9// call this, which is the fleet's pattern for an operator surface — pgv3's handlers gate on
10// `ctx.transport`, and hyper-proto-peer's UDS surface is "TRUSTED WITHOUT the writer-allowlist" for
11// the same reason.
12//
13// ⛔ WHY IT EXISTS. Lose every operator browser — cleared storage, a dead laptop — and the web surface
14// is shut for good: the bootstrap token gates the FIRST claim only, and the instance never reopens,
15// not even when the claimant is revoked. Without this the recovery is deleting the identity store,
16// which throws away every operator grant in order to get one back.
17
18import { Client } from 'hyper-svc'
19
20export const USAGE = `ap-bridge admin <command>
21
22 status who operates this bridge
23 invite mint a single-use invite for another browser
24 revoke <pubkey> retire an operator browser
25
26The bridge must be running with AP_ADMIN_PORT set — the control socket comes up beside the
27admin surface. Only a process on this machine that can open the socket may call it.`
28
29/**
30 * @returns {Promise<number>} the process exit code — this function never exits on its own, so it
31 * stays testable and a caller keeps control of the process.
32 */
33export async function runAdminCli (argv, { uds, out = console.log, err = console.error, connect = null } = {}) {
34 const cmd = argv[0]
35 if (!cmd || cmd === 'help' || cmd === '--help' || cmd === '-h') { out(USAGE); return 0 }
36
37 const client = connect
38 ? await connect(uds).catch(() => null)
39 : await Client.discover({ uds, prefer: 'uds' }).catch(() => null)
40
41 if (!client) {
42 err('no control socket at ' + uds)
43 err('Is the bridge running, and is AP_ADMIN_PORT set? The control socket comes up with the admin surface.')
44 return 1
45 }
46
47 try {
48 if (cmd === 'status') {
49 const s = await client.call('admin-status', {})
50 out(s.unclaimed
51 ? 'UNCLAIMED — claim it with the bootstrap token printed in the service log'
52 : 'claimed')
53 if (!s.operators.length) out(' no operator browsers')
54 for (const o of s.operators) out(' ' + o.publicKey + (o.revoked ? ' (revoked)' : ''))
55 return 0
56 }
57
58 if (cmd === 'invite') {
59 const { token, expiresAt } = await client.call('admin-invite', {})
60 // ⚠ The token alone on stdout, everything else on stderr, so `… invite | pbcopy` pipes a token
61 // and not a paragraph.
62 out(token)
63 err('single-use' + (expiresAt ? ', expires ' + new Date(expiresAt).toISOString() : '') +
64 ' — paste it into the Invite field on the sign-in page')
65 return 0
66 }
67
68 if (cmd === 'revoke') {
69 if (!argv[1]) { err('usage: ap-bridge admin revoke <publicKey>'); return 1 }
70 await client.call('admin-revoke', { publicKey: argv[1] })
71 out('revoked ' + argv[1])
72 return 0
73 }
74
75 err(USAGE)
76 return 1
77 } catch (e) {
78 err((e && e.message) || String(e))
79 return 1
80 } finally {
81 try { await client.close() } catch {}
82 }
83}
84
85export default { runAdminCli, USAGE }
package.json +1 -5
……@@ -1,6 +1,6 @@
11{
22 "name": "ap-bridge",
3 "version": "0.9.97",
3 "version": "0.9.98",
44 "description": "ActivityPub membrane — a second projection of the self-custody atproto peer",
55 "type": "module",
66 "main": "index.js",
……@@ -38,10 +38,6 @@
3838 "brittle": "^3.0.0"
3939 },
4040 "license": "MIT",
41 "bin": {
42 "ap-bridge": "index.js",
43 "ap-bridge-admin": "bin/ap-bridge-admin.js"
44 },
4541 "pgh": {
4642 "bin": "ap-bridge",
4743 "upgrade": "pgh://1dd36bdfa9cb7f7d84adbd07f017e7200f7f90a3a546cd0761fac2a1f8a13e86/ap-bridge",
test/admin-cli.test.js +94 -0
……@@ -0,0 +1,94 @@
1// The operator CLI — `ap-bridge admin …`.
2//
3// ⛔ A SUBCOMMAND, NOT A SECOND BINARY, and that is a constraint rather than a preference. pgh installs
4// exactly ONE launcher per app — `pgh.bin` is a single name and schema.js rejects anything else — and
5// the launcher is `exec … index.js "$@"`, so arguments already arrive here. The first version shipped a
6// bin/ap-bridge-admin.js: a file present in the tree, absent from PATH, and documented in a command
7// that silently did not exist.
8
9import '../lib/bare-globals.js'
10import test from 'brittle'
11import fs from 'bare-fs'
12import { runAdminCli, USAGE } from '../lib/admin-cli.js'
13
14// A stand-in for the control socket, so the CLI's behaviour is testable without binding one.
15function fakeControl (responses = {}) {
16 const calls = []
17 return {
18 calls,
19 connect: async () => ({
20 call: async (name, params) => {
21 calls.push([name, params])
22 if (responses[name] instanceof Error) throw responses[name]
23 return responses[name]
24 }
25 })
26 }
27}
28
29const capture = () => { const lines = []; return { lines, write: (l) => lines.push(String(l)) } }
30
31test('★★ status reports who operates the bridge', async (t) => {
32 const c = fakeControl({ 'admin-status': { unclaimed: false, operators: [{ publicKey: 'abc=', revoked: false }, { publicKey: 'def=', revoked: true }] } })
33 const out = capture()
34 const code = await runAdminCli(['status'], { uds: '/x', out: out.write, err: () => {}, connect: c.connect })
35
36 t.is(code, 0)
37 t.ok(out.lines.some((l) => /claimed/.test(l)))
38 t.ok(out.lines.some((l) => /abc=/.test(l)))
39 // ⛔ A revoked operator is LISTED, not filtered. It is still registered — which is why revoking the
40 // claimant does not reopen the instance — and hiding it would read as "removed".
41 t.ok(out.lines.some((l) => /def=.*revoked/.test(l)), 'revoked is shown, not hidden')
42})
43
44test('★★★ invite prints the token alone on stdout', async (t) => {
45 // ⚠ So `ap-bridge admin invite | pbcopy` copies a token and not a paragraph. Everything else goes to
46 // stderr, which is the difference between a CLI that composes and one that only reads well.
47 const c = fakeControl({ 'admin-invite': { token: 'deadbeefcafe', expiresAt: Date.now() + 60000 } })
48 const out = capture()
49 const err = capture()
50 const code = await runAdminCli(['invite'], { uds: '/x', out: out.write, err: err.write, connect: c.connect })
51
52 t.is(code, 0)
53 t.alike(out.lines, ['deadbeefcafe'], 'stdout is exactly the token')
54 t.ok(err.lines.some((l) => /single-use/.test(l)), 'and the advice went to stderr')
55})
56
57test('★★★ an unreachable socket says what to check, and fails', async (t) => {
58 // ⛔ THE MOST LIKELY FAILURE, since the control socket only comes up when AP_ADMIN_PORT is set. A
59 // bare "connection refused" would send an operator looking at the wrong thing.
60 const err = capture()
61 const code = await runAdminCli(['status'], { uds: '/nope/admin.sock', out: () => {}, err: err.write, connect: async () => { throw new Error('ENOENT') } })
62
63 t.is(code, 1, 'a non-zero exit, so a script notices')
64 t.ok(err.lines.some((l) => /no control socket at \/nope/.test(l)), 'it names the path it tried')
65 t.ok(err.lines.some((l) => /AP_ADMIN_PORT/.test(l)), 'and the likeliest reason')
66})
67
68test('★★ revoke needs a key, and says so', async (t) => {
69 const c = fakeControl({ 'admin-revoke': { revoked: 'abc=' } })
70 const err = capture()
71 t.is(await runAdminCli(['revoke'], { uds: '/x', out: () => {}, err: err.write, connect: c.connect }), 1)
72 t.is(c.calls.length, 0, 'nothing was called without a key')
73
74 const out = capture()
75 t.is(await runAdminCli(['revoke', 'abc='], { uds: '/x', out: out.write, err: () => {}, connect: c.connect }), 0)
76 t.alike(c.calls[0], ['admin-revoke', { publicKey: 'abc=' }])
77})
78
79test('★★ no arguments prints usage and succeeds', async (t) => {
80 const out = capture()
81 t.is(await runAdminCli([], { uds: '/x', out: out.write, err: () => {}, connect: async () => { throw new Error('should not connect') } }), 0)
82 t.ok(out.lines.join('\n').includes('ap-bridge admin'), 'usage names the real command')
83 t.ok(USAGE.includes('revoke <pubkey>'))
84})
85
86test('★★★ the entry dispatches the subcommand before booting the daemon', (t) => {
87 // ⛔ Running the CLI must not also bind the fediverse listener of a bridge that is already running.
88 const src = fs.readFileSync(new URL('../index.js', import.meta.url).pathname, 'utf8')
89 const dispatch = src.indexOf("process.argv[2] === 'admin'")
90 const listen = src.indexOf('server.listen(')
91 t.ok(dispatch > 0, 'the entry knows the subcommand')
92 t.ok(dispatch < listen, 'and handles it before anything binds')
93 t.ok(/process\.exit\(await runAdminCli/.test(src), 'then exits rather than falling through')
94})