@tesser-sh/gateway 0.53.2 → 0.55.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.
Files changed (2) hide show
  1. package/index.js +225 -162
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -43,10 +43,8 @@ li .name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
43
43
  .ports a { color: inherit; text-decoration: none; }
44
44
  .ports a:hover { color: var(--bone); text-decoration: underline; text-decoration-color: var(--remote); text-underline-offset: 3px; }
45
45
  tesser-use { color: var(--faint); }
46
- tesser-wake { display: block; margin-top: 24px; }
47
46
  .bad { margin-top: 24px; color: var(--bad); font-size: 13px; }
48
47
  .open { margin-top: 24px; padding: 9px 16px; background: var(--remote); color: #171717; border: 0; font: 500 14px var(--sans); cursor: pointer; }
49
- .open:disabled { background: var(--rule); color: var(--dim); cursor: default; }
50
48
  a.open { display: inline-block; text-decoration: none; }
51
49
  .sub { margin-top: 8px; color: var(--dim); }
52
50
  .steps { list-style: none; padding: 0; margin-top: 24px; }
@@ -98,9 +96,11 @@ document.addEventListener("click", (event) => {
98
96
  // ../front/src/viewer.ts
99
97
  var STATUS = {
100
98
  waking: 503,
99
+ asleep: 503,
101
100
  booting: 503,
102
101
  "not-answering": 504,
103
102
  stopped: 502,
103
+ idle: 502,
104
104
  "wake-failed": 503,
105
105
  "no-port": 404,
106
106
  "not-shared": 404,
@@ -113,12 +113,16 @@ function viewerText(state) {
113
113
  switch (state.kind) {
114
114
  case "waking":
115
115
  return `${state.box.name} is waking up; retry shortly`;
116
+ case "asleep":
117
+ return `${state.box.name} is asleep; open it in a browser to wake it`;
116
118
  case "booting":
117
119
  return `${state.box.name} is booting; retry shortly`;
118
120
  case "not-answering":
119
121
  return `${state.box.name} isn't answering on port ${state.port}`;
120
122
  case "stopped":
121
123
  return `${state.box.name} has stopped`;
124
+ case "idle":
125
+ return `nothing is running on ${state.box.name}`;
122
126
  case "wake-failed":
123
127
  return `${state.box.name} didn't wake up: ${state.reason}`;
124
128
  case "no-port":
@@ -143,7 +147,10 @@ function steps(stage, port, seconds) {
143
147
  function who(box) {
144
148
  return box.sharedBy === null ? "" : `<p class="who">Shared by ${escapeHtml(box.sharedBy)}</p>`;
145
149
  }
146
- function body(state) {
150
+ function cmd(command) {
151
+ return `<div class="cmd"><code>${escapeHtml(command)}</code><button type="button" data-copy="${escapeHtml(command)}">Copy</button></div>`;
152
+ }
153
+ function body(state, owner) {
147
154
  switch (state.kind) {
148
155
  case "waking":
149
156
  return {
@@ -152,6 +159,13 @@ function body(state) {
152
159
  retry: !0,
153
160
  rest: `${who(state.box)}${steps(state.stage, state.port, state.seconds)}<p class="hint">Usually about 30 seconds. This page opens the app by itself.</p>`
154
161
  };
162
+ case "asleep":
163
+ return {
164
+ headline: `${state.box.name} is asleep`,
165
+ spinner: !1,
166
+ retry: !1,
167
+ rest: `${who(state.box)}<p class="sub">It went to sleep while nobody was using it.</p><a class="open" href="">Wake it</a>`
168
+ };
155
169
  case "booting":
156
170
  return { headline: `${state.box.name} is booting`, spinner: !0, retry: !0, rest: `${who(state.box)}<p class="hint">A fresh machine takes a minute or two. This page opens the app by itself.</p>` };
157
171
  case "not-answering":
@@ -159,21 +173,28 @@ function body(state) {
159
173
  headline: `${state.box.name} isn't answering on port ${state.port}`,
160
174
  spinner: !0,
161
175
  retry: !0,
162
- rest: `${who(state.box)}<p class="hint">The box is up but the app hasn't answered yet. Still trying.</p>`
176
+ rest: `${who(state.box)}<p class="hint">The box is up but the app hasn't answered yet. Still trying.</p>${owner ? cmd(`tesser logs ${state.box.boxId} -f`) : ""}`
163
177
  };
164
178
  case "stopped":
165
179
  return {
166
180
  headline: `${state.box.name} has stopped`,
167
181
  spinner: !1,
168
182
  retry: !0,
169
- rest: `${who(state.box)}<p class="sub">The app on this box exited. ${state.box.sharedBy === null ? "Ask whoever shared it" : "Ask them"} to restart it.</p>`
183
+ rest: owner ? cmd(`tesser logs ${state.box.boxId}`) : `${who(state.box)}<p class="sub">The app on this box exited. ${state.box.sharedBy === null ? "Ask whoever shared it" : "Ask them"} to restart it.</p>`
184
+ };
185
+ case "idle":
186
+ return {
187
+ headline: `Nothing is running on ${state.box.name}`,
188
+ spinner: !1,
189
+ retry: !0,
190
+ rest: owner ? cmd("tesser dev") : `${who(state.box)}<p class="sub">${state.box.sharedBy === null ? "Ask whoever shared it" : "Ask them"} to start the app.</p>`
170
191
  };
171
192
  case "wake-failed":
172
193
  return {
173
194
  headline: `${state.box.name} didn't wake up`,
174
195
  spinner: !1,
175
196
  retry: !1,
176
- rest: `${who(state.box)}<p class="bad">${escapeHtml(state.reason)}</p><button type="button" class="open" onclick="location.reload()">Try again</button>`
197
+ rest: `${who(state.box)}<p class="bad">${escapeHtml(state.reason)}</p><a class="open" href="">Try again</a>`
177
198
  };
178
199
  case "no-port":
179
200
  return {
@@ -188,12 +209,12 @@ function body(state) {
188
209
  return { headline: "Can't check what's shared", spinner: !0, retry: !0, rest: `<p class="sub">This gateway can't reach tesser right now. Retrying.</p>` };
189
210
  }
190
211
  }
191
- function viewerPage(state) {
192
- let { headline, spinner, retry, rest } = body(state), main = `<main>
212
+ function viewerPage(state, owner = !1) {
213
+ let { headline, spinner, retry, rest } = body(state, owner), main = `<main>
193
214
  <h1>${spinner ? '<span class="sq"></span>' : ""}${escapeHtml(headline)}</h1>
194
215
  ${rest}
195
216
  </main>`;
196
- return shell(`${headline} · tesser`, main, retry ? RETRY : "", !1);
217
+ return shell(`${headline} · tesser`, main, retry ? RETRY : "", owner);
197
218
  }
198
219
 
199
220
  // ../protocol/src/table.ts
@@ -320,7 +341,7 @@ function decodeFrame(buf) {
320
341
  }
321
342
 
322
343
  // src/cloudflare.ts
323
- import { Effect as Effect4, Schema as Schema3 } from "effect";
344
+ import { Effect as Effect5, Schema as Schema3 } from "effect";
324
345
 
325
346
  // src/jwt.ts
326
347
  import { Clock, Effect, Option } from "effect";
@@ -382,7 +403,120 @@ function audienceIncludes(aud, expected) {
382
403
 
383
404
  // src/relay.ts
384
405
  import { DurableObject } from "cloudflare:workers";
385
- import { Clock as Clock3, Duration as Duration2, Effect as Effect3, Layer, Schema as Schema2 } from "effect";
406
+ import { Clock as Clock3, Duration as Duration2, Effect as Effect4, Layer as Layer2, Schema as Schema2 } from "effect";
407
+
408
+ // ../front/src/http.ts
409
+ var HOP_BY_HOP = [
410
+ "connection",
411
+ "keep-alive",
412
+ "proxy-authenticate",
413
+ "proxy-authorization",
414
+ "proxy-connection",
415
+ "te",
416
+ "trailer",
417
+ "transfer-encoding",
418
+ "upgrade"
419
+ ];
420
+ function withoutHopByHop(source) {
421
+ let headers = new Headers(source), connectionHeaders = headers.get("connection")?.split(",").map((name) => name.trim()).filter(Boolean) ?? [];
422
+ for (let name of [...HOP_BY_HOP, ...connectionHeaders])
423
+ headers.delete(name);
424
+ return headers;
425
+ }
426
+ function wantsDocument(request) {
427
+ return request.method === "GET" && (request.headers.get("sec-fetch-mode") === "navigate" || request.headers.get("accept")?.toLowerCase().includes("text/html") === !0);
428
+ }
429
+ function userNavigation(request) {
430
+ return request.headers.get("sec-fetch-mode") === "navigate" && request.headers.get("sec-fetch-user") === "?1";
431
+ }
432
+
433
+ // ../proxy/src/core.ts
434
+ import { Clock as Clock2, Context, Duration, Effect as Effect2, Schedule, Schema } from "effect";
435
+
436
+ class BoxNotShared extends Schema.TaggedError()("BoxNotShared", { boxId: Schema.String }) {
437
+ }
438
+
439
+ class DirectoryUnavailable extends Schema.TaggedError()("DirectoryUnavailable", { reason: Schema.String }) {
440
+ }
441
+
442
+ class BoxAsleep extends Schema.TaggedError()("BoxAsleep", { boxId: Schema.String }) {
443
+ }
444
+
445
+ class WakeFailed extends Schema.TaggedError()("WakeFailed", { boxId: Schema.String, reason: Schema.String }) {
446
+ }
447
+
448
+ class UpstreamRefused extends Schema.TaggedError()("UpstreamRefused", { boxId: Schema.String, port: Schema.Number }) {
449
+ }
450
+
451
+ class Resolver extends Context.Service()("@tesser/proxy/Resolver") {
452
+ }
453
+
454
+ class Waker extends Context.Service()("@tesser/proxy/Waker") {
455
+ }
456
+ var WAKING_WINDOW = Duration.minutes(5), startingRetry = { schedule: Schedule.spaced(Duration.seconds(1)), times: 60 }, respond = (response) => ({ _tag: "response", response });
457
+ function viewerResponse(request, state, owner = !1) {
458
+ let status = viewerStatus(state);
459
+ if (wantsDocument(request))
460
+ return new Response(viewerPage(state, owner), { status, headers: STATUS_HEADERS });
461
+ return new Response(`${viewerText(state)}
462
+ `, { status, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", "x-tesser-status": "1" } });
463
+ }
464
+ var page = (request, state) => Effect2.map(Resolver, ({ owner }) => respond(viewerResponse(request, state, owner)));
465
+ function viewerBox(box) {
466
+ return { boxId: box.boxId, name: box.name, sharedBy: box.sharedBy };
467
+ }
468
+ var wakingPage = Effect2.fn("Proxy.wakingPage")(function* (request, box, port, stage, since) {
469
+ let now = yield* Clock2.currentTimeMillis;
470
+ return yield* page(request, { kind: "waking", box: viewerBox(box), stage, port, seconds: Math.max(0, Math.round((now - since) / 1000)) });
471
+ }), refused = Effect2.fn("Proxy.refused")(function* (request, box, port) {
472
+ let status = yield* (yield* Waker).status(box.boxId), now = yield* Clock2.currentTimeMillis;
473
+ if (box.app === "exited")
474
+ return yield* page(request, { kind: "stopped", box: viewerBox(box) });
475
+ if (status !== null && now - status.since < Duration.toMillis(WAKING_WINDOW))
476
+ return yield* wakingPage(request, box, port, box.app === "running" ? "port" : "app", status.since);
477
+ if (box.app === "none")
478
+ return yield* page(request, { kind: "idle", box: viewerBox(box) });
479
+ return yield* page(request, { kind: "not-answering", box: viewerBox(box), port });
480
+ }), serve = Effect2.fn("Proxy.serve")(function* (request, route, box) {
481
+ let { port, path, headers } = route;
482
+ if (request.headers.get("upgrade")?.toLowerCase() === "websocket")
483
+ return { _tag: "websocket", box, port, path, headers };
484
+ let replayable = request.method === "GET" || request.method === "HEAD", document = wantsDocument(request), attempt = Effect2.suspend(() => route.fetch(box, new Request(`http://${headers.get("host")}${path}`, {
485
+ method: request.method,
486
+ headers,
487
+ body: replayable ? void 0 : request.body,
488
+ redirect: "manual",
489
+ signal: request.signal,
490
+ ...replayable ? {} : { duplex: "half" }
491
+ })));
492
+ return yield* (document || !replayable ? attempt : attempt.pipe(Effect2.retry(startingRetry))).pipe(Effect2.flatMap((response) => Effect2.map(route.present(box, response), respond)), Effect2.catchTag("UpstreamRefused", () => document ? refused(request, box, port) : page(request, { kind: "not-answering", box: viewerBox(box), port })));
493
+ }), handle = Effect2.fn("Proxy.handle")(function* (request) {
494
+ let resolved = yield* (yield* Resolver).resolve(request);
495
+ if (resolved instanceof Response)
496
+ return respond(resolved);
497
+ let { box, port } = resolved, waker = yield* Waker;
498
+ if (box.power !== "awake") {
499
+ if (box.power === "asleep" && userNavigation(request))
500
+ yield* waker.wake(box.boxId);
501
+ let status = yield* waker.status(box.boxId);
502
+ if (box.power === "asleep" && status?.failure)
503
+ return yield* page(request, { kind: "wake-failed", box: viewerBox(box), reason: status.failure });
504
+ if (box.power === "asleep" && status === null)
505
+ return yield* page(request, { kind: "asleep", box: viewerBox(box) });
506
+ if (wantsDocument(request)) {
507
+ if (box.power === "launching" && status === null)
508
+ return yield* page(request, { kind: "booting", box: viewerBox(box) });
509
+ return yield* wakingPage(request, box, port, "machine", status?.since ?? (yield* Clock2.currentTimeMillis));
510
+ }
511
+ return yield* serve(request, resolved, yield* waker.awake(box.boxId));
512
+ }
513
+ return yield* serve(request, resolved, box);
514
+ }, (effect, request) => effect.pipe(Effect2.catchTags({
515
+ BoxAsleep: (e) => page(request, { kind: "asleep", box: { boxId: e.boxId, name: e.boxId, sharedBy: null } }),
516
+ BoxNotShared: () => page(request, { kind: "not-shared" }),
517
+ DirectoryUnavailable: () => page(request, { kind: "unavailable" }),
518
+ WakeFailed: (e) => page(request, { kind: "wake-failed", box: { boxId: e.boxId, name: e.boxId, sharedBy: null }, reason: e.reason })
519
+ })));
386
520
 
387
521
  // ../front/src/csp.ts
388
522
  var SCRIPT_DIRECTIVES = ["connect-src", "script-src"];
@@ -420,28 +554,6 @@ function scriptNonce(policy) {
420
554
  return;
421
555
  }
422
556
 
423
- // ../front/src/http.ts
424
- var HOP_BY_HOP = [
425
- "connection",
426
- "keep-alive",
427
- "proxy-authenticate",
428
- "proxy-authorization",
429
- "proxy-connection",
430
- "te",
431
- "trailer",
432
- "transfer-encoding",
433
- "upgrade"
434
- ];
435
- function withoutHopByHop(source) {
436
- let headers = new Headers(source), connectionHeaders = headers.get("connection")?.split(",").map((name) => name.trim()).filter(Boolean) ?? [];
437
- for (let name of [...HOP_BY_HOP, ...connectionHeaders])
438
- headers.delete(name);
439
- return headers;
440
- }
441
- function wantsDocument(request) {
442
- return request.method === "GET" && (request.headers.get("sec-fetch-mode") === "navigate" || request.headers.get("accept")?.toLowerCase().includes("text/html") === !0);
443
- }
444
-
445
557
  // ../front/src/shim.ts
446
558
  function rewriteUrl(input, ws, ctx) {
447
559
  let url;
@@ -578,10 +690,10 @@ function shimTag(params, nonce) {
578
690
  return `<script${nonce ? ` nonce="${nonce}"` : ""} src="/__tesser/shim.js?${query.toString().replace(/&/g, "&amp;")}"></script>`;
579
691
  }
580
692
 
581
- // src/core.ts
582
- import { Clock as Clock2, Context, Duration, Effect as Effect2, Schedule, Schema } from "effect";
693
+ // ../proxy/src/share.ts
694
+ import { Context as Context2, Effect as Effect3, Layer } from "effect";
583
695
 
584
- // src/route.ts
696
+ // ../proxy/src/route.ts
585
697
  var PORT_PATH = /^\/__tesser\/(?:([a-z0-9-]+(?:\.[a-z0-9-]+)*)\.)?(\d{1,5})(\/.*)?$/;
586
698
  function parseTarget(gatewayHost, url) {
587
699
  let bare = url.hostname.toLowerCase().replace(/\.$/, ""), suffix = `.${gatewayHost.toLowerCase()}`;
@@ -609,38 +721,13 @@ function portOrigin(protocol, target, port, first) {
609
721
  return `${protocol}//${target.sub === "" ? "" : `${target.sub}.`}${gatewayLabel(target.boxId, port === first ? null : port)}.${target.gateway}`;
610
722
  }
611
723
 
612
- // src/core.ts
613
- class BoxNotShared extends Schema.TaggedError()("BoxNotShared", { boxId: Schema.String }) {
724
+ // ../proxy/src/share.ts
725
+ class BoxDirectory extends Context2.Service()("@tesser/proxy/BoxDirectory") {
614
726
  }
615
727
 
616
- class DirectoryUnavailable extends Schema.TaggedError()("DirectoryUnavailable", { reason: Schema.String }) {
617
- }
618
-
619
- class WakeFailed extends Schema.TaggedError()("WakeFailed", { boxId: Schema.String, reason: Schema.String }) {
620
- }
621
-
622
- class UpstreamRefused extends Schema.TaggedError()("UpstreamRefused", { boxId: Schema.String, port: Schema.Number }) {
623
- }
624
-
625
- class BoxDirectory extends Context.Service()("@tesser/gateway/BoxDirectory") {
626
- }
627
-
628
- class Waker extends Context.Service()("@tesser/gateway/Waker") {
629
- }
630
-
631
- class Upstream extends Context.Service()("@tesser/gateway/Upstream") {
632
- }
633
- var VIEWER_COOKIE = "__Host-tesser-viewer", GATEWAY_COOKIES = /* @__PURE__ */ new Set(["CF_Authorization", VIEWER_COOKIE]), WAKING_WINDOW = Duration.minutes(5), startingRetry = { schedule: Schedule.spaced(Duration.seconds(1)), times: 60 }, respond = (response) => ({ _tag: "response", response });
634
- function page(request, state) {
635
- let status = viewerStatus(state);
636
- if (wantsDocument(request))
637
- return respond(new Response(viewerPage(state), { status, headers: STATUS_HEADERS }));
638
- return respond(new Response(`${viewerText(state)}
639
- `, { status, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", "x-tesser-status": "1" } }));
640
- }
641
- function viewerBox(box) {
642
- return { name: box.name, sharedBy: box.sharedBy };
728
+ class Upstream extends Context2.Service()("@tesser/proxy/Upstream") {
643
729
  }
730
+ var VIEWER_COOKIE = "__Host-tesser-viewer", GATEWAY_COOKIES = /* @__PURE__ */ new Set(["CF_Authorization", VIEWER_COOKIE]);
644
731
  function upstreamHeaders(request, target, port, root) {
645
732
  let headers = withoutHopByHop(request.headers);
646
733
  for (let name of [...headers.keys()])
@@ -696,65 +783,35 @@ function downstream(request, response, upstreamUrl, ctx, shownOrigin) {
696
783
  });
697
784
  return new HTMLRewriter().on("head", inject).on("body", inject).on("a[href], area[href], link[href]", link("href")).on("form[action]", link("action")).on("button[formaction], input[formaction]", link("formaction")).on("script[src], img[src], iframe[src], source[src], video[src], audio[src], embed[src]", link("src")).on("img[srcset], source[srcset]", link("srcset", (value) => rewriteSrcset(value, (url) => rewriteUrl(url, !1, ctx)))).transform(new Response(response.body, init));
698
785
  }
699
- var wakingPage = Effect2.fn("Gateway.wakingPage")(function* (request, box, port, stage, since) {
700
- let now = yield* Clock2.currentTimeMillis;
701
- return page(request, { kind: "waking", box: viewerBox(box), stage, port, seconds: Math.max(0, Math.round((now - since) / 1000)) });
702
- }), refused = Effect2.fn("Gateway.refused")(function* (request, box, port) {
703
- let status = yield* (yield* Waker).status(box.boxId), now = yield* Clock2.currentTimeMillis;
704
- if (box.app === "exited")
705
- return page(request, { kind: "stopped", box: viewerBox(box) });
706
- if (status !== null && now - status.since < Duration.toMillis(WAKING_WINDOW))
707
- return yield* wakingPage(request, box, port, box.app === "running" ? "port" : "app", status.since);
708
- return page(request, { kind: "not-answering", box: viewerBox(box), port });
709
- }), serve = Effect2.fn("Gateway.serve")(function* (request, target, box, port, root) {
710
- let headers = upstreamHeaders(request, target, port, root);
711
- if (request.headers.get("upgrade")?.toLowerCase() === "websocket")
712
- return { _tag: "websocket", box, port, path: target.path, headers };
713
- let ctx = { href: request.url, host: target.boxHost, ports: box.ports, port: root }, upstreamUrl = `http://${headers.get("host")}${target.path}`, shownOrigin = headers.get("origin"), replayable = request.method === "GET" || request.method === "HEAD", document = wantsDocument(request), upstream = yield* Upstream, attempt = Effect2.suspend(() => upstream.fetch(box, port, new Request(upstreamUrl, {
714
- method: request.method,
715
- headers,
716
- body: replayable ? void 0 : request.body,
717
- redirect: "manual",
718
- signal: request.signal,
719
- ...replayable ? {} : { duplex: "half" }
720
- })));
721
- return yield* (document || !replayable ? attempt : attempt.pipe(Effect2.retry(startingRetry))).pipe(Effect2.map((response) => respond(downstream(request, response, upstreamUrl, ctx, shownOrigin))), Effect2.catchTag("UpstreamRefused", () => document ? refused(request, box, port) : Effect2.succeed(page(request, { kind: "not-answering", box: viewerBox(box), port }))));
722
- }), handle = Effect2.fn("Gateway.handle")(function* (request) {
723
- let directory = yield* BoxDirectory, waker = yield* Waker, url = new URL(request.url), host = yield* directory.host, target = parseTarget(host, url);
724
- if (target === null)
725
- return page(request, { kind: "not-shared" });
726
- if (target.port === null && url.pathname === "/__tesser/shim.js")
727
- return respond(new Response(shimSource(), { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "public, max-age=300" } }));
728
- let box = yield* directory.lookup(target.boxId), first = box.ports[0];
729
- if (first === void 0)
730
- return page(request, { kind: "no-port", box: viewerBox(box), port: target.port ?? target.root ?? 0, ports: [] });
731
- let root = target.root ?? first, port = target.port ?? root;
732
- if (!box.ports.includes(root) || !box.ports.includes(port)) {
733
- let ports = box.ports.map((p) => ({ port: p, href: `${portOrigin(url.protocol, target, p, first)}/` }));
734
- return page(request, { kind: "no-port", box: viewerBox(box), port: box.ports.includes(root) ? port : root, ports });
735
- }
736
- if (target.port !== null && request.headers.get("sec-fetch-mode") === "navigate")
737
- return respond(new Response(null, { status: 307, headers: { location: `${portOrigin(url.protocol, target, port, first)}${target.path}`, "cache-control": "no-store" } }));
738
- if (box.power !== "awake") {
739
- let navigate = request.headers.get("sec-fetch-mode") === "navigate";
740
- if (box.power === "asleep")
741
- yield* waker.wake(box.boxId, navigate);
742
- let status = yield* waker.status(box.boxId);
743
- if (status?.failure && box.power === "asleep")
744
- return page(request, { kind: "wake-failed", box: viewerBox(box), reason: status.failure });
745
- if (wantsDocument(request)) {
746
- if (box.power === "launching" && status === null)
747
- return page(request, { kind: "booting", box: viewerBox(box) });
748
- return yield* wakingPage(request, box, port, "machine", status?.since ?? (yield* Clock2.currentTimeMillis));
786
+ var shareResolver = Layer.effect(Resolver, Effect3.gen(function* () {
787
+ let directory = yield* BoxDirectory, upstream = yield* Upstream;
788
+ return { owner: !1, resolve: Effect3.fn("Share.resolve")(function* (request) {
789
+ let url = new URL(request.url), target = parseTarget(yield* directory.host, url);
790
+ if (target === null)
791
+ return viewerResponse(request, { kind: "not-shared" });
792
+ if (target.port === null && url.pathname === "/__tesser/shim.js")
793
+ return new Response(shimSource(), { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "public, max-age=300" } });
794
+ let box = yield* directory.lookup(target.boxId), first = box.ports[0];
795
+ if (first === void 0)
796
+ return viewerResponse(request, { kind: "no-port", box: viewerBox(box), port: target.port ?? target.root ?? 0, ports: [] });
797
+ let root = target.root ?? first, port = target.port ?? root;
798
+ if (!box.ports.includes(root) || !box.ports.includes(port)) {
799
+ let ports = box.ports.map((p) => ({ port: p, href: `${portOrigin(url.protocol, target, p, first)}/` }));
800
+ return viewerResponse(request, { kind: "no-port", box: viewerBox(box), port: box.ports.includes(root) ? port : root, ports });
749
801
  }
750
- return yield* serve(request, target, yield* waker.awake(box.boxId), port, root);
751
- }
752
- return yield* serve(request, target, box, port, root);
753
- }, (effect, request) => effect.pipe(Effect2.catchTags({
754
- BoxNotShared: () => Effect2.succeed(page(request, { kind: "not-shared" })),
755
- DirectoryUnavailable: () => Effect2.succeed(page(request, { kind: "unavailable" })),
756
- WakeFailed: (e) => Effect2.succeed(page(request, { kind: "wake-failed", box: { name: e.boxId, sharedBy: null }, reason: e.reason }))
757
- })));
802
+ if (target.port !== null && request.headers.get("sec-fetch-mode") === "navigate")
803
+ return new Response(null, { status: 307, headers: { location: `${portOrigin(url.protocol, target, port, first)}${target.path}`, "cache-control": "no-store" } });
804
+ let headers = upstreamHeaders(request, target, port, root), upstreamUrl = `http://${headers.get("host")}${target.path}`, shownOrigin = headers.get("origin");
805
+ return {
806
+ box,
807
+ port,
808
+ path: target.path,
809
+ headers,
810
+ fetch: (served, upstreamRequest) => upstream.fetch(served, port, upstreamRequest),
811
+ present: (served, response) => Effect3.sync(() => downstream(request, response, upstreamUrl, { href: request.url, host: target.boxHost, ports: served.ports, port: root }, shownOrigin))
812
+ };
813
+ }) };
814
+ }));
758
815
 
759
816
  // src/tunnel.ts
760
817
  var UPGRADE_TIMEOUT_MS = 30000, NULL_BODY = /* @__PURE__ */ new Set([101, 204, 205, 304]);
@@ -987,7 +1044,7 @@ class BoxRelay extends DurableObject {
987
1044
  return plain(403, "this box answers for another org here");
988
1045
  return this.acceptBox(decoded.value);
989
1046
  }
990
- let handled = await Effect3.runPromise(handle(request).pipe(Effect3.provide(this.layer(this.viewer(request)))));
1047
+ let handled = await Effect4.runPromise(handle(request).pipe(Effect4.provide(this.layer(this.viewer(request)))));
991
1048
  if (handled._tag === "response")
992
1049
  return handled.response;
993
1050
  return this.bridge(request, handled);
@@ -1142,10 +1199,10 @@ class BoxRelay extends DurableObject {
1142
1199
  this.webSocketClose(ws, 1006, "socket error");
1143
1200
  }
1144
1201
  ask(call) {
1145
- return Effect3.tryPromise({ try: call, catch: (err) => new DirectoryUnavailable({ reason: err instanceof Error ? err.message : String(err) }) });
1202
+ return Effect4.tryPromise({ try: call, catch: (err) => new DirectoryUnavailable({ reason: err instanceof Error ? err.message : String(err) }) });
1146
1203
  }
1147
1204
  layer(viewer) {
1148
- let self = this, remote = Effect3.fn("Relay.remote")(function* (boxId) {
1205
+ let self = this, remote = Effect4.fn("Relay.remote")(function* (boxId) {
1149
1206
  let { status, body } = yield* self.ask(() => self.lookupBox(boxId, viewer));
1150
1207
  if (status === 403 || status === 404)
1151
1208
  return yield* new BoxNotShared({ boxId });
@@ -1153,33 +1210,19 @@ class BoxRelay extends DurableObject {
1153
1210
  let message = Schema2.decodeUnknownOption(ErrorBody)(body);
1154
1211
  return yield* new DirectoryUnavailable({ reason: message._tag === "Some" ? message.value.error.message : `tesser answered ${status}` });
1155
1212
  }
1156
- return (yield* Schema2.decodeUnknownEffect(BoxResponse)(body).pipe(Effect3.mapError(() => new DirectoryUnavailable({ reason: "tesser answered with a box this gateway can't read" })))).box;
1157
- }), lookup = Effect3.fn("Relay.lookup")(function* (boxId) {
1158
- let ticket = self.live();
1159
- if (ticket === null) {
1160
- let box = yield* remote(boxId);
1161
- return { ...box, power: box.power === "awake" ? "launching" : box.power };
1162
- }
1163
- let now = yield* Clock3.currentTimeMillis;
1164
- if (viewer !== null && now - (self.verifiedAt.get(viewer) ?? 0) > RECHECK_MS) {
1165
- let answer = yield* self.ask(() => self.lookupBox(boxId, viewer)).pipe(Effect3.orElseSucceed(() => null)), verdict = self.verdict(viewer, answer, now, !1);
1166
- if (verdict === "revoked" || verdict === "refused")
1167
- return yield* new BoxNotShared({ boxId });
1168
- if (verdict === "stale")
1169
- return yield* new DirectoryUnavailable({ reason: "tesser didn't answer whether you can still open this box" });
1170
- }
1171
- return { boxId, name: ticket.name, sharedBy: ticket.sharedBy, privateIp: "", ports: ticket.ports, power: "awake", app: "running" };
1172
- }), wake = Effect3.fn("Relay.wake")(function* (boxId, retry) {
1213
+ return (yield* Schema2.decodeUnknownEffect(BoxResponse)(body).pipe(Effect4.mapError(() => new DirectoryUnavailable({ reason: "tesser answered with a box this gateway can't read" })))).box;
1214
+ }), kick = Effect4.fn("Relay.kick")(function* (boxId, asked) {
1173
1215
  let now = yield* Clock3.currentTimeMillis, current = self.waking;
1174
1216
  if (current?.inflight)
1175
1217
  return;
1176
- if (current?.failure && !retry)
1177
- return;
1178
1218
  if (current !== null && current.failure === null && now - current.kickedAt < WAKE_RETRY_MS)
1179
1219
  return;
1180
- let waking = { since: current === null || current.failure !== null || now - current.since > Duration2.toMillis(WAKE_TIMEOUT) ? now : current.since, kickedAt: now, failure: null, inflight: !0 };
1220
+ let fresh = current === null || current.failure !== null || now - current.since > Duration2.toMillis(WAKE_TIMEOUT);
1221
+ if (fresh && !asked)
1222
+ return;
1223
+ let waking = { since: fresh ? now : current.since, kickedAt: now, failure: null, inflight: !0 };
1181
1224
  self.waking = waking;
1182
- let kicked = yield* self.ask(() => self.wakeBox(boxId, viewer)).pipe(Effect3.result, Effect3.ensuring(Effect3.sync(() => waking.inflight = !1)));
1225
+ let kicked = yield* self.ask(() => self.wakeBox(boxId, viewer)).pipe(Effect4.result, Effect4.ensuring(Effect4.sync(() => waking.inflight = !1)));
1183
1226
  if (kicked._tag === "Failure")
1184
1227
  return;
1185
1228
  let { status, body } = kicked.success;
@@ -1187,30 +1230,50 @@ class BoxRelay extends DurableObject {
1187
1230
  return;
1188
1231
  let message = Schema2.decodeUnknownOption(ErrorBody)(body);
1189
1232
  waking.failure = message._tag === "Some" ? message.value.error.message : `tesser answered ${status}`;
1190
- }), status = Effect3.fn("Relay.status")(function* (_boxId) {
1233
+ }), wake = (boxId) => kick(boxId, !0), lookup = Effect4.fn("Relay.lookup")(function* (boxId) {
1234
+ let ticket = self.live();
1235
+ if (ticket === null) {
1236
+ let box = yield* remote(boxId);
1237
+ if (box.power === "asleep")
1238
+ yield* kick(boxId, !1);
1239
+ return { ...box, power: box.power === "awake" ? "launching" : box.power };
1240
+ }
1241
+ let now = yield* Clock3.currentTimeMillis;
1242
+ if (viewer !== null && now - (self.verifiedAt.get(viewer) ?? 0) > RECHECK_MS) {
1243
+ let answer = yield* self.ask(() => self.lookupBox(boxId, viewer)).pipe(Effect4.orElseSucceed(() => null)), verdict = self.verdict(viewer, answer, now, !1);
1244
+ if (verdict === "revoked" || verdict === "refused")
1245
+ return yield* new BoxNotShared({ boxId });
1246
+ if (verdict === "stale")
1247
+ return yield* new DirectoryUnavailable({ reason: "tesser didn't answer whether you can still open this box" });
1248
+ }
1249
+ return { boxId, name: ticket.name, sharedBy: ticket.sharedBy, privateIp: "", ports: ticket.ports, power: "awake", app: "running" };
1250
+ }), status = Effect4.fn("Relay.status")(function* (_boxId) {
1191
1251
  let now = yield* Clock3.currentTimeMillis, w = self.waking;
1192
1252
  if (w === null || w.failure === null && now - w.since > Duration2.toMillis(WAKE_TIMEOUT))
1193
1253
  return null;
1194
1254
  return { since: w.since, failure: w.failure };
1195
- }), dialed = (boxId, within, reason) => Effect3.promise(() => new Promise((resolve) => self.connected.push(resolve))).pipe(Effect3.timeoutOrElse({ duration: within, orElse: () => Effect3.fail(new WakeFailed({ boxId, reason })) })), awake = Effect3.fn("Relay.awake")(function* (boxId) {
1196
- if (self.live() === null)
1197
- if ((yield* remote(boxId)).power === "awake")
1255
+ }), dialed = (boxId, within, reason) => Effect4.promise(() => new Promise((resolve) => self.connected.push(resolve))).pipe(Effect4.timeoutOrElse({ duration: within, orElse: () => Effect4.fail(new WakeFailed({ boxId, reason })) })), awake = Effect4.fn("Relay.awake")(function* (boxId) {
1256
+ if (self.live() === null) {
1257
+ let box = yield* remote(boxId);
1258
+ if (box.power === "awake")
1198
1259
  yield* dialed(boxId, DIAL_GRACE, "the box is up but hasn't connected to this gateway; its boxd may need an update");
1199
1260
  else {
1200
- yield* wake(boxId, !1);
1201
- let failure = self.waking?.failure;
1202
- if (failure)
1203
- return yield* new WakeFailed({ boxId, reason: failure });
1261
+ let waking = yield* status(boxId);
1262
+ if (waking?.failure)
1263
+ return yield* new WakeFailed({ boxId, reason: waking.failure });
1264
+ if (box.power === "asleep" && waking === null)
1265
+ return yield* new BoxAsleep({ boxId });
1204
1266
  yield* dialed(boxId, WAKE_TIMEOUT, "it didn't wake up within 5 minutes");
1205
1267
  }
1268
+ }
1206
1269
  return yield* lookup(boxId);
1207
- }), upstream = (box, port, request) => Effect3.tryPromise({ try: () => self.tunnel.fetch(port, request), catch: () => new UpstreamRefused({ boxId: box.boxId, port }) });
1208
- return Layer.mergeAll(Layer.succeed(BoxDirectory)({ host: Effect3.succeed(self.host()), lookup }), Layer.succeed(Waker)({ wake, status, awake }), Layer.succeed(Upstream)({ fetch: upstream }));
1270
+ }), upstream = (box, port, request) => Effect4.tryPromise({ try: () => self.tunnel.fetch(port, request), catch: () => new UpstreamRefused({ boxId: box.boxId, port }) });
1271
+ return Layer2.merge(Layer2.provide(shareResolver, Layer2.merge(Layer2.succeed(BoxDirectory)({ host: Effect4.succeed(self.host()), lookup }), Layer2.succeed(Upstream)({ fetch: upstream }))), Layer2.succeed(Waker)({ wake, status, awake }));
1209
1272
  }
1210
1273
  }
1211
1274
 
1212
1275
  // src/cloudflare.ts
1213
- var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect4.fn("Gateway.verifyTicket")(function* (env, request) {
1276
+ var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect5.fn("Gateway.verifyTicket")(function* (env, request) {
1214
1277
  let token = request.headers.get("authorization")?.match(/^Bearer (\S+)$/)?.[1];
1215
1278
  if (token === void 0)
1216
1279
  return null;
@@ -1219,8 +1282,8 @@ var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect4.fn("Gatewa
1219
1282
  return null;
1220
1283
  if (claims.sub !== `box:${env.TESSER_ORG}:${String(claims.box)}`)
1221
1284
  return null;
1222
- return yield* Schema3.decodeUnknownEffect(Ticket)(claims).pipe(Effect4.option);
1223
- }), verifyViewer = Effect4.fn("Gateway.verifyViewer")(function* (env, request) {
1285
+ return yield* Schema3.decodeUnknownEffect(Ticket)(claims).pipe(Effect5.option);
1286
+ }), verifyViewer = Effect5.fn("Gateway.verifyViewer")(function* (env, request) {
1224
1287
  let token = request.headers.get(ACCESS_HEADER);
1225
1288
  if (token === null)
1226
1289
  return null;
@@ -1232,7 +1295,7 @@ var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect4.fn("Gatewa
1232
1295
  if (apex && url.pathname === GATEWAY_DIAL_PATH) {
1233
1296
  if (request.headers.get("upgrade")?.toLowerCase() !== "websocket")
1234
1297
  return plain(426, "boxes dial this with a websocket");
1235
- let ticket = await Effect4.runPromise(verifyTicket(env, request));
1298
+ let ticket = await Effect5.runPromise(verifyTicket(env, request));
1236
1299
  if (ticket === null || ticket._tag === "None")
1237
1300
  return plain(401, "a box dials in with a share ticket signed by tesser");
1238
1301
  let headers = new Headers(request.headers);
@@ -1243,7 +1306,7 @@ var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect4.fn("Gatewa
1243
1306
  let boxId = gatewayHostBox(env.GATEWAY_HOST, url.hostname);
1244
1307
  if (boxId === null)
1245
1308
  return new Response(viewerPage({ kind: "not-shared" }), { status: 404, headers: STATUS_HEADERS });
1246
- if (await Effect4.runPromise(verifyViewer(env, request)) === null)
1309
+ if (await Effect5.runPromise(verifyViewer(env, request)) === null)
1247
1310
  return plain(403, "this gateway only answers requests Cloudflare Access let through");
1248
1311
  let headers = new Headers(request.headers);
1249
1312
  return headers.delete(TICKET_HEADER), env.BOXES.get(env.BOXES.idFromName(boxId)).fetch(new Request(request, { headers }));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tesser-sh/gateway",
3
- "version": "0.53.2",
3
+ "version": "0.55.0",
4
4
  "description": "Tesser's share gateway as a Cloudflare Worker and Durable Object, deployed in your own account",
5
5
  "license": "MIT",
6
6
  "type": "module",