@songsid/agend 2.3.3-beta.8 → 2.3.3

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.
Files changed (50) hide show
  1. package/dist/backend/claude-code.js +17 -0
  2. package/dist/backend/claude-code.js.map +1 -1
  3. package/dist/backend/codex.d.ts +15 -0
  4. package/dist/backend/codex.js +67 -18
  5. package/dist/backend/codex.js.map +1 -1
  6. package/dist/backend/types.d.ts +7 -0
  7. package/dist/backend/types.js.map +1 -1
  8. package/dist/channel/adapters/discord.js +60 -17
  9. package/dist/channel/adapters/discord.js.map +1 -1
  10. package/dist/daemon.d.ts +17 -0
  11. package/dist/daemon.js +85 -9
  12. package/dist/daemon.js.map +1 -1
  13. package/dist/fleet-manager.d.ts +31 -0
  14. package/dist/fleet-manager.js +106 -12
  15. package/dist/fleet-manager.js.map +1 -1
  16. package/dist/instance-lifecycle.d.ts +13 -0
  17. package/dist/instance-lifecycle.js +30 -6
  18. package/dist/instance-lifecycle.js.map +1 -1
  19. package/dist/locale.js +10 -2
  20. package/dist/locale.js.map +1 -1
  21. package/dist/parked-notices.d.ts +87 -0
  22. package/dist/parked-notices.js +194 -0
  23. package/dist/parked-notices.js.map +1 -0
  24. package/dist/public-link-progress.d.ts +5 -2
  25. package/dist/public-link-progress.js +4 -3
  26. package/dist/public-link-progress.js.map +1 -1
  27. package/dist/public-web-link.d.ts +10 -0
  28. package/dist/public-web-link.js +24 -5
  29. package/dist/public-web-link.js.map +1 -1
  30. package/dist/settings-api.d.ts +10 -0
  31. package/dist/settings-api.js +41 -0
  32. package/dist/settings-api.js.map +1 -1
  33. package/dist/tunnel/cloudflared.d.ts +5 -0
  34. package/dist/tunnel/cloudflared.js +32 -1
  35. package/dist/tunnel/cloudflared.js.map +1 -1
  36. package/dist/tunnel/named-diagnostics.d.ts +2 -1
  37. package/dist/tunnel/named-diagnostics.js +4 -1
  38. package/dist/tunnel/named-diagnostics.js.map +1 -1
  39. package/dist/tunnel/named-link-test.d.ts +90 -0
  40. package/dist/tunnel/named-link-test.js +270 -0
  41. package/dist/tunnel/named-link-test.js.map +1 -0
  42. package/dist/tunnel/purpose-lane.d.ts +3 -1
  43. package/dist/tunnel/purpose-lane.js +2 -0
  44. package/dist/tunnel/purpose-lane.js.map +1 -1
  45. package/dist/ui/panel-settings.js +5 -3
  46. package/dist/ui/settings-named-test.js +144 -0
  47. package/dist/ui/settings-strings.js +6 -6
  48. package/dist/ui/shared/app.css +7 -0
  49. package/dist/ui/shared/web-authorize.js +2 -2
  50. package/package.json +1 -1
@@ -1 +1 @@
1
- {"version":3,"file":"named-diagnostics.js","sourceRoot":"","sources":["../../src/tunnel/named-diagnostics.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"named-diagnostics.js","sourceRoot":"","sources":["../../src/tunnel/named-diagnostics.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,90 @@
1
+ import { type CloudflaredOptions, type CloudflaredProtocol } from "./cloudflared.js";
2
+ import { type ProbeResponse } from "./public-fetch.js";
3
+ import type { TunnelPurposeLane } from "./purpose-lane.js";
4
+ import type { TunnelProvider } from "./types.js";
5
+ export type NamedTestStep = "values" | "origin" | "tunnel" | "dns" | "reach";
6
+ export declare const NAMED_TEST_STEPS: readonly NamedTestStep[];
7
+ /** What each step found; the page words each one (and what to do next) in the person's language. */
8
+ export type NamedTestCode = "ok" | "live" | "timeout" | "cancelled" | "bad-hostname" | "bad-port" | "no-token" | "bad-token" | "dashboard-port" | "port-in-use" | "origin-failed" | "link-open-other" | "tunnel-busy" | "auth" | "network" | "stopped" | "no-binary" | "dns-missing" | "dns-failed" | "wrong-service" | "other-page" | "route-missing" | "self-unreachable" | "reach-failed";
9
+ export interface NamedTestStepResult {
10
+ step: NamedTestStep;
11
+ ok: boolean;
12
+ code: NamedTestCode;
13
+ }
14
+ export interface NamedTestResult {
15
+ steps: NamedTestStepResult[];
16
+ hostname: string | null;
17
+ port: number | null;
18
+ /** The test connector's stop could not be proven: it may still run (the lane stays held until it is resolved). */
19
+ cleanup?: "unconfirmed";
20
+ }
21
+ export interface NamedTestInput {
22
+ hostname: unknown;
23
+ port: unknown;
24
+ protocol: unknown;
25
+ token: string | null | undefined;
26
+ }
27
+ export declare const NAMED_TEST_REGISTER_MS = 20000;
28
+ export declare const NAMED_TEST_REACH_MS = 20000;
29
+ /** The whole test before cleanup, whatever happens. */
30
+ export declare const NAMED_TEST_HARD_MS = 60000;
31
+ /** Cleanup's own allowance after the deadline: the managed stop (TERM, proof, KILL, proof) and closing the stand-in. */
32
+ export declare const NAMED_TEST_CLEANUP_MS = 15000;
33
+ /** The test connector's lane: started through the managed tunnel, given back only on a proven stop. */
34
+ export interface NamedTestLane {
35
+ start(input: {
36
+ binary: string;
37
+ hostname: string;
38
+ token: string;
39
+ protocol: CloudflaredProtocol;
40
+ origin: URL;
41
+ signal: AbortSignal;
42
+ }): Promise<{
43
+ ok: true;
44
+ } | {
45
+ ok: false;
46
+ kind: string;
47
+ }>;
48
+ stop(): Promise<{
49
+ confirmed: boolean;
50
+ }>;
51
+ /** Nothing was started: give the lane back. */
52
+ releaseUnused(): void;
53
+ }
54
+ export interface NamedLinkTestDeps {
55
+ /** The dashboard's own port: never the tunnel's origin. */
56
+ healthPort: number;
57
+ /** The bundled cloudflared's path (may fetch the pinned copy). */
58
+ ensureBinary(signal: AbortSignal): Promise<string>;
59
+ /** The open link on exactly these values: its page's readiness marker. Null: none (or other values). */
60
+ liveMarker(values: {
61
+ hostname: string;
62
+ port: number;
63
+ token: string;
64
+ protocol: CloudflaredProtocol;
65
+ }): string | null;
66
+ /** Holds the one tunnel lane for the test, or says who holds it. */
67
+ reserve(): NamedTestLane | {
68
+ busy: "link-open" | "other";
69
+ };
70
+ /** The admin who asked is still signed in (checked before anything is started). */
71
+ current?(): boolean;
72
+ /** Test seams. */
73
+ resolve4?: (host: string) => Promise<string[]>;
74
+ systemLookup?: (host: string) => Promise<unknown>;
75
+ fetchPage?: (url: string, signal: AbortSignal) => Promise<ProbeResponse>;
76
+ bindHost?: string;
77
+ hardMs?: number;
78
+ cleanupMs?: number;
79
+ reachMs?: number;
80
+ retryMs?: number;
81
+ }
82
+ export declare function runNamedLinkTest(input: NamedTestInput, deps: NamedLinkTestDeps, outer?: AbortSignal): Promise<NamedTestResult>;
83
+ /**
84
+ * The production lane: the one tunnel lane's reservation, the connector started by its managed start (lease, proven
85
+ * stop) as a CloudflaredProvider that is ready once registered. `provider` is a test seam.
86
+ */
87
+ export declare function namedTestLane(lane: TunnelPurposeLane, opts?: {
88
+ registerMs?: number;
89
+ provider?: (o: CloudflaredOptions) => TunnelProvider;
90
+ }): NamedLinkTestDeps["reserve"];
@@ -0,0 +1,270 @@
1
+ /**
2
+ * The fixed-address "Test connection" (Settings → Access; the user, 2026-11-01): checks the named-tunnel values as
3
+ * filled in, step by step, and says for each ✗ what the person can do next.
4
+ *
5
+ * ① values the hostname, the origin port and the connector token are well-formed
6
+ * ② origin a stand-in answers on 127.0.0.1:<port> for this test (what Cloudflare's route must point to)
7
+ * ③ tunnel a test connector (the bundled cloudflared, the token in its environment) registers with Cloudflare
8
+ * ④ dns the hostname resolves through a public resolver
9
+ * ⑤ reach an HTTPS request to the hostname comes back with this test's nonce (so it reached THIS AgEnD)
10
+ *
11
+ * With the admin's link already open on exactly these values (protocol included), nothing is started: ② and ③ are
12
+ * the open link's own, and ⑤ looks for its page's readiness marker instead (the open link is never stopped, replaced
13
+ * or shared).
14
+ *
15
+ * #2907 review: the connector is started through the one tunnel lane's managed start (the lease a crash leaves
16
+ * recoverable, a stop proven by the process's fingerprint); the lane is given back only on a confirmed stop — an
17
+ * unproven one is reported (`cleanup: "unconfirmed"`) and the lane stays held. Every stage races the hard deadline,
18
+ * and nothing is started once it (or the admin's session) is gone. Only closed step codes leave this module: the
19
+ * token goes into the connector's environment, never argv, and its output is read for a closed classification only.
20
+ */
21
+ import { createServer } from "node:http";
22
+ import { randomBytes } from "node:crypto";
23
+ import { lookup, Resolver } from "node:dns/promises";
24
+ import { publicTunnelHostname, normalizePublicTunnelToken } from "../public-tunnel-config.js";
25
+ import { CloudflaredProvider } from "./cloudflared.js";
26
+ import { classifyNamedTunnelProbeError } from "./named-diagnostics.js";
27
+ import { fetchViaPublicResolver, fetchViaSystemResolver, isPublicIPv4, PUBLIC_RESOLVERS } from "./public-fetch.js";
28
+ export const NAMED_TEST_STEPS = ["values", "origin", "tunnel", "dns", "reach"];
29
+ /** Cloudflare's own "no connector / origin unreachable" answers (502 Bad gateway, 503, 530 incl. error 1033). */
30
+ const WRONG_SERVICE_STATUSES = new Set([502, 503, 504, 530]);
31
+ export const NAMED_TEST_REGISTER_MS = 20_000;
32
+ export const NAMED_TEST_REACH_MS = 20_000;
33
+ /** The whole test before cleanup, whatever happens. */
34
+ export const NAMED_TEST_HARD_MS = 60_000;
35
+ /** Cleanup's own allowance after the deadline: the managed stop (TERM, proof, KILL, proof) and closing the stand-in. */
36
+ export const NAMED_TEST_CLEANUP_MS = 15_000;
37
+ const DEADLINE = Symbol("deadline");
38
+ /** A step that threw (not a closed answer): its own closed failure. */
39
+ const UNEXPECTED = { values: "bad-token", origin: "origin-failed", tunnel: "stopped", dns: "dns-failed", reach: "reach-failed" };
40
+ /** `p`, unless the signal fires first (then DEADLINE); `p` itself is left to settle on its own. */
41
+ function within(p, signal) {
42
+ if (signal.aborted) {
43
+ p.catch(() => { });
44
+ return Promise.reject(DEADLINE);
45
+ }
46
+ return new Promise((resolve, reject) => {
47
+ const stop = () => reject(DEADLINE);
48
+ signal.addEventListener("abort", stop, { once: true });
49
+ p.then(v => { signal.removeEventListener("abort", stop); resolve(v); }, e => { signal.removeEventListener("abort", stop); reject(e); });
50
+ });
51
+ }
52
+ const settleWithin = (p, ms) => Promise.race([p, new Promise(r => setTimeout(() => r(DEADLINE), ms).unref?.())]);
53
+ export async function runNamedLinkTest(input, deps, outer) {
54
+ const steps = [];
55
+ const pass = (step, code = "ok") => { steps.push({ step, ok: true, code }); };
56
+ const fail = (step, code) => { steps.push({ step, ok: false, code }); return { steps, hostname, port }; };
57
+ // ① values — nothing runs on a value that could not be used.
58
+ const hostname = publicTunnelHostname(typeof input.hostname === "string" ? input.hostname.trim() : input.hostname);
59
+ const portRaw = typeof input.port === "string" && input.port.trim() !== "" ? Number(input.port) : input.port;
60
+ const port = Number.isInteger(portRaw) && portRaw >= 1024 && portRaw <= 65535 ? portRaw : null;
61
+ if (!hostname)
62
+ return fail("values", "bad-hostname");
63
+ if (port === null)
64
+ return fail("values", "bad-port");
65
+ if (!input.token)
66
+ return fail("values", "no-token");
67
+ const token = normalizePublicTunnelToken(input.token);
68
+ if (!token)
69
+ return fail("values", "bad-token");
70
+ pass("values");
71
+ const protocol = input.protocol === "quic" || input.protocol === "auto" ? input.protocol : "http2";
72
+ const abort = new AbortController();
73
+ const hard = setTimeout(() => abort.abort(), deps.hardMs ?? NAMED_TEST_HARD_MS);
74
+ const onOuter = () => abort.abort();
75
+ outer?.addEventListener("abort", onOuter, { once: true });
76
+ const signal = abort.signal;
77
+ /** Before anything is started: the deadline has not passed and the admin who asked is still signed in. */
78
+ const gone = () => signal.aborted ? "timeout" : deps.current && !deps.current() ? "cancelled" : null;
79
+ let server = null, lane = null, started = false;
80
+ let result;
81
+ let step = "origin";
82
+ try {
83
+ result = await (async () => {
84
+ // The admin's link is open on exactly these values: test it as it is.
85
+ const marker = deps.liveMarker({ hostname, port, token, protocol });
86
+ if (marker) {
87
+ pass("origin", "live");
88
+ pass("tunnel", "live");
89
+ step = "dns";
90
+ const dns = await within(resolveStep(hostname, deps), signal);
91
+ if (dns !== "ok")
92
+ return fail("dns", dns);
93
+ pass("dns");
94
+ step = "reach";
95
+ const reach = await within(reachStep(`https://${hostname}/signin`, marker, true, deps, signal), signal);
96
+ return reach === "ok" ? (pass("reach"), { steps, hostname, port }) : fail("reach", reach);
97
+ }
98
+ if (port === deps.healthPort)
99
+ return fail("origin", "dashboard-port");
100
+ const stop = gone();
101
+ if (stop)
102
+ return fail("origin", stop);
103
+ const held = deps.reserve();
104
+ if ("busy" in held)
105
+ return fail("origin", held.busy === "link-open" ? "link-open-other" : "tunnel-busy");
106
+ lane = held;
107
+ // ② origin — a stand-in that answers every request with this test's nonce, and nothing else.
108
+ const nonce = randomBytes(16).toString("hex");
109
+ const page = `<!doctype html><title>AgEnD</title><p>agend-named-test ${nonce}</p>`;
110
+ const stand = createServer((req, res) => { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8", "Cache-Control": "no-store" }); res.end(req.method === "HEAD" ? undefined : page); });
111
+ server = stand;
112
+ const bound = await within(new Promise(resolve => {
113
+ stand.once("error", (err) => resolve(err?.code === "EADDRINUSE" || err?.code === "EACCES" ? "port-in-use" : "origin-failed"));
114
+ stand.listen(port, deps.bindHost ?? "127.0.0.1", () => resolve("ok"));
115
+ }), signal);
116
+ if (bound !== "ok") {
117
+ server = null;
118
+ return fail("origin", bound);
119
+ }
120
+ pass("origin");
121
+ // ③ tunnel — a test connector of the same tunnel, through the lane's managed start.
122
+ step = "tunnel";
123
+ let binary;
124
+ try {
125
+ binary = await within(deps.ensureBinary(signal), signal);
126
+ }
127
+ catch (err) {
128
+ if (err === DEADLINE)
129
+ throw err;
130
+ return fail("tunnel", "no-binary");
131
+ }
132
+ const late = gone();
133
+ if (late)
134
+ return fail("tunnel", late);
135
+ started = true;
136
+ const connector = await within(lane.start({ binary, hostname, token, protocol, origin: new URL(`http://127.0.0.1:${port}`), signal }), signal);
137
+ if (!connector.ok)
138
+ return fail("tunnel", tunnelCode(connector.kind, signal));
139
+ pass("tunnel");
140
+ // ④ dns, ⑤ reach — through a public resolver, as a phone elsewhere would.
141
+ step = "dns";
142
+ const dns = await within(resolveStep(hostname, deps), signal);
143
+ if (dns !== "ok")
144
+ return fail("dns", dns);
145
+ pass("dns");
146
+ step = "reach";
147
+ const reach = await within(reachStep(`https://${hostname}/agend-named-test-${nonce.slice(0, 8)}`, `agend-named-test ${nonce}`, false, deps, signal), signal);
148
+ return reach === "ok" ? (pass("reach"), { steps, hostname, port }) : fail("reach", reach);
149
+ })();
150
+ }
151
+ catch (err) {
152
+ // #2907 review (r2): nothing thrown escapes before the cleanup below — the deadline is "timeout" (or "cancelled"),
153
+ // anything else (a lease write refused, an unexpected error) is the step's closed failure; never a raw error.
154
+ result = fail(step, err === DEADLINE ? (outer?.aborted ? "cancelled" : "timeout") : UNEXPECTED[step]);
155
+ }
156
+ finally {
157
+ clearTimeout(hard);
158
+ outer?.removeEventListener("abort", onOuter);
159
+ // Cleanup, unconditionally and with its own allowance: what was started goes through the lane's managed stop
160
+ // (which gives the lane back only on a proven stop); the stand-in closes on every outcome.
161
+ const cleanupMs = deps.cleanupMs ?? NAMED_TEST_CLEANUP_MS;
162
+ let unconfirmed = false;
163
+ if (lane) {
164
+ const held = lane;
165
+ if (started) {
166
+ const stopped = await settleWithin(held.stop().catch(() => ({ confirmed: false })), cleanupMs);
167
+ unconfirmed = stopped === DEADLINE || !stopped.confirmed;
168
+ }
169
+ else
170
+ held.releaseUnused();
171
+ }
172
+ if (server) {
173
+ const stand = server;
174
+ await settleWithin(new Promise(r => { stand.close(() => r()); stand.closeAllConnections?.(); }), cleanupMs);
175
+ }
176
+ if (unconfirmed)
177
+ result.cleanup = "unconfirmed";
178
+ }
179
+ return result;
180
+ }
181
+ /** ③'s closed codes from the managed start's error kinds. */
182
+ function tunnelCode(kind, signal) {
183
+ switch (kind) {
184
+ case "named-auth-failed": return "auth";
185
+ case "named-connection-failed":
186
+ case "timeout": return "network";
187
+ case "cancelled": return signal.aborted ? "timeout" : "stopped";
188
+ case "lease-held": return "tunnel-busy";
189
+ case "binary-missing":
190
+ case "binary-not-executable": return "no-binary";
191
+ default: return "stopped";
192
+ }
193
+ }
194
+ /** ④: a public resolver's answer (what a phone elsewhere gets); the system's only when the public one cannot answer. */
195
+ async function resolveStep(hostname, deps) {
196
+ try {
197
+ const resolve4 = deps.resolve4 ?? ((h) => { const r = new Resolver({ timeout: 3_000, tries: 2 }); r.setServers([...PUBLIC_RESOLVERS]); return r.resolve4(h); });
198
+ const addresses = await resolve4(hostname);
199
+ return addresses.some(isPublicIPv4) ? "ok" : "dns-missing";
200
+ }
201
+ catch (err) {
202
+ const code = err?.code;
203
+ if (code === "ENOTFOUND" || code === "ENODATA")
204
+ return "dns-missing";
205
+ }
206
+ try {
207
+ await (deps.systemLookup ?? ((h) => lookup(h)))(hostname);
208
+ return "ok";
209
+ }
210
+ catch (err) {
211
+ return err?.code === "ENOTFOUND" ? "dns-missing" : "dns-failed";
212
+ }
213
+ }
214
+ /** ⑤: retried until the budget ends (the edge takes a moment to learn a new connector); the last answer decides. */
215
+ async function reachStep(url, expected, live, deps, signal) {
216
+ const fetchPage = deps.fetchPage ?? ((u, s) => fetchViaPublicResolver(u, s, { fallback: (v, t) => fetchViaSystemResolver(v, t) }));
217
+ const deadline = performance.now() + (deps.reachMs ?? NAMED_TEST_REACH_MS);
218
+ let last = "reach-failed";
219
+ while (!signal.aborted && performance.now() < deadline) {
220
+ const attempt = new AbortController();
221
+ const stop = () => attempt.abort();
222
+ signal.addEventListener("abort", stop, { once: true });
223
+ const timer = setTimeout(stop, Math.max(1, Math.min(5_000, deadline - performance.now())));
224
+ try {
225
+ const res = await within(fetchPage(url, attempt.signal), attempt.signal);
226
+ if (res.status === 200 && res.body.includes(expected))
227
+ return "ok";
228
+ last = res.status === 200 ? "other-page" : res.status === 404 ? "route-missing" : WRONG_SERVICE_STATUSES.has(res.status) ? "wrong-service" : "reach-failed";
229
+ // The open link's page may be served by another of the tunnel's connectors; any 200 there is its page.
230
+ if (live && res.status === 200)
231
+ return "other-page";
232
+ }
233
+ catch (err) {
234
+ if (err !== DEADLINE) {
235
+ const kind = classifyNamedTunnelProbeError(err, "");
236
+ last = kind === "named-route-missing" ? "route-missing" : kind === "named-connection-failed" ? "self-unreachable" : "reach-failed";
237
+ }
238
+ }
239
+ finally {
240
+ clearTimeout(timer);
241
+ signal.removeEventListener("abort", stop);
242
+ }
243
+ await new Promise(r => setTimeout(r, deps.retryMs ?? 1_000));
244
+ }
245
+ return last;
246
+ }
247
+ /**
248
+ * The production lane: the one tunnel lane's reservation, the connector started by its managed start (lease, proven
249
+ * stop) as a CloudflaredProvider that is ready once registered. `provider` is a test seam.
250
+ */
251
+ export function namedTestLane(lane, opts = {}) {
252
+ return () => {
253
+ const id = `named-test-${randomBytes(6).toString("hex")}`;
254
+ const reservation = lane.reserve("named-test", id);
255
+ if (!reservation)
256
+ return { busy: lane.holder() === "dashboard" ? "link-open" : "other" };
257
+ return {
258
+ async start(i) {
259
+ const provider = (opts.provider ?? (o => new CloudflaredProvider(o)))({ binaryName: i.binary, protocol: i.protocol,
260
+ named: { hostname: i.hostname, token: i.token }, registeredOnly: true, deadlineMs: opts.registerMs ?? NAMED_TEST_REGISTER_MS });
261
+ const result = await reservation.start(provider, { sid: id, origin: i.origin, pagePath: "/", readinessMarker: id,
262
+ expiresAt: Date.now() + NAMED_TEST_HARD_MS, signal: i.signal });
263
+ return result.ok ? { ok: true } : { ok: false, kind: result.errorKind };
264
+ },
265
+ async stop() { return { confirmed: (await reservation.stop("named link test done")).confirmed }; },
266
+ releaseUnused() { reservation.releaseUnused(); },
267
+ };
268
+ };
269
+ }
270
+ //# sourceMappingURL=named-link-test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"named-link-test.js","sourceRoot":"","sources":["../../src/tunnel/named-link-test.ts"],"names":[],"mappings":"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"}
@@ -14,5 +14,7 @@ export declare class TunnelPurposeLane {
14
14
  private readonly manager;
15
15
  private owner;
16
16
  constructor(manager: Pick<ManagedTunnel, "start" | "stop">);
17
- reserve(purpose: "login" | "dashboard", id: string): TunnelReservation | null;
17
+ /** #2906: who holds the lane now (the fixed address's Test connection says which), or null. */
18
+ holder(): string | null;
19
+ reserve(purpose: "login" | "dashboard" | "named-test", id: string): TunnelReservation | null;
18
20
  }
@@ -6,6 +6,8 @@ export class TunnelPurposeLane {
6
6
  constructor(manager) {
7
7
  this.manager = manager;
8
8
  }
9
+ /** #2906: who holds the lane now (the fixed address's Test connection says which), or null. */
10
+ holder() { return this.owner?.purpose ?? null; }
9
11
  reserve(purpose, id) {
10
12
  if (this.owner)
11
13
  return this.owner.purpose === purpose && this.owner.id === id ? this.owner.reservation : null;
@@ -1 +1 @@
1
- {"version":3,"file":"purpose-lane.js","sourceRoot":"","sources":["../../src/tunnel/purpose-lane.ts"],"names":[],"mappings":"AAYA,MAAM,QAAQ,GAAG,GAAqB,EAAE,CAAC,CAAC,EAAE,SAAS,EAAE,KAAK,EAAE,MAAM,EAAE,aAAa,EAAE,GAAG,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,CAAC,CAAC;AAClH,kGAAkG;AAClG,MAAM,OAAO,iBAAiB;IAEC,OAAO;IAD5B,KAAK,GAA2E,IAAI,CAAC;IAC7F,YAA6B,OAA8C;uBAA9C,OAAO;IAA0C,CAAC;IAC/E,OAAO,CAAC,OAA8B,EAAE,EAAU;QAChD,IAAI,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,KAAK,CAAC,OAAO,KAAK,OAAO,IAAI,IAAI,CAAC,KAAK,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,WAAW,CAAC,CAAC,CAAC,IAAI,CAAC;QAC9G,IAAI,OAAO,GAAG,IAAI,CAAC;QACnB,IAAI,QAAiD,CAAC;QACtD,IAAI,QAA+C,CAAC;QACpD,MAAM,OAAO,GAAG,GAAS,EAAE,GAAG,IAAI,OAAO,IAAI,IAAI,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW;YAAE,IAAI,CAAC,KAAK,GAAG,IAAI,CAAC,CAAC,CAAC,CAAC;QAC3G,MAAM,WAAW,GAAsB;YACrC,KAAK,EAAE,CAAC,QAAQ,EAAE,OAAO,EAAE,EAAE;gBAC3B,IAAI,IAAI,CAAC,KAAK,EAAE,WAAW,KAAK,WAAW;oBAAE,OAAO,OAAO,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC,CAAC;gBACzG,IAAI,QAAQ;oBAAE,OAAO,QAAQ,CAAC;gBAC9B,OAAO,GAAG,KAAK,CAAC;gBAChB,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,KAAK,CAAC,QAAQ,EAAE,OAAO,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE;oBAC7D,IAAI,CAAC,MAAM,CAAC,EAAE,IAAI,CAAC,MAAM,CAAC,SAAS;wBAAE,OAAO,GAAG,IAAI,CAAC;oBACpD,OAAO,MAAM,CAAC;gBAChB,CAAC,CAAC,CAAC;gBACH,OAAO,QAAQ,CAAC;YAClB,CAAC;YACD,IAAI,EAAE,CAAC,MAAM,EAAE,UAAU,EAAE,EAAE;gBAC3B,4GAA4G;gBAC5G,6CAA6C;gBAC7C,MAAM,KAAK,GAAG,UAAU,EAAE,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,EAAE,QAAQ,CAAC,CAAC;gBACjD,OAAO,QAAQ,KAAK,CAAC,KAAK,IAAI,EAAE;oBAC9B,MAAM,QAAQ,EAAE,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;oBAChC,MAAM,GAAG,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;oBAC5D,MAAM,MAAM,GAAG,CAAC,GAAG,CAAC,SAAS,CAAC,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC;oBAChE,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC;oBAC3B,OAAO,EAAE,CAAC;oBACV,OAAO,MAAM,CAAC;gBAChB,CAAC,CAAC,EAAE,CAAC;YACP,CAAC;YACD,aAAa,EAAE,OAAO;SACvB,CAAC;QACF,IAAI,CAAC,KAAK,GAAG,EAAE,OAAO,EAAE,EAAE,EAAE,WAAW,EAAE,CAAC;QAC1C,OAAO,WAAW,CAAC;IACrB,CAAC;CACF"}
1
+ {"version":3,"file":"purpose-lane.js","sourceRoot":"","sources":["../../src/tunnel/purpose-lane.ts"],"names":[],"mappings":"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"}
@@ -50,6 +50,7 @@ import { GuestsCard } from "./settings-guests.js";
50
50
  import { OfficeSharesCard } from "./office-share-dialog.js";
51
51
  import { syncStage, useDraft } from "./settings-draft.js";
52
52
  import { WebAuthorityCard } from "./settings-web-authority.js";
53
+ import { NamedLinkTest, setTypedTunnelToken } from "./settings-named-test.js";
53
54
  import { SignInList } from "./backend-signin.js";
54
55
 
55
56
  const tn = (k, ...v) => t(`settings.${k}`, ...v);
@@ -1353,6 +1354,7 @@ function WebSection({ ctx, staged, part = "all" }) {
1353
1354
  <div class=${fc("field", "originPort")}><label for="publicWebOriginPort">${tn("namedOriginPort")}</label><input id="publicWebOriginPort" type="number" min="1024" max="65535" value=${f.originPort} class=${errors.originPort ? "invalid" : ""} onInput=${(e) => set("originPort")(e.target.value)} /><${Feedback} error=${errors.originPort} /></div>
1354
1355
  </div>
1355
1356
  <p class="note">${tn("namedTunnelHint")} <a href="https://github.com/songsid/AgEnD/blob/main/docs/named-tunnel.md" target="_blank" rel="noopener noreferrer">${tn("namedTunnelSetup")}</a></p>
1357
+ ${String(f.hostname || "").trim() ? html`<${NamedLinkTest} hostname=${f.hostname} port=${f.originPort} protocol=${f.protocol} />` : null}
1356
1358
  <div class="grid2">
1357
1359
  <div class=${fc("field", "ttl")}><label for="publicWebTtl">${tn(named ? "publicTtlNamed" : "publicTtl")}</label><input id="publicWebTtl" type="number" min=${named ? "0" : "1"} max=${named ? "10080" : "480"} step="1" class=${errors.ttl ? "invalid" : ""} value=${f.ttl} onInput=${(e) => set("ttl")(e.target.value)} /><${Feedback} error=${errors.ttl} /></div>
1358
1360
  <div class=${fc("field", "protocol")}><label for="publicWebProtocol">${tn("publicProtocol")}</label><${Select} id="publicWebProtocol" value=${f.protocol} onChange=${set("protocol")} options=${["http2", "quic", "auto"]} /></div>
@@ -1548,11 +1550,11 @@ function PublicTunnelToken() {
1548
1550
  const result = await api(base);
1549
1551
  if (lease.current() && result.ok) setPresent(result.body?.[0]?.token_present === true);
1550
1552
  };
1551
- useEffect(() => { refresh().catch(() => {}); }, []);
1553
+ useEffect(() => { refresh().catch(() => {}); return () => setTypedTunnelToken(""); }, []);
1552
1554
  const go = async (clear) => {
1553
1555
  if (inFlight.current || !clear && !secret) return;
1554
1556
  const value = secret;
1555
- setSecret(""); inFlight.current = true; setBusy(true); setMessage("");
1557
+ setSecret(""); setTypedTunnelToken(""); inFlight.current = true; setBusy(true); setMessage("");
1556
1558
  const key = newKey("tunnel-token");
1557
1559
  try {
1558
1560
  const prepared = await api(`${base}/verify`, { method: "POST", headers: { "Idempotency-Key": key }, body: JSON.stringify(clear ? { clear: true } : { secret: value }) });
@@ -1576,7 +1578,7 @@ function PublicTunnelToken() {
1576
1578
  return html`<section class="card"><h3>${tn("namedToken")}</h3>
1577
1579
  <p class="note">${tn("namedTokenHint")}</p>
1578
1580
  <span class="tag">${present ? tn("keyConfigured") : tn("keyNotConfigured")}</span>
1579
- <input id="publicTunnelToken" type="password" autocomplete="new-password" aria-label=${tn("namedToken")} value=${secret} onInput=${e => setSecret(e.target.value)} />
1581
+ <input id="publicTunnelToken" type="password" autocomplete="new-password" aria-label=${tn("namedToken")} value=${secret} onInput=${e => { setSecret(e.target.value); setTypedTunnelToken(e.target.value); }} />
1580
1582
  <button type="button" class="btn btn-sm" disabled=${busy || !secret} onClick=${() => go(false)}>${tn("namedTokenSet")}</button>
1581
1583
  <button type="button" class="btn btn-sm" disabled=${busy || !present} onClick=${() => go(true)}>${tn("namedTokenClear")}</button>
1582
1584
  ${message ? html`<p class="note" role="status">${message}</p>` : null}
@@ -0,0 +1,144 @@
1
+ // #2906 (the user, 2026-11-01): Settings → Access → the fixed address's 「測試連線」 / "Test connection". It tests the
2
+ // values as filled in (unsaved too: the typed connector token, else the saved one) and shows each step ✓ or ✗; every ✗
3
+ // says what the person can do next. The server answers closed step codes only (settings-api.ts, named-link-test.ts):
4
+ // the token never comes back. Words never name the person's workplace — "this machine's network", "network administrator".
5
+ import { html, useRef, useState } from "/assets/app-html.js";
6
+ import { register, t } from "/assets/app-i18n.js";
7
+ import { useLease } from "/assets/app-ctx.js";
8
+ import { api } from "./settings-confirm.js";
9
+
10
+ register("settings", {
11
+ en: {
12
+ ntButton: "Test connection",
13
+ ntRunning: "Testing… (up to about a minute)",
14
+ ntHint: "Tests the values above as filled in, saved or not — with the token typed below, or the saved one.",
15
+ ntBusy: "A test is already running. Try again when it ends.",
16
+ ntFailed: "The test could not run. Try again.",
17
+ ntNotRun: "Not tested (an earlier step failed).",
18
+ ntTimeout: "The test took too long and was stopped. Test again; if it repeats, check this machine's network.",
19
+ ntCancelled: "Your sign-in ended, so nothing was started. Sign in again and test again.",
20
+ ntCleanup: "The test's connector could not be confirmed stopped. Run agend restart before testing or opening the link again.",
21
+ ntStep_values: "The domain, port and token",
22
+ ntStep_origin: "This machine answers on port {1}",
23
+ ntStep_tunnel: "The tunnel connects to Cloudflare",
24
+ ntStep_dns: "{0} can be looked up",
25
+ ntStep_reach: "{0} reaches this AgEnD",
26
+ nt_values_ok: "Filled in correctly.",
27
+ "nt_values_bad-hostname": "Enter a domain such as agend.example.com (no https://, path or port).",
28
+ "nt_values_bad-port": "Enter a port from 1024 to 65535.",
29
+ "nt_values_no-token": "No connector token: paste it below (Cloudflare → your tunnel → the long string that starts with eyJ).",
30
+ "nt_values_bad-token": "This is not a connector token: copy only the long string that starts with eyJ.",
31
+ nt_origin_ok: "Answered for this test.",
32
+ nt_origin_live: "Your link is open on these values: it is tested as it is.",
33
+ "nt_origin_dashboard-port": "{1} is the dashboard's own port. Choose another (19282 by default), and make the Cloudflare route's service http://127.0.0.1:<that port>.",
34
+ "nt_origin_port-in-use": "Port {1} is used by another program on this machine. Choose another port here, and make the Cloudflare route's service http://127.0.0.1:<that port>.",
35
+ "nt_origin_origin-failed": "AgEnD could not listen on port {1}. Try another port.",
36
+ "nt_origin_link-open-other": "Your link is open with other settings. Close it (/web → Close public link), then test again.",
37
+ "nt_origin_tunnel-busy": "Another tunnel is starting or running (a public link or a sign-in). Try again in a minute.",
38
+ nt_tunnel_ok: "Connected to Cloudflare.",
39
+ nt_tunnel_live: "Your open link is connected.",
40
+ nt_tunnel_auth: "Cloudflare refused the token: it is wrong or was rotated. Copy the current token from the tunnel's page in Cloudflare and paste it again.",
41
+ nt_tunnel_network: "This machine could not reach Cloudflare within 20 seconds. Set the protocol to http2 here, or ask the network administrator to allow outbound connections to Cloudflare (port 7844).",
42
+ nt_tunnel_stopped: "cloudflared stopped before it connected. Test again; if it repeats, check the token and the protocol.",
43
+ "nt_tunnel_no-binary": "cloudflared could not be prepared on this machine (its download or install failed). Check this machine's internet access and test again.",
44
+ nt_dns_ok: "Found.",
45
+ "nt_dns_dns-missing": "{0} does not exist yet. In Cloudflare, add a Published application route for {0} on this tunnel.",
46
+ "nt_dns_dns-failed": "This machine could not look up {0}: this machine's network may block DNS. Ask the network administrator.",
47
+ nt_reach_ok: "It works: {0} reached this AgEnD.",
48
+ "nt_reach_wrong-service": "Cloudflare answered but could not reach the service: make the route's service http://127.0.0.1:{1} (HTTP, not HTTPS).",
49
+ "nt_reach_other-page": "{0} answered with another page: its route points to another service or tunnel. Point it to this tunnel, with the service http://127.0.0.1:{1}.",
50
+ "nt_reach_route-missing": "{0} is not routed to this tunnel. In Cloudflare, add a Published application route for {0}.",
51
+ "nt_reach_self-unreachable": "The tunnel is connected, but this machine's network can't open {0} (some networks filter new domains). From another network, such as a phone on mobile data, it may still work. Ask the network administrator to allow {0}.",
52
+ "nt_reach_reach-failed": "{0} did not answer as expected. Test again in a minute; if it repeats, check the route in Cloudflare.",
53
+ },
54
+ "zh-TW": {
55
+ ntButton: "測試連線",
56
+ ntRunning: "測試中…(最多約 1 分鐘)",
57
+ ntHint: "用上面填的設定測試(不論是否已儲存),token 用下方輸入的,沒有就用已儲存的。",
58
+ ntBusy: "已經有一個測試在進行,請等它結束再試。",
59
+ ntFailed: "測試沒有執行成功,請再試一次。",
60
+ ntNotRun: "未測試(前面的步驟失敗)。",
61
+ ntTimeout: "測試太久,已經停止。請再測一次;如果一直這樣,請檢查這台主機所在的網路。",
62
+ ntCancelled: "你的登入已結束,所以沒有啟動任何東西。請重新登入後再測。",
63
+ ntCleanup: "無法確認測試用的連接器已經停止。請先執行 agend restart,再測試或開啟連結。",
64
+ ntStep_values: "網域、port 與 token",
65
+ ntStep_origin: "這台主機在 port {1} 回應",
66
+ ntStep_tunnel: "tunnel 連上 Cloudflare",
67
+ ntStep_dns: "查得到 {0}",
68
+ ntStep_reach: "{0} 連得到這台 AgEnD",
69
+ nt_values_ok: "填寫正確。",
70
+ "nt_values_bad-hostname": "請填網域,例如 agend.example.com(不含 https://、路徑或 port)。",
71
+ "nt_values_bad-port": "請填 1024–65535 之間的 port。",
72
+ "nt_values_no-token": "沒有 connector token:請貼在下方(Cloudflare → 你的 tunnel → 以 eyJ 開頭的長字串)。",
73
+ "nt_values_bad-token": "這不是 connector token:只複製以 eyJ 開頭的那串長字串。",
74
+ nt_origin_ok: "這次測試有回應。",
75
+ nt_origin_live: "你的連結正以這些設定開著:直接測試它。",
76
+ "nt_origin_dashboard-port": "{1} 是儀表板自己的 port。請換一個(預設 19282),並把 Cloudflare 路由的 service 設成 http://127.0.0.1:<那個 port>。",
77
+ "nt_origin_port-in-use": "這台主機上有其他程式在用 port {1}。請在這裡換一個 port,並把 Cloudflare 路由的 service 改成 http://127.0.0.1:<那個 port>。",
78
+ "nt_origin_origin-failed": "AgEnD 無法在 port {1} 監聽,請換一個 port。",
79
+ "nt_origin_link-open-other": "你的連結正以其他設定開著。請先關閉(/web → 關閉公開連結),再測試。",
80
+ "nt_origin_tunnel-busy": "另一條 tunnel 正在啟動或執行(公開連結或登入用)。請過一分鐘再試。",
81
+ nt_tunnel_ok: "已連上 Cloudflare。",
82
+ nt_tunnel_live: "開著的連結已連線。",
83
+ nt_tunnel_auth: "Cloudflare 拒絕這個 token:它是錯的或已被更換。請到 Cloudflare 的 tunnel 頁面複製目前的 token 再貼上。",
84
+ nt_tunnel_network: "這台主機在 20 秒內連不上 Cloudflare。請在這裡把通訊協定改成 http2,或請網路管理員允許對 Cloudflare 的對外連線(port 7844)。",
85
+ nt_tunnel_stopped: "cloudflared 在連上之前就停止了。請再測一次;如果一直這樣,請檢查 token 與通訊協定。",
86
+ "nt_tunnel_no-binary": "這台主機無法準備 cloudflared(下載或安裝失敗)。請確認這台主機能連上網路後再測。",
87
+ nt_dns_ok: "查得到。",
88
+ "nt_dns_dns-missing": "{0} 還不存在。請在 Cloudflare 為這條 tunnel 新增 {0} 的 Published application 路由。",
89
+ "nt_dns_dns-failed": "這台主機查不到 {0}:這台主機所在的網路可能擋了 DNS。請洽網路管理員。",
90
+ nt_reach_ok: "正常:{0} 連到了這台 AgEnD。",
91
+ "nt_reach_wrong-service": "Cloudflare 有回應,但連不到服務:請把路由的 service 設成 http://127.0.0.1:{1}(HTTP,不是 HTTPS)。",
92
+ "nt_reach_other-page": "{0} 回應了別的頁面:它的路由指向別的服務或 tunnel。請把它指向這條 tunnel,service 設成 http://127.0.0.1:{1}。",
93
+ "nt_reach_route-missing": "{0} 沒有路由到這條 tunnel。請在 Cloudflare 新增 {0} 的 Published application 路由。",
94
+ "nt_reach_self-unreachable": "tunnel 已連上,但這台主機所在的網路打不開 {0}(有些網路會擋新的網域)。從其他網路(例如手機行動網路)可能可以正常使用。請網路管理員允許 {0}。",
95
+ "nt_reach_reach-failed": "{0} 沒有如預期回應。請過一分鐘再測;如果一直這樣,請檢查 Cloudflare 上的路由。",
96
+ },
97
+ });
98
+ const tn = (k, ...v) => t(`settings.${k}`, ...v);
99
+ const STEPS = ["values", "origin", "tunnel", "dns", "reach"];
100
+
101
+ /**
102
+ * The connector token typed in the token card below (not yet saved): the test uses it when there is one. The card
103
+ * owns it: it sets it as typed and clears it when saved, cleared or unmounted (#2907 review: a hidden token from a
104
+ * card no longer on screen is never used).
105
+ */
106
+ let typedToken = "";
107
+ export function setTypedTunnelToken(value) { typedToken = typeof value === "string" ? value : ""; }
108
+
109
+ /** The button and its result, for the fixed-address values the form holds now. */
110
+ export function NamedLinkTest({ hostname, port, protocol }) {
111
+ const lease = useLease("named-link-test");
112
+ const [busy, setBusy] = useState(false);
113
+ const [result, setResult] = useState(null);
114
+ const [note, setNote] = useState("");
115
+ const running = useRef(false);
116
+ const run = async () => {
117
+ if (running.current) return;
118
+ running.current = true; setBusy(true); setNote(""); setResult(null);
119
+ try {
120
+ const r = await api("/api/settings/public-link/test", { method: "POST", body: JSON.stringify({
121
+ hostname: String(hostname ?? "").trim(), origin_port: String(port ?? "").trim() || "19282", protocol, ...(typedToken.trim() ? { token: typedToken } : {}),
122
+ }) });
123
+ if (!lease.current()) return;
124
+ if (r.status === 409) setNote(tn("ntBusy"));
125
+ else if (!r.ok || !Array.isArray(r.body?.steps)) setNote(tn("ntFailed"));
126
+ else setResult(r.body);
127
+ } catch { if (lease.current()) setNote(tn("ntFailed")); }
128
+ finally { running.current = false; if (lease.current()) setBusy(false); }
129
+ };
130
+ const host = result?.hostname || String(hostname ?? "").trim() || "—", shownPort = result?.port ?? (String(port ?? "").trim() || "19282");
131
+ return html`<div class="named-test">
132
+ <div class="save-row"><button type="button" class="btn btn-sm" id="namedLinkTest" disabled=${busy} onClick=${run}>${busy ? tn("ntRunning") : tn("ntButton")}</button></div>
133
+ <p class="note">${tn("ntHint")}</p>
134
+ ${note ? html`<p class="note" role="status">${note}</p>` : null}
135
+ ${result ? html`<ol class="named-test-steps" role="status" aria-live="polite">${STEPS.map((step) => {
136
+ const r = result.steps.find((s) => s.step === step);
137
+ const mark = !r ? "–" : r.ok ? "✓" : "✗";
138
+ const said = !r ? tn("ntNotRun") : r.code === "timeout" ? tn("ntTimeout") : r.code === "cancelled" ? tn("ntCancelled") : tn(`nt_${step}_${r.code}`, host, shownPort);
139
+ return html`<li key=${step} class=${!r ? "nt-skip" : r.ok ? "nt-ok" : "nt-fail"} data-step=${step} data-code=${r ? r.code : ""}>
140
+ <span class="nt-mark" aria-hidden="true">${mark}</span> <strong>${tn(`ntStep_${step}`, host, shownPort)}</strong> — ${said}</li>`;
141
+ })}</ol>` : null}
142
+ ${result?.cleanup ? html`<p class="feedback error" role="alert">${tn("ntCleanup")}</p>` : null}
143
+ </div>`;
144
+ }