@songsid/agend 2.1.5 → 2.1.6-beta.2

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 (102) hide show
  1. package/dist/apply-job.d.ts +114 -0
  2. package/dist/apply-job.js +214 -0
  3. package/dist/apply-job.js.map +1 -0
  4. package/dist/backend/codex.d.ts +16 -0
  5. package/dist/backend/codex.js +50 -0
  6. package/dist/backend/codex.js.map +1 -1
  7. package/dist/backend/credential-profile.d.ts +114 -0
  8. package/dist/backend/credential-profile.js +242 -0
  9. package/dist/backend/credential-profile.js.map +1 -0
  10. package/dist/backend/kiro-auth-store.d.ts +25 -0
  11. package/dist/backend/kiro-auth-store.js +61 -0
  12. package/dist/backend/kiro-auth-store.js.map +1 -0
  13. package/dist/backend/kiro.d.ts +8 -0
  14. package/dist/backend/kiro.js +31 -2
  15. package/dist/backend/kiro.js.map +1 -1
  16. package/dist/backend/types.d.ts +13 -0
  17. package/dist/backend/types.js.map +1 -1
  18. package/dist/cli.js +113 -0
  19. package/dist/cli.js.map +1 -1
  20. package/dist/config-validator.js +21 -0
  21. package/dist/config-validator.js.map +1 -1
  22. package/dist/daemon.d.ts +25 -0
  23. package/dist/daemon.js +157 -16
  24. package/dist/daemon.js.map +1 -1
  25. package/dist/fleet-level-config.d.ts +42 -0
  26. package/dist/fleet-level-config.js +78 -0
  27. package/dist/fleet-level-config.js.map +1 -0
  28. package/dist/fleet-lock.d.ts +21 -0
  29. package/dist/fleet-lock.js +42 -8
  30. package/dist/fleet-lock.js.map +1 -1
  31. package/dist/fleet-manager.d.ts +130 -1
  32. package/dist/fleet-manager.js +611 -83
  33. package/dist/fleet-manager.js.map +1 -1
  34. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +121 -0
  35. package/dist/instance-config-impact.d.ts +55 -0
  36. package/dist/instance-config-impact.js +152 -0
  37. package/dist/instance-config-impact.js.map +1 -0
  38. package/dist/locale.js +2 -0
  39. package/dist/locale.js.map +1 -1
  40. package/dist/outbound-handlers.d.ts +5 -1
  41. package/dist/outbound-handlers.js +179 -1
  42. package/dist/outbound-handlers.js.map +1 -1
  43. package/dist/outbound-schemas.d.ts +6 -5
  44. package/dist/outbound-schemas.js +1 -0
  45. package/dist/outbound-schemas.js.map +1 -1
  46. package/dist/provider-probe.d.ts +45 -0
  47. package/dist/provider-probe.js +111 -0
  48. package/dist/provider-probe.js.map +1 -0
  49. package/dist/quickstart-api.d.ts +161 -0
  50. package/dist/quickstart-api.js +340 -0
  51. package/dist/quickstart-api.js.map +1 -0
  52. package/dist/quickstart.js +23 -50
  53. package/dist/quickstart.js.map +1 -1
  54. package/dist/self-restart-limit.d.ts +21 -0
  55. package/dist/self-restart-limit.js +129 -0
  56. package/dist/self-restart-limit.js.map +1 -0
  57. package/dist/settings-api.d.ts +15 -0
  58. package/dist/settings-api.js +126 -1
  59. package/dist/settings-api.js.map +1 -1
  60. package/dist/setup-auth.d.ts +113 -0
  61. package/dist/setup-auth.js +181 -0
  62. package/dist/setup-auth.js.map +1 -0
  63. package/dist/setup-form.d.ts +18 -0
  64. package/dist/setup-form.js +315 -0
  65. package/dist/setup-form.js.map +1 -0
  66. package/dist/setup-host.d.ts +162 -0
  67. package/dist/setup-host.js +496 -0
  68. package/dist/setup-host.js.map +1 -0
  69. package/dist/setup-marker.d.ts +8 -0
  70. package/dist/setup-marker.js +39 -0
  71. package/dist/setup-marker.js.map +1 -0
  72. package/dist/setup-tunnel-consent.d.ts +53 -0
  73. package/dist/setup-tunnel-consent.js +82 -0
  74. package/dist/setup-tunnel-consent.js.map +1 -0
  75. package/dist/tips.js +1 -1
  76. package/dist/tips.js.map +1 -1
  77. package/dist/transcript-sources.d.ts +12 -1
  78. package/dist/transcript-sources.js +19 -3
  79. package/dist/transcript-sources.js.map +1 -1
  80. package/dist/tunnel/cloudflared.d.ts +71 -0
  81. package/dist/tunnel/cloudflared.js +447 -0
  82. package/dist/tunnel/cloudflared.js.map +1 -0
  83. package/dist/tunnel/lease.d.ts +75 -0
  84. package/dist/tunnel/lease.js +209 -0
  85. package/dist/tunnel/lease.js.map +1 -0
  86. package/dist/tunnel/manager.d.ts +56 -0
  87. package/dist/tunnel/manager.js +167 -0
  88. package/dist/tunnel/manager.js.map +1 -0
  89. package/dist/tunnel/types.d.ts +119 -0
  90. package/dist/tunnel/types.js +33 -0
  91. package/dist/tunnel/types.js.map +1 -0
  92. package/dist/ui/dashboard.html +4 -3
  93. package/dist/ui/settings.html +640 -153
  94. package/dist/usage/providers.d.ts +16 -13
  95. package/dist/usage/providers.js +79 -53
  96. package/dist/usage/providers.js.map +1 -1
  97. package/dist/web-api.js +6 -4
  98. package/dist/web-api.js.map +1 -1
  99. package/dist/web-auth.d.ts +75 -0
  100. package/dist/web-auth.js +208 -2
  101. package/dist/web-auth.js.map +1 -1
  102. package/package.json +1 -1
@@ -0,0 +1,209 @@
1
+ /**
2
+ * The record that says "a tunnel process may exist right now".
3
+ *
4
+ * It is written before anything is spawned and removed only after a death has
5
+ * been proven, so the file's presence is the pessimistic answer: a lease we
6
+ * cannot resolve blocks the next managed tunnel rather than being assumed
7
+ * stale. That is the whole point — a tunnel nobody is tracking is a public
8
+ * entrance nobody is watching.
9
+ *
10
+ * Deliberately free of any dependency on the fleet: the pre-fleet setup host
11
+ * has to run the reaper too, and it runs in a process where no FleetManager
12
+ * exists. Everything here takes a data directory and nothing else.
13
+ */
14
+ import { closeSync, fsyncSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeSync } from "node:fs";
15
+ import { randomBytes } from "node:crypto";
16
+ import { join } from "node:path";
17
+ import { probeProcess } from "../web-terminal.js";
18
+ import { TUNNEL_STOP_GRACE_MS } from "./types.js";
19
+ export function leasePath(dataDir) {
20
+ return join(dataDir, "tunnel.lease");
21
+ }
22
+ /**
23
+ * Atomic, owner-only, and on disk before the call returns.
24
+ *
25
+ * The fsync is not ceremony: the lease has to survive the crash that makes it
26
+ * matter, and a rename that is only in page cache does not.
27
+ */
28
+ export function writeLease(dataDir, lease) {
29
+ mkdirSync(dataDir, { recursive: true });
30
+ const path = leasePath(dataDir);
31
+ const temp = `${path}.tmp-${process.pid}-${randomBytes(6).toString("hex")}`;
32
+ const fd = openSync(temp, "wx", 0o600);
33
+ try {
34
+ writeSync(fd, JSON.stringify(lease));
35
+ fsyncSync(fd);
36
+ }
37
+ finally {
38
+ closeSync(fd);
39
+ }
40
+ try {
41
+ renameSync(temp, path);
42
+ }
43
+ catch (err) {
44
+ try {
45
+ unlinkSync(temp);
46
+ }
47
+ catch { /* already gone */ }
48
+ throw err;
49
+ }
50
+ }
51
+ export function readLease(dataDir) {
52
+ let raw;
53
+ try {
54
+ raw = readFileSync(leasePath(dataDir), "utf8");
55
+ }
56
+ catch (err) {
57
+ if (err.code === "ENOENT")
58
+ return null;
59
+ // Unreadable is not absent. Saying "no lease" here would let a second
60
+ // tunnel start beside one we simply could not see.
61
+ throw err;
62
+ }
63
+ try {
64
+ const parsed = JSON.parse(raw);
65
+ if (typeof parsed.provider !== "string" || typeof parsed.ownerPid !== "number")
66
+ return CORRUPT;
67
+ return {
68
+ sid: String(parsed.sid ?? ""),
69
+ provider: parsed.provider,
70
+ originPort: Number(parsed.originPort ?? 0),
71
+ providerPid: typeof parsed.providerPid === "number" ? parsed.providerPid : null,
72
+ strongIdentity: typeof parsed.strongIdentity === "string" ? parsed.strongIdentity : null,
73
+ expiresAt: Number(parsed.expiresAt ?? 0),
74
+ ownerPid: parsed.ownerPid,
75
+ ownerIdentity: typeof parsed.ownerIdentity === "string" ? parsed.ownerIdentity : null,
76
+ };
77
+ }
78
+ catch {
79
+ return CORRUPT;
80
+ }
81
+ }
82
+ /**
83
+ * A lease we cannot parse still means a tunnel may exist.
84
+ *
85
+ * Returning null would be "no tunnel", which is the one answer a damaged file
86
+ * cannot justify. This stands in as an unresolvable lease, so the reaper asks
87
+ * for a human instead of clearing the way.
88
+ */
89
+ const CORRUPT = {
90
+ sid: "", provider: "unknown", originPort: 0,
91
+ providerPid: null, strongIdentity: null, expiresAt: 0, ownerPid: 0, ownerIdentity: null,
92
+ };
93
+ export function clearLease(dataDir) {
94
+ try {
95
+ unlinkSync(leasePath(dataDir));
96
+ }
97
+ catch { /* already gone */ }
98
+ }
99
+ /**
100
+ * Resolve whatever the last run left behind, before anything new is started.
101
+ *
102
+ * Runs from any process — the fleet at startup and the pre-fleet setup host
103
+ * both call it, which is why it takes a directory rather than a manager.
104
+ */
105
+ export async function reapStaleTunnel(dataDir, opts = {}) {
106
+ const now = opts.now ?? Date.now;
107
+ const probe = opts.probe ?? probeProcess;
108
+ const kill = opts.kill ?? ((pid, signal) => { process.kill(pid, signal); });
109
+ const wait = opts.wait ?? ((ms) => new Promise(r => setTimeout(r, ms)));
110
+ const graceMs = opts.graceMs ?? TUNNEL_STOP_GRACE_MS;
111
+ let lease;
112
+ try {
113
+ lease = readLease(dataDir);
114
+ }
115
+ catch (err) {
116
+ // A lease we cannot read is not a lease that is absent. Letting this throw
117
+ // meant an unreadable file took the whole start down instead of closing it:
118
+ // the caller asks "may I open a tunnel", and the honest answer here is no.
119
+ return {
120
+ kind: "manual",
121
+ reason: `the lease file could not be read (${err.code ?? "unknown error"})`,
122
+ pid: null,
123
+ leasePath: leasePath(dataDir),
124
+ };
125
+ }
126
+ if (!lease)
127
+ return { kind: "clear" };
128
+ // Someone else is using it right now. An expired lease held by a live owner
129
+ // is still theirs: the owner is the one that will prove its child's death,
130
+ // and reaping underneath it would race that proof.
131
+ //
132
+ // "Alive" means the same process, not the same number. A pid that outlived
133
+ // its owner and was handed to something unrelated would otherwise hold this
134
+ // lease forever — which is exactly how ticket 5's setup-host lock became
135
+ // unreclaimable.
136
+ if (lease.ownerPid > 0 && lease.ownerPid !== process.pid) {
137
+ const owner = probe(lease.ownerPid);
138
+ const sameOwner = owner.kind === "identified" && lease.ownerIdentity
139
+ ? owner.identity === lease.ownerIdentity
140
+ : owner.kind !== "gone";
141
+ if (sameOwner)
142
+ return { kind: "held", ownerPid: lease.ownerPid };
143
+ }
144
+ void now;
145
+ if (lease.providerPid === null) {
146
+ // Reserved but never recorded: either nothing was ever spawned, or the
147
+ // owner died in the instant between the spawn returning and the pid being
148
+ // written. The second case leaves a child we cannot name, and guessing is
149
+ // exactly what this whole file exists to avoid.
150
+ return {
151
+ kind: "manual",
152
+ reason: "the lease names no process, so a tunnel may exist that cannot be identified",
153
+ pid: null,
154
+ leasePath: leasePath(dataDir),
155
+ };
156
+ }
157
+ const before = probe(lease.providerPid);
158
+ if (before.kind === "gone") {
159
+ clearLease(dataDir);
160
+ return { kind: "reaped", how: "already-gone", pid: lease.providerPid };
161
+ }
162
+ if (before.kind === "identified" && lease.strongIdentity && before.identity !== lease.strongIdentity) {
163
+ // The pid belongs to something else now, which proves the original died.
164
+ // Signalling it would kill an innocent process — the single most damaging
165
+ // thing a reaper can do, and the reason the fingerprint is recorded at all.
166
+ clearLease(dataDir);
167
+ return { kind: "reaped", how: "pid-reused", pid: lease.providerPid };
168
+ }
169
+ if (before.kind === "unknown" || !lease.strongIdentity) {
170
+ return {
171
+ kind: "manual",
172
+ reason: before.kind === "unknown"
173
+ ? "this platform cannot prove which process holds that pid"
174
+ : "the lease carries no fingerprint, so the pid cannot be matched to the original process",
175
+ pid: lease.providerPid,
176
+ leasePath: leasePath(dataDir),
177
+ };
178
+ }
179
+ for (const signal of ["SIGTERM", "SIGKILL"]) {
180
+ try {
181
+ kill(lease.providerPid, signal);
182
+ }
183
+ catch { /* it may have just exited */ }
184
+ await wait(graceMs);
185
+ const after = probe(lease.providerPid);
186
+ if (after.kind === "gone") {
187
+ clearLease(dataDir);
188
+ return { kind: "reaped", how: "killed", pid: lease.providerPid };
189
+ }
190
+ if (after.kind === "identified" && after.identity !== lease.strongIdentity) {
191
+ clearLease(dataDir);
192
+ return { kind: "reaped", how: "pid-reused", pid: lease.providerPid };
193
+ }
194
+ }
195
+ return {
196
+ kind: "manual",
197
+ reason: "the process did not exit after SIGTERM and SIGKILL",
198
+ pid: lease.providerPid,
199
+ leasePath: leasePath(dataDir),
200
+ };
201
+ }
202
+ /** What to tell a human when a lease could not be resolved. Never "closed safely". */
203
+ export function manualCleanupMessage(outcome) {
204
+ const target = outcome.pid === null ? "the tunnel process" : `pid ${outcome.pid}`;
205
+ return `A tunnel could not be confirmed closed: ${outcome.reason}. `
206
+ + `No new tunnel will be opened until this is resolved. Check ${target}, kill it if it is still running, `
207
+ + `then delete ${outcome.leasePath}.`;
208
+ }
209
+ //# sourceMappingURL=lease.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"lease.js","sourceRoot":"","sources":["../../src/tunnel/lease.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,56 @@
1
+ import { probeProcess } from "../web-terminal.js";
2
+ import { type ReapOptions, type ReapOutcome } from "./lease.js";
3
+ import { type TunnelHandle, type TunnelProvider, type TunnelStartContext, type TunnelStopResult } from "./types.js";
4
+ export interface ManagedTunnelOptions extends ReapOptions {
5
+ dataDir: string;
6
+ log?: (message: string) => void;
7
+ }
8
+ export type ManagedStartResult = {
9
+ readonly ok: true;
10
+ readonly handle: TunnelHandle;
11
+ } | {
12
+ readonly ok: false;
13
+ readonly errorKind: string;
14
+ readonly message: string;
15
+ readonly leaseHeld: boolean;
16
+ };
17
+ /**
18
+ * Owns the fleet-wide managed-tunnel lease.
19
+ *
20
+ * Fleet-wide rather than per-caller: the lease outlives the process that wrote
21
+ * it, so "am I already running one" is a question about the machine, not about
22
+ * this object.
23
+ */
24
+ export declare class ManagedTunnel {
25
+ private readonly opts;
26
+ private inFlight;
27
+ private active;
28
+ /** Set when a stop could not be proven; blocks every later start in this process. */
29
+ private blocked;
30
+ constructor(opts: ManagedTunnelOptions);
31
+ private log;
32
+ /** Our own fingerprint, so a reaper can tell this owner from a reused pid. */
33
+ private ownerIdentity;
34
+ get handle(): TunnelHandle | null;
35
+ /** Clear whatever a previous run left behind. Safe to call from any process. */
36
+ reap(): Promise<ReapOutcome>;
37
+ /**
38
+ * Single-flight: two concurrent callers get the same attempt, not two tunnels.
39
+ *
40
+ * The guard is set synchronously before the first await, because "check then
41
+ * await then set" is how two callers both find it free.
42
+ */
43
+ start(provider: TunnelProvider, ctx: TunnelStartContext): Promise<ManagedStartResult>;
44
+ private doStart;
45
+ /**
46
+ * Stop the active tunnel and release the lease — but only on proof.
47
+ *
48
+ * An unconfirmed stop keeps the lease and blocks the next start. The caller
49
+ * gets the result rather than an exception precisely so it cannot forget to
50
+ * say so out loud.
51
+ */
52
+ stop(reason: string): Promise<TunnelStopResult>;
53
+ }
54
+ /** A tunnel's opaque id. Not a credential — it identifies, it does not authorize. */
55
+ export declare function newTunnelSid(): string;
56
+ export { probeProcess };
@@ -0,0 +1,167 @@
1
+ /**
2
+ * One managed tunnel at a time, and never one nobody is tracking.
3
+ *
4
+ * The provider knows how to run cloudflared. This knows the two rules that make
5
+ * running it safe: the lease is written before a child can exist and removed
6
+ * only after a death is proven, and a start that fails does not report failure
7
+ * until it has proven the child it created is gone. A "clean" fallback running
8
+ * beside a live tunnel is the failure this exists to prevent.
9
+ */
10
+ import { randomBytes } from "node:crypto";
11
+ import { probeProcess } from "../web-terminal.js";
12
+ import { clearLease, manualCleanupMessage, reapStaleTunnel, writeLease } from "./lease.js";
13
+ import { TunnelStartError, } from "./types.js";
14
+ /**
15
+ * Owns the fleet-wide managed-tunnel lease.
16
+ *
17
+ * Fleet-wide rather than per-caller: the lease outlives the process that wrote
18
+ * it, so "am I already running one" is a question about the machine, not about
19
+ * this object.
20
+ */
21
+ export class ManagedTunnel {
22
+ opts;
23
+ inFlight = null;
24
+ active = null;
25
+ /** Set when a stop could not be proven; blocks every later start in this process. */
26
+ blocked = null;
27
+ constructor(opts) {
28
+ this.opts = opts;
29
+ }
30
+ log(message) { (this.opts.log ?? (() => { }))(message); }
31
+ /** Our own fingerprint, so a reaper can tell this owner from a reused pid. */
32
+ ownerIdentity() {
33
+ const probe = (this.opts.probe ?? probeProcess)(process.pid);
34
+ return probe.kind === "identified" ? probe.identity : null;
35
+ }
36
+ get handle() { return this.active; }
37
+ /** Clear whatever a previous run left behind. Safe to call from any process. */
38
+ async reap() {
39
+ const outcome = await reapStaleTunnel(this.opts.dataDir, this.opts);
40
+ if (outcome.kind === "manual") {
41
+ this.blocked = manualCleanupMessage(outcome);
42
+ this.log(this.blocked);
43
+ }
44
+ return outcome;
45
+ }
46
+ /**
47
+ * Single-flight: two concurrent callers get the same attempt, not two tunnels.
48
+ *
49
+ * The guard is set synchronously before the first await, because "check then
50
+ * await then set" is how two callers both find it free.
51
+ */
52
+ start(provider, ctx) {
53
+ if (this.inFlight)
54
+ return this.inFlight;
55
+ const run = this.doStart(provider, ctx).finally(() => { this.inFlight = null; });
56
+ this.inFlight = run;
57
+ return run;
58
+ }
59
+ async doStart(provider, ctx) {
60
+ if (this.blocked) {
61
+ return { ok: false, errorKind: "lease-held", message: this.blocked, leaseHeld: true };
62
+ }
63
+ if (this.active) {
64
+ return { ok: false, errorKind: "lease-held", message: "a managed tunnel is already running", leaseHeld: true };
65
+ }
66
+ const outcome = await this.reap();
67
+ if (outcome.kind === "held") {
68
+ return {
69
+ ok: false, errorKind: "lease-held", leaseHeld: true,
70
+ message: `another process (pid ${outcome.ownerPid}) already owns the managed tunnel`,
71
+ };
72
+ }
73
+ if (outcome.kind === "manual") {
74
+ return { ok: false, errorKind: "lease-held", message: manualCleanupMessage(outcome), leaseHeld: true };
75
+ }
76
+ // Reserved before the spawn, not after: between these two writes a child
77
+ // may come into existence, and a crash in that window must leave evidence
78
+ // rather than silence.
79
+ writeLease(this.opts.dataDir, {
80
+ sid: ctx.sid,
81
+ provider: provider.name,
82
+ originPort: Number(ctx.origin.port),
83
+ providerPid: null,
84
+ strongIdentity: null,
85
+ expiresAt: ctx.expiresAt,
86
+ ownerPid: process.pid,
87
+ ownerIdentity: this.ownerIdentity(),
88
+ });
89
+ let handle;
90
+ try {
91
+ handle = await provider.start(ctx);
92
+ }
93
+ catch (err) {
94
+ const startError = err instanceof TunnelStartError ? err : null;
95
+ if (startError?.unconfirmed) {
96
+ // A child exists and we cannot prove otherwise. The lease stays, and so
97
+ // does the block: this is the one case where doing nothing further is
98
+ // the correct, and only honest, behaviour.
99
+ writeLease(this.opts.dataDir, {
100
+ sid: ctx.sid,
101
+ provider: provider.name,
102
+ originPort: Number(ctx.origin.port),
103
+ providerPid: startError.unconfirmed.pid,
104
+ strongIdentity: startError.unconfirmed.identity,
105
+ expiresAt: ctx.expiresAt,
106
+ ownerPid: process.pid,
107
+ ownerIdentity: this.ownerIdentity(),
108
+ });
109
+ this.blocked = `A tunnel process could not be confirmed stopped after a failed start`
110
+ + `${startError.unconfirmed.pid !== null ? ` (pid ${startError.unconfirmed.pid})` : ""}. `
111
+ + "No new tunnel will be opened until it is resolved.";
112
+ this.log(this.blocked);
113
+ return { ok: false, errorKind: startError.errorKind, message: this.blocked, leaseHeld: true };
114
+ }
115
+ // The provider proved its child is gone (or never made one), so the lease
116
+ // describes nothing and may go.
117
+ clearLease(this.opts.dataDir);
118
+ return {
119
+ ok: false,
120
+ errorKind: startError?.errorKind ?? "spawn-failed",
121
+ message: err.message,
122
+ leaseHeld: false,
123
+ };
124
+ }
125
+ writeLease(this.opts.dataDir, {
126
+ sid: ctx.sid,
127
+ provider: provider.name,
128
+ originPort: Number(ctx.origin.port),
129
+ providerPid: handle.pid,
130
+ strongIdentity: handle.identity,
131
+ expiresAt: ctx.expiresAt,
132
+ ownerPid: process.pid,
133
+ ownerIdentity: this.ownerIdentity(),
134
+ });
135
+ this.active = handle;
136
+ return { ok: true, handle };
137
+ }
138
+ /**
139
+ * Stop the active tunnel and release the lease — but only on proof.
140
+ *
141
+ * An unconfirmed stop keeps the lease and blocks the next start. The caller
142
+ * gets the result rather than an exception precisely so it cannot forget to
143
+ * say so out loud.
144
+ */
145
+ async stop(reason) {
146
+ const handle = this.active;
147
+ if (!handle)
148
+ return { confirmed: true };
149
+ const result = await handle.stop(reason);
150
+ this.active = null;
151
+ if (result.confirmed) {
152
+ clearLease(this.opts.dataDir);
153
+ return result;
154
+ }
155
+ this.blocked = `A tunnel could not be confirmed closed`
156
+ + `${result.pid !== null ? ` (pid ${result.pid})` : ""}: ${result.reason}. `
157
+ + "No new tunnel will be opened until it is resolved.";
158
+ this.log(this.blocked);
159
+ return result;
160
+ }
161
+ }
162
+ /** A tunnel's opaque id. Not a credential — it identifies, it does not authorize. */
163
+ export function newTunnelSid() {
164
+ return randomBytes(16).toString("hex");
165
+ }
166
+ export { probeProcess };
167
+ //# sourceMappingURL=manager.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"manager.js","sourceRoot":"","sources":["../../src/tunnel/manager.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,119 @@
1
+ /**
2
+ * What a tunnel provider can do, and nothing about how.
3
+ *
4
+ * The contract exists so the thing that owns a listener never learns a
5
+ * provider's command line. It is deliberately narrow: a provider can be asked
6
+ * whether it could run, asked to expose one already-listening loopback origin,
7
+ * and asked to stop — and the answer to "did it stop" is a first-class value,
8
+ * not an exception, because "we could not tell" is a state the caller has to
9
+ * record and act on rather than a failure to handle.
10
+ *
11
+ * See `docs/design/setup-host-tunnel.zh-TW.md` for the envelope these types
12
+ * serve, and `docs/design/web-terminal-tunnel-provider.zh-TW.md` (sol) for the
13
+ * original provider design.
14
+ */
15
+ export type TunnelVisibility = "public" | "tailnet" | "loopback" | "manual";
16
+ /** Why a provider cannot run, in bounded terms — never a vendor error string. */
17
+ export type TunnelErrorKind = "binary-missing" | "binary-not-executable" | "not-logged-in" | "spawn-failed" | "no-url" | "bad-url" | "readiness-failed" | "timeout" | "cancelled" | "lease-held";
18
+ export type PreflightResult = {
19
+ readonly ok: true;
20
+ readonly binaryPath: string;
21
+ } | {
22
+ readonly ok: false;
23
+ readonly errorKind: TunnelErrorKind;
24
+ readonly detail: string;
25
+ };
26
+ export interface TunnelStartContext {
27
+ /** Opaque id for logs and the lease; never a credential. */
28
+ readonly sid: string;
29
+ /** The origin to expose. Must be the loopback address the server really bound. */
30
+ readonly origin: URL;
31
+ /** Page path under the origin, trailing slash kept so relative assets resolve. */
32
+ readonly pagePath: string;
33
+ /**
34
+ * A string that must appear in the page fetched back over the public URL.
35
+ *
36
+ * Readiness is not "the process is alive" or "we saw a URL" — it is "the edge
37
+ * reaches the exact listener we meant", and only the caller knows what that
38
+ * listener serves.
39
+ */
40
+ readonly readinessMarker: string;
41
+ /** Wall clock. The tunnel may not outlive what it is fronting. */
42
+ readonly expiresAt: number;
43
+ readonly signal: AbortSignal;
44
+ /**
45
+ * The hostname this tunnel will answer on, as soon as it is known to be
46
+ * well-formed and before anything is fetched through it.
47
+ *
48
+ * A provider that forwards the browser's Host — which cloudflared does by
49
+ * default — makes the readiness probe arrive at the origin under the public
50
+ * hostname. An origin with a host allowlist will refuse it, and the tunnel
51
+ * that would have worked reports itself unreachable. Handing the exact
52
+ * validated string over at this point lets the origin expect it without
53
+ * loosening anything: it is the same host the provider is about to publish,
54
+ * one step earlier.
55
+ *
56
+ * The caller must undo whatever it did with this if `start` does not resolve.
57
+ */
58
+ readonly onCandidateHost?: (host: string) => void;
59
+ }
60
+ export interface TunnelExit {
61
+ readonly code: number | null;
62
+ readonly signal: NodeJS.Signals | null;
63
+ }
64
+ /**
65
+ * `confirmed: false` is an outcome, not an error.
66
+ *
67
+ * A caller that treats it as one will write "closed safely" in a log next to a
68
+ * process that is still running. It carries what a human needs to finish the
69
+ * job by hand.
70
+ */
71
+ export type TunnelStopResult = {
72
+ readonly confirmed: true;
73
+ } | {
74
+ readonly confirmed: false;
75
+ readonly reason: string;
76
+ readonly pid: number | null;
77
+ readonly identity: string | null;
78
+ };
79
+ export interface TunnelHandle {
80
+ readonly provider: string;
81
+ readonly visibility: TunnelVisibility;
82
+ /** Validated base, e.g. `https://<label>.trycloudflare.com`. No trailing slash. */
83
+ readonly baseUrl: string;
84
+ /** Where the page actually is, trailing slash intact. */
85
+ readonly pageUrl: string;
86
+ readonly pid: number | null;
87
+ /** The strong fingerprint recorded at spawn, for proving death later. */
88
+ readonly identity: string | null;
89
+ stop(reason: string): Promise<TunnelStopResult>;
90
+ /** Returns an unsubscribe. Fires only for an exit nobody asked for. */
91
+ onUnexpectedExit(listener: (exit: TunnelExit) => void): () => void;
92
+ }
93
+ export interface TunnelProvider {
94
+ readonly name: string;
95
+ /** Can this run at all? Bounded, read-only: never installs, never logs in. */
96
+ preflight(signal: AbortSignal): Promise<PreflightResult>;
97
+ /** Expose one loopback origin. Rejects with a `TunnelStartError`. */
98
+ start(ctx: TunnelStartContext): Promise<TunnelHandle>;
99
+ }
100
+ export declare class TunnelStartError extends Error {
101
+ readonly errorKind: TunnelErrorKind;
102
+ /** Present when a child was created and its death could not be confirmed. */
103
+ readonly unconfirmed?: {
104
+ readonly pid: number | null;
105
+ readonly identity: string | null;
106
+ } | undefined;
107
+ constructor(errorKind: TunnelErrorKind, message: string,
108
+ /** Present when a child was created and its death could not be confirmed. */
109
+ unconfirmed?: {
110
+ readonly pid: number | null;
111
+ readonly identity: string | null;
112
+ } | undefined);
113
+ }
114
+ /** One wall-clock budget for spawn, URL and readiness together — not per step. */
115
+ export declare const TUNNEL_STARTUP_DEADLINE_MS = 30000;
116
+ /** Preflight is a stat, not a network call. */
117
+ export declare const TUNNEL_PREFLIGHT_TIMEOUT_MS = 5000;
118
+ /** How long a polite stop is given before escalating, and again before giving up. */
119
+ export declare const TUNNEL_STOP_GRACE_MS = 5000;
@@ -0,0 +1,33 @@
1
+ /**
2
+ * What a tunnel provider can do, and nothing about how.
3
+ *
4
+ * The contract exists so the thing that owns a listener never learns a
5
+ * provider's command line. It is deliberately narrow: a provider can be asked
6
+ * whether it could run, asked to expose one already-listening loopback origin,
7
+ * and asked to stop — and the answer to "did it stop" is a first-class value,
8
+ * not an exception, because "we could not tell" is a state the caller has to
9
+ * record and act on rather than a failure to handle.
10
+ *
11
+ * See `docs/design/setup-host-tunnel.zh-TW.md` for the envelope these types
12
+ * serve, and `docs/design/web-terminal-tunnel-provider.zh-TW.md` (sol) for the
13
+ * original provider design.
14
+ */
15
+ export class TunnelStartError extends Error {
16
+ errorKind;
17
+ unconfirmed;
18
+ constructor(errorKind, message,
19
+ /** Present when a child was created and its death could not be confirmed. */
20
+ unconfirmed) {
21
+ super(message);
22
+ this.errorKind = errorKind;
23
+ this.unconfirmed = unconfirmed;
24
+ this.name = "TunnelStartError";
25
+ }
26
+ }
27
+ /** One wall-clock budget for spawn, URL and readiness together — not per step. */
28
+ export const TUNNEL_STARTUP_DEADLINE_MS = 30_000;
29
+ /** Preflight is a stat, not a network call. */
30
+ export const TUNNEL_PREFLIGHT_TIMEOUT_MS = 5_000;
31
+ /** How long a polite stop is given before escalating, and again before giving up. */
32
+ export const TUNNEL_STOP_GRACE_MS = 5_000;
33
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/tunnel/types.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AAoGH,MAAM,OAAO,gBAAiB,SAAQ,KAAK;IAE9B;IAGA;IAJX,YACW,SAA0B,EACnC,OAAe;IACf,6EAA6E;IACpE,WAA+E;QAExF,KAAK,CAAC,OAAO,CAAC,CAAC;QALN,cAAS,GAAT,SAAS,CAAiB;QAG1B,gBAAW,GAAX,WAAW,CAAoE;QAGxF,IAAI,CAAC,IAAI,GAAG,kBAAkB,CAAC;IACjC,CAAC;CACF;AAED,kFAAkF;AAClF,MAAM,CAAC,MAAM,0BAA0B,GAAG,MAAM,CAAC;AACjD,+CAA+C;AAC/C,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,CAAC;AACjD,qFAAqF;AACrF,MAAM,CAAC,MAAM,oBAAoB,GAAG,KAAK,CAAC"}
@@ -277,7 +277,8 @@
277
277
  </div>
278
278
 
279
279
  <script>
280
- const T = new URLSearchParams(location.search).get("token") || "";
280
+ // No token in the URL: the gate redeemed it for an HttpOnly session cookie,
281
+ // which the browser attaches to these same-origin requests on its own.
281
282
  const UI_LANG = localStorage.getItem("agend_lang") || (navigator.language?.toLowerCase().startsWith("zh") ? "zh-TW" : "en");
282
283
  const UI_TEXT = {
283
284
  en: {
@@ -308,7 +309,7 @@ const msgs = {};
308
309
  async function api(m, p, b) {
309
310
  const o = { method: m, headers: { "Content-Type": "application/json" } };
310
311
  if (b) o.body = JSON.stringify(b);
311
- const r = await fetch(`${p}${p.includes("?") ? "&" : "?"}token=${T}`, o);
312
+ const r = await fetch(p, o);
312
313
  return r.json();
313
314
  }
314
315
  function esc(s) { const d = document.createElement("div"); d.textContent = s || ""; return d.innerHTML; }
@@ -318,7 +319,7 @@ function toast(msg, ok=true) {
318
319
  document.body.appendChild(el); setTimeout(() => el.remove(), 3000);
319
320
  }
320
321
 
321
- const sse = new EventSource(`/ui/events?token=${T}`);
322
+ const sse = new EventSource("/ui/events");
322
323
  sse.addEventListener("status", e => {
323
324
  const d = JSON.parse(e.data); instances = d.instances;
324
325
  document.getElementById("uptime").textContent = fmtUp(d.uptime);