fedipod 1.30.0 → 1.32.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -69919,6 +69919,8 @@ var ADMIN_PATHS = /* @__PURE__ */ new Set([
69919
69919
  "/park",
69920
69920
  "/revive",
69921
69921
  "/takeover",
69922
+ "/gateway/pause",
69923
+ "/gateway/close",
69922
69924
  "/fediacct/connect",
69923
69925
  "/fediacct/disconnect",
69924
69926
  "/fediacct/callback"
@@ -70023,7 +70025,11 @@ var AdminFacade = class {
70023
70025
  frontActor: g?.frontActor || null,
70024
70026
  mode: g?.mode || "off",
70025
70027
  hasSecret: !!g?.hmacSecret,
70026
- stats: a.store.read("gateway-stats.json", { verified: 0, unverified: 0, lastAt: null })
70028
+ stats: a.store.read("gateway-stats.json", { verified: 0, unverified: 0, lastAt: null }),
70029
+ // The account's standing at the gateway — paused, closed — as the
70030
+ // gateway last said it (agent.mjs tellGateway). The record page
70031
+ // shows its pause and close controls only when this is here.
70032
+ standing: a.gatewayStanding || null
70027
70033
  });
70028
70034
  }
70029
70035
  case "/deadletter":
@@ -70167,6 +70173,24 @@ var AdminFacade = class {
70167
70173
  if (!await a.requestTakeover?.()) return json2(503, { error: "the lease could not be taken \u2014 the pod refused the write" });
70168
70174
  return json2(200, { ok: true, mode: a.status().mode });
70169
70175
  }
70176
+ // The account's standing at its gateway: paused by its owner, or closed
70177
+ // for good (front-core: accounts that go quiet). Both go to the gateway
70178
+ // with the pod session as proof, and its answer is the new standing.
70179
+ case "/gateway/pause": {
70180
+ if (typeof body.paused !== "boolean") return json2(400, { error: "paused must be true or false" });
70181
+ if (!a.pauseAtGateway || !a.gatewayApi) return json2(501, { error: "this account is not at a gateway" });
70182
+ const r = await a.pauseAtGateway(body.paused);
70183
+ if (!r) return json2(502, { error: "the gateway could not be reached" });
70184
+ return json2(r.status === 200 ? 200 : r.status, r);
70185
+ }
70186
+ case "/gateway/close": {
70187
+ const handle7 = String(a.store.getConfig()?.handle || "").toLowerCase();
70188
+ if (!handle7 || String(body.confirm || "").toLowerCase() !== handle7) return json2(400, { error: "type the handle to confirm" });
70189
+ if (!a.closeAtGateway || !a.gatewayApi) return json2(501, { error: "this account is not at a gateway" });
70190
+ const r = await a.closeAtGateway();
70191
+ if (!r) return json2(502, { error: "the gateway could not be reached" });
70192
+ return json2(r.status === 200 ? 200 : r.status, r);
70193
+ }
70170
70194
  // Going quiet, and coming back. The record page's active/parked select.
70171
70195
  case "/park": {
70172
70196
  if (!await a.requestTakeover?.()) return json2(503, { error: "another device is active for this pod \u2014 park from there" });
@@ -71708,10 +71732,46 @@ var BrowserAgent = class _BrowserAgent {
71708
71732
  this.log("took over from the other device (action here)");
71709
71733
  return true;
71710
71734
  }
71735
+ // One call to the gateway's owner API, proved with the pod session. What
71736
+ // comes back on 200 or 410 is the account's standing there, kept for the
71737
+ // manage page; anything else is logged and forgotten.
71738
+ async tellGateway(what, body) {
71739
+ if (!this.gatewayApi) return null;
71740
+ let res;
71741
+ try {
71742
+ res = await this.sessionFetch(`${this.gatewayApi}/${what}`, {
71743
+ method: "POST",
71744
+ headers: { "content-type": "application/json" },
71745
+ body: JSON.stringify(body)
71746
+ });
71747
+ } catch (e) {
71748
+ this.log(`gateway ${what}: ${e.message}`);
71749
+ return null;
71750
+ }
71751
+ const json2 = await res.json().catch(() => ({}));
71752
+ const out = { status: res.status, ...json2 };
71753
+ if (res.status === 200 || res.status === 410) this.gatewayStanding = out;
71754
+ else this.log(`gateway ${what}: ${res.status} ${json2.error || ""}`);
71755
+ return out;
71756
+ }
71757
+ openAtGateway() {
71758
+ return this.tellGateway("open", { handle: this.doorKey });
71759
+ }
71760
+ pauseAtGateway(paused) {
71761
+ return this.tellGateway("pause", { handle: this.doorKey, paused: !!paused });
71762
+ }
71763
+ closeAtGateway() {
71764
+ return this.tellGateway("close", { handle: this.doorKey, confirm: true });
71765
+ }
71766
+ static OPEN_EVERY_MS = 60 * 6e4;
71711
71767
  // Become the active agent: renew the lease, then start what a viewer skips —
71712
71768
  // publish the face, drain the inbox, run the mirrors.
71713
71769
  async goActive() {
71714
71770
  this.viewer = false;
71771
+ clearInterval(this._openTimer);
71772
+ this._openTimer = setInterval(() => {
71773
+ this.openAtGateway();
71774
+ }, _BrowserAgent.OPEN_EVERY_MS);
71715
71775
  this.lease.onLost = () => this.demote();
71716
71776
  this.lease.startRenewal();
71717
71777
  try {
@@ -71738,6 +71798,8 @@ var BrowserAgent = class _BrowserAgent {
71738
71798
  this.viewer = true;
71739
71799
  this.log("another device took over \u2014 read-only here");
71740
71800
  this.lease.stopRenewal();
71801
+ clearInterval(this._openTimer);
71802
+ this._openTimer = null;
71741
71803
  this.intake?.stop?.();
71742
71804
  this.deliverer?.stop?.();
71743
71805
  this.importer?.stop?.();
@@ -71851,6 +71913,29 @@ var BrowserAgent = class _BrowserAgent {
71851
71913
  sessionFetch: session.fetch,
71852
71914
  onGone: () => this.publisher.publishCollections({ followers: true })
71853
71915
  });
71916
+ this.gatewayApi = null;
71917
+ try {
71918
+ if (config.gateway?.url) this.gatewayApi = `${new URL(config.gateway.url).origin}/api`;
71919
+ } catch {
71920
+ this.gatewayApi = null;
71921
+ }
71922
+ this.doorKey = doorKeyOf(config.gateway?.url) || config.handle;
71923
+ this.gatewayStanding = null;
71924
+ const standing = await this.openAtGateway();
71925
+ if (standing?.status === 410) {
71926
+ const host = (() => {
71927
+ try {
71928
+ return new URL(this.gatewayApi).host;
71929
+ } catch {
71930
+ return "the gateway";
71931
+ }
71932
+ })();
71933
+ const address = this.doorKey.includes("@") ? `@${this.doorKey}` : `@${this.doorKey}@${host}`;
71934
+ const why = standing.closedBy === "owner" ? "you closed it" : "nothing opened it for six months";
71935
+ const e = new Error(`${address} is closed at ${host}: ${why}, and a closed address does not come back. Everything on your pod is untouched. The sign-in used here was ${webId}.`);
71936
+ e.code = "address-closed";
71937
+ throw e;
71938
+ }
71854
71939
  this.publisher = new Publisher({
71855
71940
  config: this.store.getConfig(),
71856
71941
  remote: this.remote,