@tesser-sh/gateway 0.0.1 → 0.54.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 +579 -373
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1,6 +1,3 @@
1
- // src/cloudflare.ts
2
- import { DurableObject } from "cloudflare:workers";
3
-
4
1
  // ../front/src/page.ts
5
2
  var FAVICON = `data:image/svg+xml,${encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><rect width="24" height="24" rx="4" fill="#000000"/><g transform="translate(2.4 2.4) scale(0.8)"><path fill="#f0f1e9" d="M11.5 11.5H2A9.5 9.5 0 0 1 11.5 2ZM12.5 12.5H22A9.5 9.5 0 0 1 12.5 22Z"/><path fill="#fbffc9" d="M12.5 2H22V11.5H12.5Z"/></g></svg>')}`;
6
3
  function escapeHtml(text) {
@@ -46,10 +43,8 @@ li .name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
46
43
  .ports a { color: inherit; text-decoration: none; }
47
44
  .ports a:hover { color: var(--bone); text-decoration: underline; text-decoration-color: var(--remote); text-underline-offset: 3px; }
48
45
  tesser-use { color: var(--faint); }
49
- tesser-wake { display: block; margin-top: 24px; }
50
46
  .bad { margin-top: 24px; color: var(--bad); font-size: 13px; }
51
47
  .open { margin-top: 24px; padding: 9px 16px; background: var(--remote); color: #171717; border: 0; font: 500 14px var(--sans); cursor: pointer; }
52
- .open:disabled { background: var(--rule); color: var(--dim); cursor: default; }
53
48
  a.open { display: inline-block; text-decoration: none; }
54
49
  .sub { margin-top: 8px; color: var(--dim); }
55
50
  .steps { list-style: none; padding: 0; margin-top: 24px; }
@@ -101,9 +96,11 @@ document.addEventListener("click", (event) => {
101
96
  // ../front/src/viewer.ts
102
97
  var STATUS = {
103
98
  waking: 503,
99
+ asleep: 503,
104
100
  booting: 503,
105
101
  "not-answering": 504,
106
102
  stopped: 502,
103
+ idle: 502,
107
104
  "wake-failed": 503,
108
105
  "no-port": 404,
109
106
  "not-shared": 404,
@@ -116,12 +113,16 @@ function viewerText(state) {
116
113
  switch (state.kind) {
117
114
  case "waking":
118
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`;
119
118
  case "booting":
120
119
  return `${state.box.name} is booting; retry shortly`;
121
120
  case "not-answering":
122
121
  return `${state.box.name} isn't answering on port ${state.port}`;
123
122
  case "stopped":
124
123
  return `${state.box.name} has stopped`;
124
+ case "idle":
125
+ return `nothing is running on ${state.box.name}`;
125
126
  case "wake-failed":
126
127
  return `${state.box.name} didn't wake up: ${state.reason}`;
127
128
  case "no-port":
@@ -146,7 +147,10 @@ function steps(stage, port, seconds) {
146
147
  function who(box) {
147
148
  return box.sharedBy === null ? "" : `<p class="who">Shared by ${escapeHtml(box.sharedBy)}</p>`;
148
149
  }
149
- 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) {
150
154
  switch (state.kind) {
151
155
  case "waking":
152
156
  return {
@@ -155,6 +159,13 @@ function body(state) {
155
159
  retry: !0,
156
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>`
157
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
+ };
158
169
  case "booting":
159
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>` };
160
171
  case "not-answering":
@@ -162,21 +173,28 @@ function body(state) {
162
173
  headline: `${state.box.name} isn't answering on port ${state.port}`,
163
174
  spinner: !0,
164
175
  retry: !0,
165
- 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`) : ""}`
166
177
  };
167
178
  case "stopped":
168
179
  return {
169
180
  headline: `${state.box.name} has stopped`,
170
181
  spinner: !1,
171
182
  retry: !0,
172
- 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>`
173
191
  };
174
192
  case "wake-failed":
175
193
  return {
176
194
  headline: `${state.box.name} didn't wake up`,
177
195
  spinner: !1,
178
196
  retry: !1,
179
- 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>`
180
198
  };
181
199
  case "no-port":
182
200
  return {
@@ -191,12 +209,12 @@ function body(state) {
191
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>` };
192
210
  }
193
211
  }
194
- function viewerPage(state) {
195
- 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>
196
214
  <h1>${spinner ? '<span class="sq"></span>' : ""}${escapeHtml(headline)}</h1>
197
215
  ${rest}
198
216
  </main>`;
199
- return shell(`${headline} · tesser`, main, retry ? RETRY : "", !1);
217
+ return shell(`${headline} · tesser`, main, retry ? RETRY : "", owner);
200
218
  }
201
219
 
202
220
  // ../protocol/src/table.ts
@@ -220,6 +238,12 @@ function parseGatewayLabel(label) {
220
238
  return null;
221
239
  return { boxId, port: Number(port) };
222
240
  }
241
+ function gatewayHostBox(gatewayHost, hostname) {
242
+ let suffix = `.${gatewayHost.toLowerCase()}`, host = hostname.toLowerCase().replace(/\.$/, "");
243
+ if (!host.endsWith(suffix))
244
+ return null;
245
+ return parseGatewayLabel(host.slice(0, -suffix.length).split(".").pop())?.boxId ?? null;
246
+ }
223
247
 
224
248
  // ../protocol/src/mux.ts
225
249
  var MUX_WINDOW = 262144, MUX_CHUNK = 65536;
@@ -317,7 +341,182 @@ function decodeFrame(buf) {
317
341
  }
318
342
 
319
343
  // src/cloudflare.ts
320
- import { Clock as Clock3, Duration as Duration2, Effect as Effect3, Layer, Schema as Schema2 } from "effect";
344
+ import { Effect as Effect5, Schema as Schema3 } from "effect";
345
+
346
+ // src/jwt.ts
347
+ import { Clock, Effect, Option } from "effect";
348
+ var KEYS_TTL_MS = 600000, REFETCH_MIN_MS = 30000, CLOCK_SKEW_S = 60, keySets = /* @__PURE__ */ new Map;
349
+ function base64url(text) {
350
+ let b64 = text.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text.length / 4) * 4, "=");
351
+ return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
352
+ }
353
+ function json2(part) {
354
+ try {
355
+ let value = JSON.parse((/* @__PURE__ */ new TextDecoder()).decode(base64url(part)));
356
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
357
+ } catch {
358
+ return null;
359
+ }
360
+ }
361
+ var fetchKeys = (jwksUri, now) => Effect.tryPromise(async () => {
362
+ let res = await fetch(jwksUri);
363
+ if (!res.ok)
364
+ throw Error(`${jwksUri} answered ${res.status}`);
365
+ let body = await res.json(), keys = /* @__PURE__ */ new Map;
366
+ for (let jwk of body.keys ?? []) {
367
+ if (typeof jwk.kid !== "string" || jwk.kty !== "RSA")
368
+ continue;
369
+ keys.set(jwk.kid, await crypto.subtle.importKey("jwk", jwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, !1, ["verify"]));
370
+ }
371
+ let set = { keys, fetchedAt: now };
372
+ return keySets.set(jwksUri, set), set;
373
+ }).pipe(Effect.option), keyFor = Effect.fn("Jwt.keyFor")(function* (jwksUri, kid, now) {
374
+ let set = keySets.get(jwksUri);
375
+ if (set === void 0 || now - set.fetchedAt > KEYS_TTL_MS)
376
+ set = Option.getOrElse(yield* fetchKeys(jwksUri, now), () => set);
377
+ if (set !== void 0 && !set.keys.has(kid) && now - set.fetchedAt > REFETCH_MIN_MS)
378
+ set = Option.getOrElse(yield* fetchKeys(jwksUri, now), () => set);
379
+ return set?.keys.get(kid) ?? null;
380
+ }), verifyRs256 = Effect.fn("Jwt.verifyRs256")(function* (token, issuer, jwksUri) {
381
+ let parts = token.split(".");
382
+ if (parts.length !== 3)
383
+ return null;
384
+ let [h, p, s] = parts, header = json2(h), claims = json2(p);
385
+ if (header === null || claims === null || header.alg !== "RS256" || typeof header.kid !== "string" || claims.iss !== issuer)
386
+ return null;
387
+ let nowMs = yield* Clock.currentTimeMillis, key = yield* keyFor(jwksUri, header.kid, nowMs);
388
+ if (key === null)
389
+ return null;
390
+ let signature = base64url(s);
391
+ if (!(yield* Effect.tryPromise(() => crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, (/* @__PURE__ */ new TextEncoder()).encode(`${h}.${p}`))).pipe(Effect.orElseSucceed(() => !1))))
392
+ return null;
393
+ let now = Math.floor(nowMs / 1000);
394
+ if (typeof claims.exp !== "number" || claims.exp + CLOCK_SKEW_S < now)
395
+ return null;
396
+ if (typeof claims.nbf === "number" && claims.nbf - CLOCK_SKEW_S > now)
397
+ return null;
398
+ return claims;
399
+ });
400
+ function audienceIncludes(aud, expected) {
401
+ return aud === expected || Array.isArray(aud) && aud.includes(expected);
402
+ }
403
+
404
+ // src/relay.ts
405
+ import { DurableObject } from "cloudflare:workers";
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
+ })));
321
520
 
322
521
  // ../front/src/csp.ts
323
522
  var SCRIPT_DIRECTIVES = ["connect-src", "script-src"];
@@ -355,28 +554,6 @@ function scriptNonce(policy) {
355
554
  return;
356
555
  }
357
556
 
358
- // ../front/src/http.ts
359
- var HOP_BY_HOP = [
360
- "connection",
361
- "keep-alive",
362
- "proxy-authenticate",
363
- "proxy-authorization",
364
- "proxy-connection",
365
- "te",
366
- "trailer",
367
- "transfer-encoding",
368
- "upgrade"
369
- ];
370
- function withoutHopByHop(source) {
371
- let headers2 = new Headers(source), connectionHeaders = headers2.get("connection")?.split(",").map((name) => name.trim()).filter(Boolean) ?? [];
372
- for (let name of [...HOP_BY_HOP, ...connectionHeaders])
373
- headers2.delete(name);
374
- return headers2;
375
- }
376
- function wantsDocument(request) {
377
- return request.method === "GET" && (request.headers.get("sec-fetch-mode") === "navigate" || request.headers.get("accept")?.toLowerCase().includes("text/html") === !0);
378
- }
379
-
380
557
  // ../front/src/shim.ts
381
558
  function rewriteUrl(input, ws, ctx) {
382
559
  let url;
@@ -396,7 +573,16 @@ function rewriteUrl(input, ws, ctx) {
396
573
  let port = Number(url.port || (url.protocol === "https:" || url.protocol === "wss:" ? 443 : 80));
397
574
  if (!ctx.ports.includes(port))
398
575
  return input;
399
- return url.pathname = `${port === ctx.port ? "" : `/__tesser/${port}`}${url.pathname}`, url.protocol = ws ? page.protocol === "https:" ? "wss:" : "ws:" : page.protocol, url.port = "", url.host = name === page.hostname ? page.host : sub === null ? ctx.host : `${sub}.${ctx.host}`, url.href;
576
+ return url.pathname = `${sub !== null ? `/__tesser/${sub}.${port}` : port === ctx.port ? "" : `/__tesser/${port}`}${url.pathname}`, url.protocol = ws ? page.protocol === "https:" ? "wss:" : "ws:" : page.protocol, url.port = "", url.host = name === page.hostname || sub !== null ? page.host : ctx.host, url.href;
577
+ }
578
+ function rewriteSrcset(value, to) {
579
+ let changed = !1, candidates = value.split(",").map((candidate) => {
580
+ let [url = "", ...descriptors] = candidate.trim().split(/\s+/), rewritten = to(url);
581
+ if (rewritten !== url)
582
+ changed = !0;
583
+ return [rewritten, ...descriptors].join(" ");
584
+ });
585
+ return changed ? candidates.join(", ") : value;
400
586
  }
401
587
  function shimSource() {
402
588
  return `(() => {
@@ -410,6 +596,7 @@ function shimSource() {
410
596
  const port = Number(params.get("port"));
411
597
  const rewriteUrl = ${rewriteUrl.toString()};
412
598
  const to = (input, ws) => rewriteUrl(String(input), ws, { href: location.href, host, ports, port });
599
+ const rewriteSrcset = ${rewriteSrcset.toString()};
413
600
  const fetch = window.fetch;
414
601
  window.fetch = function (input, init) {
415
602
  if (!(input instanceof Request)) return fetch.call(this, to(input, false), init);
@@ -437,6 +624,51 @@ function shimSource() {
437
624
  };
438
625
  wrap("WebSocket", true);
439
626
  wrap("EventSource", false);
627
+ const loads = [
628
+ ["HTMLImageElement", ["src", "srcset"]],
629
+ ["HTMLSourceElement", ["src", "srcset"]],
630
+ ["HTMLScriptElement", ["src"]],
631
+ ["HTMLIFrameElement", ["src"]],
632
+ ["HTMLMediaElement", ["src"]],
633
+ ["HTMLVideoElement", ["poster"]],
634
+ ["HTMLEmbedElement", ["src"]],
635
+ ["HTMLTrackElement", ["src"]],
636
+ ["HTMLLinkElement", ["href"]],
637
+ ].filter(([name]) => window[name]);
638
+ const fix = (name, value) => name === "srcset" ? rewriteSrcset(String(value), (url) => to(url, false)) : to(value, false);
639
+ const loaded = (element, name) => loads.some(([type, names]) => element instanceof window[type] && names.includes(name));
640
+ const setAttribute = Element.prototype.setAttribute;
641
+ Element.prototype.setAttribute = function (name, value) {
642
+ const lower = String(name).toLowerCase();
643
+ return setAttribute.call(this, name, loaded(this, lower) ? fix(lower, value) : value);
644
+ };
645
+ for (const [type, names] of loads) {
646
+ for (const name of names) {
647
+ const proto = window[type].prototype;
648
+ const descriptor = Object.getOwnPropertyDescriptor(proto, name);
649
+ if (!descriptor?.set) continue;
650
+ Object.defineProperty(proto, name, { ...descriptor, set(value) { descriptor.set.call(this, fix(name, value)); } });
651
+ }
652
+ }
653
+ const selector = "img, source, script, iframe, video, audio, embed, track, link";
654
+ const sweep = (element) => {
655
+ for (const name of ["src", "srcset", "poster", "href"]) {
656
+ const value = element.getAttribute(name);
657
+ if (value !== null && loaded(element, name)) {
658
+ const rewritten = fix(name, value);
659
+ if (rewritten !== value) setAttribute.call(element, name, rewritten);
660
+ }
661
+ }
662
+ };
663
+ new MutationObserver((records) => {
664
+ for (const record of records) {
665
+ for (const node of record.addedNodes) {
666
+ if (!(node instanceof Element)) continue;
667
+ if (node.matches(selector)) sweep(node);
668
+ for (const element of node.querySelectorAll(selector)) sweep(element);
669
+ }
670
+ }
671
+ }).observe(document, { childList: true, subtree: true });
440
672
  const follow = (element, attribute) => {
441
673
  const value = element?.getAttribute(attribute);
442
674
  if (!value) return;
@@ -458,11 +690,11 @@ function shimTag(params, nonce) {
458
690
  return `<script${nonce ? ` nonce="${nonce}"` : ""} src="/__tesser/shim.js?${query.toString().replace(/&/g, "&amp;")}"></script>`;
459
691
  }
460
692
 
461
- // src/core.ts
462
- import { Clock, Context, Duration, Effect, Schedule, Schema } from "effect";
693
+ // ../proxy/src/share.ts
694
+ import { Context as Context2, Effect as Effect3, Layer } from "effect";
463
695
 
464
- // src/route.ts
465
- var PORT_PATH = /^\/__tesser\/(\d{1,5})(\/.*)?$/;
696
+ // ../proxy/src/route.ts
697
+ var PORT_PATH = /^\/__tesser\/(?:([a-z0-9-]+(?:\.[a-z0-9-]+)*)\.)?(\d{1,5})(\/.*)?$/;
466
698
  function parseTarget(gatewayHost, url) {
467
699
  let bare = url.hostname.toLowerCase().replace(/\.$/, ""), suffix = `.${gatewayHost.toLowerCase()}`;
468
700
  if (!bare.endsWith(suffix))
@@ -470,14 +702,14 @@ function parseTarget(gatewayHost, url) {
470
702
  let labels = bare.slice(0, -suffix.length).split("."), label = labels[labels.length - 1], parsed = parseGatewayLabel(label);
471
703
  if (parsed === null)
472
704
  return null;
473
- let gateway = `${gatewayHost}${url.port ? `:${url.port}` : ""}`, base = { boxId: parsed.boxId, sub: labels.slice(0, -1).join("."), boxHost: `${label}.${gateway}`, gateway, root: parsed.port }, match = PORT_PATH.exec(url.pathname);
705
+ let gateway = `${gatewayHost}${url.port ? `:${url.port}` : ""}`, sub = labels.slice(0, -1).join("."), base = { boxId: parsed.boxId, sub, pageSub: sub, boxHost: `${label}.${gateway}`, gateway, root: parsed.port }, match = PORT_PATH.exec(url.pathname);
474
706
  if (match === null)
475
707
  return { ...base, port: null, path: `${url.pathname}${url.search}` };
476
- return { ...base, port: Number(match[1]), path: `${match[2] ?? "/"}${url.search}` };
708
+ return { ...base, sub: match[1] ?? base.sub, port: Number(match[2]), path: `${match[3] ?? "/"}${url.search}` };
477
709
  }
478
- function parseUrl(raw) {
710
+ function parseUrl(raw, base) {
479
711
  try {
480
- return new URL(raw);
712
+ return new URL(raw, base);
481
713
  } catch {
482
714
  return null;
483
715
  }
@@ -489,54 +721,29 @@ function portOrigin(protocol, target, port, first) {
489
721
  return `${protocol}//${target.sub === "" ? "" : `${target.sub}.`}${gatewayLabel(target.boxId, port === first ? null : port)}.${target.gateway}`;
490
722
  }
491
723
 
492
- // src/core.ts
493
- class BoxNotShared extends Schema.TaggedError()("BoxNotShared", { boxId: Schema.String }) {
494
- }
495
-
496
- class DirectoryUnavailable extends Schema.TaggedError()("DirectoryUnavailable", { reason: Schema.String }) {
497
- }
498
-
499
- class WakeFailed extends Schema.TaggedError()("WakeFailed", { boxId: Schema.String, reason: Schema.String }) {
500
- }
501
-
502
- class UpstreamRefused extends Schema.TaggedError()("UpstreamRefused", { boxId: Schema.String, port: Schema.Number }) {
503
- }
504
-
505
- class BoxDirectory extends Context.Service()("@tesser/gateway/BoxDirectory") {
724
+ // ../proxy/src/share.ts
725
+ class BoxDirectory extends Context2.Service()("@tesser/proxy/BoxDirectory") {
506
726
  }
507
727
 
508
- class Waker extends Context.Service()("@tesser/gateway/Waker") {
509
- }
510
-
511
- class Upstream extends Context.Service()("@tesser/gateway/Upstream") {
512
- }
513
- var WAKING_WINDOW = Duration.minutes(5), startingRetry = { schedule: Schedule.spaced(Duration.seconds(1)), times: 60 }, respond = (response) => ({ _tag: "response", response });
514
- function page(request, state) {
515
- let status = viewerStatus(state);
516
- if (wantsDocument(request))
517
- return respond(new Response(viewerPage(state), { status, headers: STATUS_HEADERS }));
518
- return respond(new Response(`${viewerText(state)}
519
- `, { status, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store", "x-tesser-status": "1" } }));
520
- }
521
- function viewerBox(box) {
522
- return { name: box.name, sharedBy: box.sharedBy };
728
+ class Upstream extends Context2.Service()("@tesser/proxy/Upstream") {
523
729
  }
730
+ var VIEWER_COOKIE = "__Host-tesser-viewer", GATEWAY_COOKIES = /* @__PURE__ */ new Set(["CF_Authorization", VIEWER_COOKIE]);
524
731
  function upstreamHeaders(request, target, port, root) {
525
- let headers2 = withoutHopByHop(request.headers);
526
- for (let name of [...headers2.keys()])
732
+ let headers = withoutHopByHop(request.headers);
733
+ for (let name of [...headers.keys()])
527
734
  if (name.startsWith("cf-access-") || name.startsWith("x-forwarded-"))
528
- headers2.delete(name);
529
- headers2.delete("forwarded");
530
- let cookie = headers2.get("cookie")?.split(";").filter((c) => c.trim().split("=")[0] !== "CF_Authorization").join(";").trim();
735
+ headers.delete(name);
736
+ headers.delete("forwarded"), headers.delete("x-tesser-viewer"), headers.delete("x-tesser-ticket");
737
+ let cookie = headers.get("cookie")?.split(";").filter((c) => !GATEWAY_COOKIES.has(c.trim().split("=")[0])).join(";").trim();
531
738
  if (cookie)
532
- headers2.set("cookie", cookie);
739
+ headers.set("cookie", cookie);
533
740
  else
534
- headers2.delete("cookie");
535
- headers2.set("host", presentedHost(target.sub, port)), headers2.set("x-forwarded-for", "127.0.0.1"), headers2.set("x-forwarded-proto", "http"), headers2.set("accept-encoding", "identity");
741
+ headers.delete("cookie");
742
+ headers.set("host", presentedHost(target.sub, port)), headers.set("x-forwarded-for", "127.0.0.1"), headers.set("x-forwarded-proto", "http"), headers.set("accept-encoding", "identity");
536
743
  let origin = request.headers.get("origin");
537
744
  if (origin !== null && parseUrl(origin)?.host === new URL(request.url).host)
538
- headers2.set("origin", `http://${presentedHost(target.sub, root)}`);
539
- return headers2;
745
+ headers.set("origin", `http://${presentedHost(target.pageSub, root)}`);
746
+ return headers;
540
747
  }
541
748
  function isHtmlDocument(request, response) {
542
749
  if (!wantsDocument(request) || response.status < 200 || response.status >= 300)
@@ -544,155 +751,67 @@ function isHtmlDocument(request, response) {
544
751
  return (response.headers.get("content-type")?.toLowerCase() ?? "").includes("text/html") && !response.headers.has("content-range");
545
752
  }
546
753
  function downstream(request, response, upstreamUrl, ctx, shownOrigin) {
547
- let headers2 = withoutHopByHop(response.headers);
548
- headers2.delete("content-encoding"), headers2.delete("content-length");
549
- let location = headers2.get("location");
754
+ let headers = withoutHopByHop(response.headers);
755
+ headers.delete("content-encoding"), headers.delete("content-length");
756
+ let location = headers.get("location");
550
757
  if (location !== null)
551
- headers2.set("location", rewriteUrl(parseUrl(location) === null ? new URL(location, upstreamUrl).href : location, !1, ctx));
758
+ headers.set("location", rewriteUrl(parseUrl(location) === null ? new URL(location, upstreamUrl).href : location, !1, ctx));
552
759
  let origin = request.headers.get("origin");
553
- if (origin !== null && shownOrigin !== null && headers2.get("access-control-allow-origin") === shownOrigin)
554
- headers2.set("access-control-allow-origin", origin);
555
- let init = { status: response.status, statusText: response.statusText, headers: headers2 };
760
+ if (origin !== null && shownOrigin !== null && headers.get("access-control-allow-origin") === shownOrigin)
761
+ headers.set("access-control-allow-origin", origin);
762
+ let init = { status: response.status, statusText: response.statusText, headers };
556
763
  if (!isHtmlDocument(request, response))
557
764
  return new Response(response.body, init);
558
- let csp = headers2.get("content-security-policy");
765
+ let csp = headers.get("content-security-policy");
559
766
  if (csp !== null)
560
- headers2.set("content-security-policy", allowSelfScripts(csp));
767
+ headers.set("content-security-policy", allowSelfScripts(csp));
561
768
  let tag = shimTag({ host: ctx.host, ports: ctx.ports, port: ctx.port }, csp === null ? void 0 : scriptNonce(csp)), placed = !1, inject = {
562
769
  element(element) {
563
770
  if (placed)
564
771
  return;
565
772
  placed = !0, element.prepend(tag, { html: !0 });
566
773
  }
567
- }, link = (attribute) => ({
774
+ }, link = (attribute, rewrite = (value) => rewriteUrl(value, !1, ctx)) => ({
568
775
  element(element) {
569
776
  let value = element.getAttribute(attribute);
570
777
  if (value === null)
571
778
  return;
572
- let rewritten = rewriteUrl(value, !1, ctx);
779
+ let rewritten = rewrite(value);
573
780
  if (rewritten !== value)
574
781
  element.setAttribute(attribute, rewritten);
575
782
  }
576
783
  });
577
- 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")).transform(new Response(response.body, init));
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));
578
785
  }
579
- var wakingPage = Effect.fn("Gateway.wakingPage")(function* (request, box, port, stage, since) {
580
- let now = yield* Clock.currentTimeMillis;
581
- return page(request, { kind: "waking", box: viewerBox(box), stage, port, seconds: Math.max(0, Math.round((now - since) / 1000)) });
582
- }), refused = Effect.fn("Gateway.refused")(function* (request, box, port) {
583
- let status = yield* (yield* Waker).status(box.boxId), now = yield* Clock.currentTimeMillis;
584
- if (box.app === "exited")
585
- return page(request, { kind: "stopped", box: viewerBox(box) });
586
- if (status !== null && now - status.since < Duration.toMillis(WAKING_WINDOW))
587
- return yield* wakingPage(request, box, port, box.app === "running" ? "port" : "app", status.since);
588
- return page(request, { kind: "not-answering", box: viewerBox(box), port });
589
- }), serve = Effect.fn("Gateway.serve")(function* (request, target, box, port, root) {
590
- let headers2 = upstreamHeaders(request, target, port, root);
591
- if (request.headers.get("upgrade")?.toLowerCase() === "websocket")
592
- return { _tag: "websocket", box, port, path: target.path, headers: headers2 };
593
- let ctx = { href: request.url, host: target.boxHost, ports: box.ports, port: root }, upstreamUrl = `http://${headers2.get("host")}${target.path}`, shownOrigin = headers2.get("origin"), replayable = request.method === "GET" || request.method === "HEAD", document = wantsDocument(request), upstream = yield* Upstream, attempt = Effect.suspend(() => upstream.fetch(box, port, new Request(upstreamUrl, {
594
- method: request.method,
595
- headers: headers2,
596
- body: replayable ? void 0 : request.body,
597
- redirect: "manual",
598
- signal: request.signal,
599
- ...replayable ? {} : { duplex: "half" }
600
- })));
601
- return yield* (document || !replayable ? attempt : attempt.pipe(Effect.retry(startingRetry))).pipe(Effect.map((response) => respond(downstream(request, response, upstreamUrl, ctx, shownOrigin))), Effect.catchTag("UpstreamRefused", () => document ? refused(request, box, port) : Effect.succeed(page(request, { kind: "not-answering", box: viewerBox(box), port }))));
602
- }), handle = Effect.fn("Gateway.handle")(function* (request) {
603
- let directory = yield* BoxDirectory, waker = yield* Waker, url = new URL(request.url), host = yield* directory.host, target = parseTarget(host, url);
604
- if (target === null)
605
- return page(request, { kind: "not-shared" });
606
- if (target.port === null && url.pathname === "/__tesser/shim.js")
607
- return respond(new Response(shimSource(), { headers: { "content-type": "text/javascript; charset=utf-8", "cache-control": "public, max-age=300" } }));
608
- let box = yield* directory.lookup(target.boxId), first = box.ports[0];
609
- if (first === void 0)
610
- return page(request, { kind: "no-port", box: viewerBox(box), port: target.port ?? target.root ?? 0, ports: [] });
611
- let root = target.root ?? first, port = target.port ?? root;
612
- if (!box.ports.includes(root) || !box.ports.includes(port)) {
613
- let ports = box.ports.map((p) => ({ port: p, href: `${portOrigin(url.protocol, target, p, first)}/` }));
614
- return page(request, { kind: "no-port", box: viewerBox(box), port: box.ports.includes(root) ? port : root, ports });
615
- }
616
- if (target.port !== null && request.headers.get("sec-fetch-mode") === "navigate")
617
- return respond(new Response(null, { status: 307, headers: { location: `${portOrigin(url.protocol, target, port, first)}${target.path}`, "cache-control": "no-store" } }));
618
- if (box.power !== "awake") {
619
- let navigate = request.headers.get("sec-fetch-mode") === "navigate";
620
- if (box.power === "asleep")
621
- yield* waker.wake(box.boxId, navigate);
622
- let status = yield* waker.status(box.boxId);
623
- if (status?.failure && box.power === "asleep")
624
- return page(request, { kind: "wake-failed", box: viewerBox(box), reason: status.failure });
625
- if (wantsDocument(request)) {
626
- if (box.power === "launching" && status === null)
627
- return page(request, { kind: "booting", box: viewerBox(box) });
628
- return yield* wakingPage(request, box, port, "machine", status?.since ?? (yield* Clock.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 });
629
801
  }
630
- return yield* serve(request, target, yield* waker.awake(box.boxId), port, root);
631
- }
632
- return yield* serve(request, target, box, port, root);
633
- }, (effect, request) => effect.pipe(Effect.catchTags({
634
- BoxNotShared: () => Effect.succeed(page(request, { kind: "not-shared" })),
635
- DirectoryUnavailable: () => Effect.succeed(page(request, { kind: "unavailable" })),
636
- WakeFailed: (e) => Effect.succeed(page(request, { kind: "wake-failed", box: { name: e.boxId, sharedBy: null }, reason: e.reason }))
637
- })));
638
-
639
- // src/jwt.ts
640
- import { Clock as Clock2, Effect as Effect2, Option } from "effect";
641
- var KEYS_TTL_MS = 600000, REFETCH_MIN_MS = 30000, CLOCK_SKEW_S = 60, keySets = /* @__PURE__ */ new Map;
642
- function base64url(text2) {
643
- let b64 = text2.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text2.length / 4) * 4, "=");
644
- return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
645
- }
646
- function json2(part) {
647
- try {
648
- let value = JSON.parse((/* @__PURE__ */ new TextDecoder()).decode(base64url(part)));
649
- return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
650
- } catch {
651
- return null;
652
- }
653
- }
654
- var fetchKeys = (jwksUri, now) => Effect2.tryPromise(async () => {
655
- let res = await fetch(jwksUri);
656
- if (!res.ok)
657
- throw Error(`${jwksUri} answered ${res.status}`);
658
- let body2 = await res.json(), keys = /* @__PURE__ */ new Map;
659
- for (let jwk of body2.keys ?? []) {
660
- if (typeof jwk.kid !== "string" || jwk.kty !== "RSA")
661
- continue;
662
- keys.set(jwk.kid, await crypto.subtle.importKey("jwk", jwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, !1, ["verify"]));
663
- }
664
- let set = { keys, fetchedAt: now };
665
- return keySets.set(jwksUri, set), set;
666
- }).pipe(Effect2.option), keyFor = Effect2.fn("Jwt.keyFor")(function* (jwksUri, kid, now) {
667
- let set = keySets.get(jwksUri);
668
- if (set === void 0 || now - set.fetchedAt > KEYS_TTL_MS)
669
- set = Option.getOrElse(yield* fetchKeys(jwksUri, now), () => set);
670
- if (set !== void 0 && !set.keys.has(kid) && now - set.fetchedAt > REFETCH_MIN_MS)
671
- set = Option.getOrElse(yield* fetchKeys(jwksUri, now), () => set);
672
- return set?.keys.get(kid) ?? null;
673
- }), verifyRs256 = Effect2.fn("Jwt.verifyRs256")(function* (token, issuer, jwksUri) {
674
- let parts = token.split(".");
675
- if (parts.length !== 3)
676
- return null;
677
- let [h, p, s] = parts, header = json2(h), claims = json2(p);
678
- if (header === null || claims === null || header.alg !== "RS256" || typeof header.kid !== "string" || claims.iss !== issuer)
679
- return null;
680
- let nowMs = yield* Clock2.currentTimeMillis, key = yield* keyFor(jwksUri, header.kid, nowMs);
681
- if (key === null)
682
- return null;
683
- let signature = base64url(s);
684
- if (!(yield* Effect2.tryPromise(() => crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, (/* @__PURE__ */ new TextEncoder()).encode(`${h}.${p}`))).pipe(Effect2.orElseSucceed(() => !1))))
685
- return null;
686
- let now = Math.floor(nowMs / 1000);
687
- if (typeof claims.exp !== "number" || claims.exp + CLOCK_SKEW_S < now)
688
- return null;
689
- if (typeof claims.nbf === "number" && claims.nbf - CLOCK_SKEW_S > now)
690
- return null;
691
- return claims;
692
- });
693
- function audienceIncludes(aud, expected) {
694
- return aud === expected || Array.isArray(aud) && aud.includes(expected);
695
- }
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
+ }));
696
815
 
697
816
  // src/tunnel.ts
698
817
  var UPGRADE_TIMEOUT_MS = 30000, NULL_BODY = /* @__PURE__ */ new Set([101, 204, 205, 304]);
@@ -708,12 +827,12 @@ class Tunnel {
708
827
  this.socket = socket;
709
828
  this.inUse = inUse;
710
829
  }
711
- send(frame2) {
830
+ send(frame) {
712
831
  let ws = this.socket();
713
832
  if (ws === null)
714
833
  return !1;
715
834
  try {
716
- return ws.send(encodeFrame(frame2)), !0;
835
+ return ws.send(encodeFrame(frame)), !0;
717
836
  } catch {
718
837
  return !1;
719
838
  }
@@ -728,14 +847,14 @@ class Tunnel {
728
847
  async fetch(port, request) {
729
848
  let url = new URL(request.url), stream = this.newStream(), s = { kind: "http", head: null, controller: null, buffered: [], owed: 0, window: MUX_WINDOW, onCredit: null, closed: !1 }, head = new Promise((resolve, reject) => s.head = { resolve, reject });
730
849
  this.streams.set(stream, s);
731
- let headers2 = [...request.headers];
732
- if (!this.send({ type: "open", stream, port, method: request.method, path: `${url.pathname}${url.search}`, headers: headers2, ws: !1 }))
850
+ let headers = [...request.headers];
851
+ if (!this.send({ type: "open", stream, port, method: request.method, path: `${url.pathname}${url.search}`, headers, ws: !1 }))
733
852
  throw this.streams.delete(stream), new StreamRefused("the box is not connected");
734
853
  request.signal.addEventListener("abort", () => this.abort(stream, "the viewer went away")), this.pump(stream, s, request.body);
735
854
  let h = await head;
736
855
  if (NULL_BODY.has(h.status) || request.method === "HEAD")
737
856
  return this.abort(stream, "no body expected"), new Response(null, { status: h.status, statusText: h.statusText, headers: h.headers });
738
- let body2 = new ReadableStream({
857
+ let body = new ReadableStream({
739
858
  start: (controller) => {
740
859
  s.controller = controller;
741
860
  for (let chunk of s.buffered.splice(0))
@@ -746,11 +865,11 @@ class Tunnel {
746
865
  pull: () => this.credit(stream, s),
747
866
  cancel: () => this.abort(stream, "the viewer stopped reading")
748
867
  }, { highWaterMark: MUX_WINDOW, size: (chunk) => chunk.byteLength });
749
- return new Response(body2, { status: h.status, statusText: h.statusText, headers: h.headers });
868
+ return new Response(body, { status: h.status, statusText: h.statusText, headers: h.headers });
750
869
  }
751
- async upgrade(port, path, headers2) {
870
+ async upgrade(port, path, headers) {
752
871
  let stream = this.newStream(), head = new Promise((resolve, reject) => this.streams.set(stream, { kind: "ws", resolve, reject }));
753
- if (!this.send({ type: "open", stream, port, method: "GET", path, headers: headers2, ws: !0 }))
872
+ if (!this.send({ type: "open", stream, port, method: "GET", path, headers, ws: !0 }))
754
873
  throw this.streams.delete(stream), new StreamRefused("the box is not connected");
755
874
  let timeout = setTimeout(() => {
756
875
  this.fail(stream, new StreamRefused("the app did not answer the websocket upgrade")), this.send({ type: "reset", stream, reason: "the upgrade timed out" });
@@ -761,12 +880,12 @@ class Tunnel {
761
880
  clearTimeout(timeout);
762
881
  }
763
882
  }
764
- async pump(stream, s, body2) {
765
- if (body2 === null) {
883
+ async pump(stream, s, body) {
884
+ if (body === null) {
766
885
  this.send({ type: "end", stream });
767
886
  return;
768
887
  }
769
- let reader = body2.getReader();
888
+ let reader = body.getReader();
770
889
  try {
771
890
  for (;; ) {
772
891
  let { done, value } = await reader.read();
@@ -815,41 +934,41 @@ class Tunnel {
815
934
  s.controller?.error(err);
816
935
  } catch {}
817
936
  }
818
- onFrame(frame2) {
819
- let s = this.streams.get(frame2.stream);
937
+ onFrame(frame) {
938
+ let s = this.streams.get(frame.stream);
820
939
  if (s === void 0)
821
940
  return !1;
822
941
  if (s.kind === "ws") {
823
- if (frame2.type === "head")
824
- this.streams.delete(frame2.stream), s.resolve(frame2);
825
- else if (frame2.type === "reset")
826
- this.streams.delete(frame2.stream), s.reject(new StreamRefused(frame2.reason));
942
+ if (frame.type === "head")
943
+ this.streams.delete(frame.stream), s.resolve(frame);
944
+ else if (frame.type === "reset")
945
+ this.streams.delete(frame.stream), s.reject(new StreamRefused(frame.reason));
827
946
  return !0;
828
947
  }
829
- switch (frame2.type) {
948
+ switch (frame.type) {
830
949
  case "head":
831
- return s.head?.resolve(frame2), s.head = null, !0;
950
+ return s.head?.resolve(frame), s.head = null, !0;
832
951
  case "data":
833
952
  if (s.controller === null)
834
- return s.buffered.push(frame2.data), s.owed += frame2.data.byteLength, !0;
953
+ return s.buffered.push(frame.data), s.owed += frame.data.byteLength, !0;
835
954
  try {
836
- s.controller.enqueue(frame2.data);
955
+ s.controller.enqueue(frame.data);
837
956
  } catch {
838
957
  return !0;
839
958
  }
840
- if (s.owed += frame2.data.byteLength, (s.controller.desiredSize ?? 0) > 0)
841
- this.credit(frame2.stream, s);
959
+ if (s.owed += frame.data.byteLength, (s.controller.desiredSize ?? 0) > 0)
960
+ this.credit(frame.stream, s);
842
961
  return !0;
843
962
  case "end":
844
- this.streams.delete(frame2.stream), s.closed = !0;
963
+ this.streams.delete(frame.stream), s.closed = !0;
845
964
  try {
846
965
  s.controller?.close();
847
966
  } catch {}
848
967
  return !0;
849
968
  case "reset":
850
- return this.fail(frame2.stream, new StreamRefused(frame2.reason)), !0;
969
+ return this.fail(frame.stream, new StreamRefused(frame.reason)), !0;
851
970
  case "credit":
852
- return s.window += frame2.bytes, s.onCredit?.(), s.onCredit = null, !0;
971
+ return s.window += frame.bytes, s.onCredit?.(), s.onCredit = null, !0;
853
972
  default:
854
973
  return !0;
855
974
  }
@@ -860,8 +979,9 @@ class Tunnel {
860
979
  }
861
980
  }
862
981
 
863
- // src/cloudflare.ts
864
- var TICKET_HEADER = "x-tesser-ticket", ACCESS_HEADER = "cf-access-jwt-assertion", WAKE_TIMEOUT = Duration2.minutes(5), WAKE_RETRY_MS = 30000, DIAL_GRACE = Duration2.seconds(30), RECHECK_MS = 60000, RETRYABLE = /* @__PURE__ */ new Set([0, 408, 409, 429]), Ticket = Schema2.Struct({
982
+ // src/relay.ts
983
+ var TICKET_HEADER = "x-tesser-ticket", WAKE_TIMEOUT = Duration2.minutes(5), WAKE_RETRY_MS = 30000, DIAL_GRACE = Duration2.seconds(30), RECHECK_MS = 60000, STALE_MS = 300000, RETRYABLE = /* @__PURE__ */ new Set([0, 408, 409, 429]), Ticket = Schema2.Struct({
984
+ org: Schema2.String,
865
985
  box: Schema2.String,
866
986
  name: Schema2.String,
867
987
  sharedBy: Schema2.NullOr(Schema2.String),
@@ -878,63 +998,18 @@ var TICKET_HEADER = "x-tesser-ticket", ACCESS_HEADER = "cf-access-jwt-assertion"
878
998
  app: Schema2.NullOr(Schema2.Literals(["none", "running", "unsupervised", "exited"]))
879
999
  })
880
1000
  }), ErrorBody = Schema2.Struct({ error: Schema2.Struct({ message: Schema2.String }) });
881
- function plain(status, text2) {
882
- return new Response(`${text2}
1001
+ function plain(status, text) {
1002
+ return new Response(`${text}
883
1003
  `, { status, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" } });
884
1004
  }
885
- var verifyTicket = Effect3.fn("Gateway.verifyTicket")(function* (env, request) {
886
- let token = request.headers.get("authorization")?.match(/^Bearer (\S+)$/)?.[1];
887
- if (token === void 0)
888
- return null;
889
- let issuer = env.TESSER_URL.replace(/\/$/, ""), claims = yield* verifyRs256(token, issuer, `${issuer}/.well-known/jwks.json`);
890
- if (claims === null || !audienceIncludes(claims.aud, `https://${env.GATEWAY_HOST}`) || claims.org !== env.TESSER_ORG)
891
- return null;
892
- if (claims.sub !== `box:${env.TESSER_ORG}:${String(claims.box)}`)
893
- return null;
894
- return yield* Schema2.decodeUnknownEffect(Ticket)(claims).pipe(Effect3.option);
895
- }), verifyViewer = Effect3.fn("Gateway.verifyViewer")(function* (env, request) {
896
- let token = request.headers.get(ACCESS_HEADER);
897
- if (token === null)
898
- return null;
899
- let issuer = `https://${env.ACCESS_TEAM}`, claims = yield* verifyRs256(token, issuer, `${issuer}/cdn-cgi/access/certs`);
900
- return claims !== null && audienceIncludes(claims.aud, env.ACCESS_AUD) ? claims : null;
901
- });
902
- function boxIdFromHost(env, hostname) {
903
- let suffix = `.${env.GATEWAY_HOST.toLowerCase()}`, host = hostname.toLowerCase();
904
- if (!host.endsWith(suffix))
905
- return null;
906
- let label = host.slice(0, -suffix.length).split(".").pop(), match = /^box-([0-9a-z]+)(?:--\d{1,5})?$/.exec(label);
907
- return match === null ? null : `box_${match[1]}`;
908
- }
909
- var cloudflare_default = {
910
- async fetch(request, env) {
911
- let url = new URL(request.url), apex = url.hostname.toLowerCase() === env.GATEWAY_HOST.toLowerCase();
912
- if (apex && url.pathname === GATEWAY_DIAL_PATH) {
913
- if (request.headers.get("upgrade")?.toLowerCase() !== "websocket")
914
- return plain(426, "boxes dial this with a websocket");
915
- let ticket = await Effect3.runPromise(verifyTicket(env, request));
916
- if (ticket === null || ticket._tag === "None")
917
- return plain(401, "a box dials in with a share ticket signed by tesser");
918
- let headers3 = new Headers(request.headers);
919
- return headers3.delete("authorization"), headers3.set(TICKET_HEADER, JSON.stringify(ticket.value)), env.BOXES.get(env.BOXES.idFromName(ticket.value.box)).fetch(new Request(request, { headers: headers3 }));
920
- }
921
- if (apex && url.pathname === "/__tesser/health")
922
- return plain(200, "ok");
923
- let boxId = boxIdFromHost(env, url.hostname);
924
- if (boxId === null)
925
- return new Response(viewerPage({ kind: "not-shared" }), { status: 404, headers: STATUS_HEADERS });
926
- if (await Effect3.runPromise(verifyViewer(env, request)) === null)
927
- return plain(403, "this gateway only answers requests Cloudflare Access let through");
928
- let headers2 = new Headers(request.headers);
929
- return headers2.delete(TICKET_HEADER), env.BOXES.get(env.BOXES.idFromName(boxId)).fetch(new Request(request, { headers: headers2 }));
930
- }
931
- };
932
1005
 
933
- class GatewayBoxes extends DurableObject {
1006
+ class BoxRelay extends DurableObject {
934
1007
  tunnel = new Tunnel(() => this.boxSocket(), (stream) => this.ctx.getWebSockets(`v:${stream}`).length > 0);
935
1008
  waking = null;
936
1009
  connected = [];
937
- verifiedAt = 0;
1010
+ verifiedAt = /* @__PURE__ */ new Map;
1011
+ unstored = /* @__PURE__ */ new Map;
1012
+ failingSince = /* @__PURE__ */ new Map;
938
1013
  constructor(ctx, env) {
939
1014
  super(ctx, env);
940
1015
  ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(MUX_PING, MUX_PONG));
@@ -963,9 +1038,13 @@ class GatewayBoxes extends DurableObject {
963
1038
  }
964
1039
  async fetch(request) {
965
1040
  let ticket = request.headers.get(TICKET_HEADER);
966
- if (ticket !== null)
967
- return this.acceptBox(JSON.parse(ticket));
968
- let handled = await Effect3.runPromise(handle(request).pipe(Effect3.provide(this.layer(request.headers.get(ACCESS_HEADER)))));
1041
+ if (ticket !== null) {
1042
+ let decoded = Schema2.decodeUnknownOption(Ticket)(JSON.parse(ticket));
1043
+ if (decoded._tag === "None" || !this.admits(decoded.value))
1044
+ return plain(403, "this box answers for another org here");
1045
+ return this.acceptBox(decoded.value);
1046
+ }
1047
+ let handled = await Effect4.runPromise(handle(request).pipe(Effect4.provide(this.layer(this.viewer(request)))));
969
1048
  if (handled._tag === "response")
970
1049
  return handled.response;
971
1050
  return this.bridge(request, handled);
@@ -975,32 +1054,99 @@ class GatewayBoxes extends DurableObject {
975
1054
  if (previous !== null)
976
1055
  this.retire(previous, 4000, "replaced by a newer connection");
977
1056
  let [client, server] = Object.values(new WebSocketPair);
978
- this.ctx.acceptWebSocket(server, ["box"]), server.serializeAttachment({ ticket }), this.waking = null, this.verifiedAt = Date.now();
1057
+ this.ctx.acceptWebSocket(server, ["box"]), server.serializeAttachment({ ticket }), this.waking = null, this.verifiedAt.clear(), this.failingSince.clear();
979
1058
  for (let resolve of this.connected.splice(0))
980
1059
  resolve();
981
- return this.ctx.waitUntil(this.ctx.storage.setAlarm(ticket.exp * 1000)), new Response(null, { status: 101, webSocket: client });
1060
+ return this.ctx.waitUntil(this.armAlarm(ticket)), new Response(null, { status: 101, webSocket: client });
1061
+ }
1062
+ socketViewer(ws) {
1063
+ let { stream, viewer } = ws.deserializeAttachment();
1064
+ return { stream, viewer: viewer ?? this.unstored.get(stream) ?? null };
1065
+ }
1066
+ viewers() {
1067
+ return new Set(this.ctx.getWebSockets("viewer").flatMap((ws) => this.socketViewer(ws).viewer ?? []));
1068
+ }
1069
+ async armAlarm(ticket) {
1070
+ let want = this.viewers().size > 0 ? Math.min(ticket.exp * 1000, Date.now() + RECHECK_MS) : ticket.exp * 1000, pending = await this.ctx.storage.getAlarm();
1071
+ if (pending === null || pending <= Date.now() || want < pending)
1072
+ await this.ctx.storage.setAlarm(want);
1073
+ }
1074
+ closeViewer(viewer, reason) {
1075
+ for (let ws of this.ctx.getWebSockets("viewer")) {
1076
+ let { stream, viewer: who } = this.socketViewer(ws);
1077
+ if (who !== viewer)
1078
+ continue;
1079
+ this.tunnel.send({ type: "close", stream, code: 1008, reason });
1080
+ try {
1081
+ ws.close(1008, reason);
1082
+ } catch {}
1083
+ }
1084
+ }
1085
+ verdict(viewer, answer, now, open) {
1086
+ if (answer?.status === 200) {
1087
+ for (let [key, at] of this.verifiedAt)
1088
+ if (now - at > STALE_MS)
1089
+ this.verifiedAt.delete(key);
1090
+ return this.verifiedAt.set(viewer, now), this.failingSince.delete(viewer), "ok";
1091
+ }
1092
+ if (answer?.status === 404 || answer?.status === 403)
1093
+ this.verifiedAt.delete(viewer), this.failingSince.delete(viewer);
1094
+ let box = this.boxSocket();
1095
+ if (answer?.status === 404) {
1096
+ if (box !== null)
1097
+ this.retire(box, 4003, "share revoked");
1098
+ return "revoked";
1099
+ }
1100
+ if (answer?.status === 403)
1101
+ return this.closeViewer(viewer, "access ended"), "refused";
1102
+ let verified = this.verifiedAt.get(viewer);
1103
+ if (open && verified === void 0 && !this.failingSince.has(viewer))
1104
+ this.failingSince.set(viewer, now);
1105
+ let since = verified ?? (open ? this.failingSince.get(viewer) : 0);
1106
+ if (now - since > STALE_MS) {
1107
+ if (open)
1108
+ this.closeViewer(viewer, "tesser can't confirm access right now");
1109
+ return "stale";
1110
+ }
1111
+ return "unknown";
982
1112
  }
983
1113
  async alarm() {
984
1114
  let ws = this.boxSocket();
985
1115
  if (ws === null)
986
1116
  return;
987
1117
  let { ticket } = ws.deserializeAttachment();
988
- if (ticket.exp * 1000 <= Date.now())
1118
+ if (ticket.exp * 1000 <= Date.now()) {
989
1119
  this.retire(ws, 4001, "share expired");
990
- else
991
- await this.ctx.storage.setAlarm(ticket.exp * 1000);
1120
+ return;
1121
+ }
1122
+ for (let viewer of this.viewers()) {
1123
+ let answer = await this.lookupBox(ticket.box, viewer).catch(() => null);
1124
+ if (this.verdict(viewer, answer, Date.now(), !0) === "revoked")
1125
+ return;
1126
+ }
1127
+ await this.armAlarm(ticket);
992
1128
  }
993
1129
  async bridge(request, target) {
994
- let headers2 = [...target.headers].filter(([name]) => !/^sec-websocket-(key|version|extensions)$/i.test(name)), upgraded;
1130
+ let headers = [...target.headers].filter(([name]) => !/^sec-websocket-(key|version|extensions)$/i.test(name)), upgraded;
995
1131
  try {
996
- upgraded = await this.tunnel.upgrade(target.port, target.path, headers2);
1132
+ upgraded = await this.tunnel.upgrade(target.port, target.path, headers);
997
1133
  } catch (err) {
998
1134
  return plain(502, `${target.box.name} refused the websocket: ${err instanceof Error ? err.message : String(err)}`);
999
1135
  }
1000
1136
  if (upgraded.head.status !== 101)
1001
1137
  return plain(502, `${target.box.name} answered ${upgraded.head.status} to the websocket`);
1002
1138
  let [client, server] = Object.values(new WebSocketPair);
1003
- this.ctx.acceptWebSocket(server, ["viewer", `v:${upgraded.stream}`]), server.serializeAttachment({ stream: upgraded.stream });
1139
+ this.ctx.acceptWebSocket(server, ["viewer", `v:${upgraded.stream}`]);
1140
+ let viewer = this.viewer(request);
1141
+ try {
1142
+ server.serializeAttachment({ stream: upgraded.stream, viewer });
1143
+ } catch {
1144
+ if (server.serializeAttachment({ stream: upgraded.stream, viewer: null }), viewer !== null)
1145
+ this.unstored.set(upgraded.stream, viewer);
1146
+ }
1147
+ let ticket = this.live();
1148
+ if (ticket !== null)
1149
+ await this.armAlarm(ticket);
1004
1150
  let protocol = upgraded.head.headers.find(([name]) => name.toLowerCase() === "sec-websocket-protocol")?.[1];
1005
1151
  return new Response(null, { status: 101, webSocket: client, headers: protocol === void 0 ? {} : { "sec-websocket-protocol": protocol } });
1006
1152
  }
@@ -1008,29 +1154,29 @@ class GatewayBoxes extends DurableObject {
1008
1154
  if (this.ctx.getTags(ws).includes("box")) {
1009
1155
  if (typeof message === "string")
1010
1156
  return;
1011
- let frame2;
1157
+ let frame;
1012
1158
  try {
1013
- frame2 = decodeFrame(new Uint8Array(message));
1159
+ frame = decodeFrame(new Uint8Array(message));
1014
1160
  } catch {
1015
1161
  return;
1016
1162
  }
1017
- if (this.tunnel.onFrame(frame2))
1163
+ if (this.tunnel.onFrame(frame))
1018
1164
  return;
1019
- let viewer = this.ctx.getWebSockets(`v:${frame2.stream}`)[0];
1165
+ let viewer = this.ctx.getWebSockets(`v:${frame.stream}`)[0];
1020
1166
  if (viewer === void 0) {
1021
- if (frame2.type === "text" || frame2.type === "binary")
1022
- this.tunnel.send({ type: "close", stream: frame2.stream, code: 1001, reason: "the viewer went away" });
1167
+ if (frame.type === "text" || frame.type === "binary")
1168
+ this.tunnel.send({ type: "close", stream: frame.stream, code: 1001, reason: "the viewer went away" });
1023
1169
  return;
1024
1170
  }
1025
1171
  try {
1026
- if (frame2.type === "text")
1027
- viewer.send(frame2.text);
1028
- else if (frame2.type === "binary")
1029
- viewer.send(frame2.data);
1030
- else if (frame2.type === "close")
1031
- viewer.close(frame2.code, frame2.reason);
1032
- else if (frame2.type === "reset")
1033
- viewer.close(1011, frame2.reason.slice(0, 120));
1172
+ if (frame.type === "text")
1173
+ viewer.send(frame.text);
1174
+ else if (frame.type === "binary")
1175
+ viewer.send(frame.data);
1176
+ else if (frame.type === "close")
1177
+ viewer.close(frame.code, frame.reason);
1178
+ else if (frame.type === "reset")
1179
+ viewer.close(1011, frame.reason.slice(0, 120));
1034
1180
  } catch {}
1035
1181
  return;
1036
1182
  }
@@ -1044,91 +1190,151 @@ class GatewayBoxes extends DurableObject {
1044
1190
  this.retire(ws, 1000, "box hung up");
1045
1191
  return;
1046
1192
  }
1047
- let { stream } = ws.deserializeAttachment(), sendable = code === 1005 ? 1000 : code === 1004 || code === 1006 || code === 1015 || code < 1000 || code > 4999 ? 1001 : code;
1193
+ let { stream } = ws.deserializeAttachment();
1194
+ this.unstored.delete(stream);
1195
+ let sendable = code === 1005 ? 1000 : code === 1004 || code === 1006 || code === 1015 || code < 1000 || code > 4999 ? 1001 : code;
1048
1196
  this.tunnel.send({ type: "close", stream, code: sendable, reason: reason.slice(0, 120) });
1049
1197
  }
1050
1198
  webSocketError(ws) {
1051
1199
  this.webSocketClose(ws, 1006, "socket error");
1052
1200
  }
1053
- tesser(method, path, jwt) {
1054
- let env = this.env;
1055
- return Effect3.tryPromise({
1056
- try: async () => {
1057
- let res = await fetch(`${env.TESSER_URL.replace(/\/$/, "")}/v1/${env.TESSER_ORG}${path}`, { method, headers: jwt === null ? {} : { [ACCESS_HEADER]: jwt } }), body2 = await res.json().catch(() => null);
1058
- return { status: res.status, body: body2 };
1059
- },
1060
- catch: (err) => new DirectoryUnavailable({ reason: err instanceof Error ? err.message : String(err) })
1061
- });
1201
+ ask(call) {
1202
+ return Effect4.tryPromise({ try: call, catch: (err) => new DirectoryUnavailable({ reason: err instanceof Error ? err.message : String(err) }) });
1062
1203
  }
1063
- layer(jwt) {
1064
- let self = this, remote = Effect3.fn("Cloudflare.remote")(function* (boxId) {
1065
- let { status: status2, body: body2 } = yield* self.tesser("GET", `/gateway/boxes/${boxId}`, jwt);
1066
- if (status2 === 403 || status2 === 404)
1204
+ layer(viewer) {
1205
+ let self = this, remote = Effect4.fn("Relay.remote")(function* (boxId) {
1206
+ let { status, body } = yield* self.ask(() => self.lookupBox(boxId, viewer));
1207
+ if (status === 403 || status === 404)
1067
1208
  return yield* new BoxNotShared({ boxId });
1068
- if (status2 !== 200) {
1069
- let message = Schema2.decodeUnknownOption(ErrorBody)(body2);
1070
- return yield* new DirectoryUnavailable({ reason: message._tag === "Some" ? message.value.error.message : `tesser answered ${status2}` });
1071
- }
1072
- return (yield* Schema2.decodeUnknownEffect(BoxResponse)(body2).pipe(Effect3.mapError(() => new DirectoryUnavailable({ reason: "tesser answered with a box this gateway can't read" })))).box;
1073
- }), lookup = Effect3.fn("Cloudflare.lookup")(function* (boxId) {
1074
- let ticket = self.live();
1075
- if (ticket === null) {
1076
- let box = yield* remote(boxId);
1077
- return { ...box, power: box.power === "awake" ? "launching" : box.power };
1078
- }
1079
- let now = yield* Clock3.currentTimeMillis;
1080
- if (jwt !== null && now - self.verifiedAt > RECHECK_MS) {
1081
- let checked = yield* remote(boxId).pipe(Effect3.result);
1082
- if (checked._tag === "Success")
1083
- self.verifiedAt = now;
1084
- else if (checked.failure._tag === "BoxNotShared") {
1085
- let ws = self.boxSocket();
1086
- if (ws !== null)
1087
- self.retire(ws, 4003, "share revoked");
1088
- return yield* checked.failure;
1089
- }
1209
+ if (status !== 200) {
1210
+ let message = Schema2.decodeUnknownOption(ErrorBody)(body);
1211
+ return yield* new DirectoryUnavailable({ reason: message._tag === "Some" ? message.value.error.message : `tesser answered ${status}` });
1090
1212
  }
1091
- return { boxId, name: ticket.name, sharedBy: ticket.sharedBy, privateIp: "", ports: ticket.ports, power: "awake", app: "running" };
1092
- }), wake = Effect3.fn("Cloudflare.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) {
1093
1215
  let now = yield* Clock3.currentTimeMillis, current = self.waking;
1094
1216
  if (current?.inflight)
1095
1217
  return;
1096
- if (current?.failure && !retry)
1097
- return;
1098
1218
  if (current !== null && current.failure === null && now - current.kickedAt < WAKE_RETRY_MS)
1099
1219
  return;
1100
- 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 };
1101
1224
  self.waking = waking;
1102
- let kicked = yield* self.tesser("POST", `/gateway/boxes/${boxId}/wake`, jwt).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)));
1103
1226
  if (kicked._tag === "Failure")
1104
1227
  return;
1105
- let { status: status2, body: body2 } = kicked.success;
1106
- if (status2 < 300 || status2 >= 500 || RETRYABLE.has(status2))
1228
+ let { status, body } = kicked.success;
1229
+ if (status < 300 || status >= 500 || RETRYABLE.has(status))
1107
1230
  return;
1108
- let message = Schema2.decodeUnknownOption(ErrorBody)(body2);
1109
- waking.failure = message._tag === "Some" ? message.value.error.message : `tesser answered ${status2}`;
1110
- }), status = Effect3.fn("Cloudflare.status")(function* (_boxId) {
1231
+ let message = Schema2.decodeUnknownOption(ErrorBody)(body);
1232
+ waking.failure = message._tag === "Some" ? message.value.error.message : `tesser answered ${status}`;
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) {
1111
1251
  let now = yield* Clock3.currentTimeMillis, w = self.waking;
1112
1252
  if (w === null || w.failure === null && now - w.since > Duration2.toMillis(WAKE_TIMEOUT))
1113
1253
  return null;
1114
1254
  return { since: w.since, failure: w.failure };
1115
- }), 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("Cloudflare.awake")(function* (boxId) {
1116
- if (self.live() === null)
1117
- 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")
1118
1259
  yield* dialed(boxId, DIAL_GRACE, "the box is up but hasn't connected to this gateway; its boxd may need an update");
1119
1260
  else {
1120
- yield* wake(boxId, !1);
1121
- let failure = self.waking?.failure;
1122
- if (failure)
1123
- 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 });
1124
1266
  yield* dialed(boxId, WAKE_TIMEOUT, "it didn't wake up within 5 minutes");
1125
1267
  }
1268
+ }
1126
1269
  return yield* lookup(boxId);
1127
- }), upstream = (box, port, request) => Effect3.tryPromise({ try: () => self.tunnel.fetch(port, request), catch: () => new UpstreamRefused({ boxId: box.boxId, port }) });
1128
- return Layer.mergeAll(Layer.succeed(BoxDirectory)({ host: Effect3.succeed(self.env.GATEWAY_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 }));
1272
+ }
1273
+ }
1274
+
1275
+ // src/cloudflare.ts
1276
+ var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect5.fn("Gateway.verifyTicket")(function* (env, request) {
1277
+ let token = request.headers.get("authorization")?.match(/^Bearer (\S+)$/)?.[1];
1278
+ if (token === void 0)
1279
+ return null;
1280
+ let issuer = env.TESSER_URL.replace(/\/$/, ""), claims = yield* verifyRs256(token, issuer, `${issuer}/.well-known/jwks.json`);
1281
+ if (claims === null || !audienceIncludes(claims.aud, `https://${env.GATEWAY_HOST}`) || claims.org !== env.TESSER_ORG)
1282
+ return null;
1283
+ if (claims.sub !== `box:${env.TESSER_ORG}:${String(claims.box)}`)
1284
+ return null;
1285
+ return yield* Schema3.decodeUnknownEffect(Ticket)(claims).pipe(Effect5.option);
1286
+ }), verifyViewer = Effect5.fn("Gateway.verifyViewer")(function* (env, request) {
1287
+ let token = request.headers.get(ACCESS_HEADER);
1288
+ if (token === null)
1289
+ return null;
1290
+ let issuer = `https://${env.ACCESS_TEAM}`, claims = yield* verifyRs256(token, issuer, `${issuer}/cdn-cgi/access/certs`);
1291
+ return claims !== null && audienceIncludes(claims.aud, env.ACCESS_AUD) ? claims : null;
1292
+ }), cloudflare_default = {
1293
+ async fetch(request, env) {
1294
+ let url = new URL(request.url), apex = url.hostname.toLowerCase() === env.GATEWAY_HOST.toLowerCase();
1295
+ if (apex && url.pathname === GATEWAY_DIAL_PATH) {
1296
+ if (request.headers.get("upgrade")?.toLowerCase() !== "websocket")
1297
+ return plain(426, "boxes dial this with a websocket");
1298
+ let ticket = await Effect5.runPromise(verifyTicket(env, request));
1299
+ if (ticket === null || ticket._tag === "None")
1300
+ return plain(401, "a box dials in with a share ticket signed by tesser");
1301
+ let headers = new Headers(request.headers);
1302
+ return headers.delete("authorization"), headers.set(TICKET_HEADER, JSON.stringify(ticket.value)), env.BOXES.get(env.BOXES.idFromName(ticket.value.box)).fetch(new Request(request, { headers }));
1303
+ }
1304
+ if (apex && url.pathname === "/__tesser/health")
1305
+ return plain(200, "ok");
1306
+ let boxId = gatewayHostBox(env.GATEWAY_HOST, url.hostname);
1307
+ if (boxId === null)
1308
+ return new Response(viewerPage({ kind: "not-shared" }), { status: 404, headers: STATUS_HEADERS });
1309
+ if (await Effect5.runPromise(verifyViewer(env, request)) === null)
1310
+ return plain(403, "this gateway only answers requests Cloudflare Access let through");
1311
+ let headers = new Headers(request.headers);
1312
+ return headers.delete(TICKET_HEADER), env.BOXES.get(env.BOXES.idFromName(boxId)).fetch(new Request(request, { headers }));
1313
+ }
1314
+ };
1315
+
1316
+ class GatewayBoxes extends BoxRelay {
1317
+ host() {
1318
+ return this.env.GATEWAY_HOST;
1319
+ }
1320
+ viewer(request) {
1321
+ return request.headers.get(ACCESS_HEADER);
1322
+ }
1323
+ admits(ticket) {
1324
+ return ticket.org === this.env.TESSER_ORG;
1325
+ }
1326
+ lookupBox(boxId, viewer) {
1327
+ return this.tesser("GET", `/gateway/boxes/${boxId}`, viewer);
1328
+ }
1329
+ wakeBox(boxId, viewer) {
1330
+ return this.tesser("POST", `/gateway/boxes/${boxId}/wake`, viewer);
1331
+ }
1332
+ async tesser(method, path, jwt) {
1333
+ let res = await fetch(`${this.env.TESSER_URL.replace(/\/$/, "")}/v1/${this.env.TESSER_ORG}${path}`, { method, headers: jwt === null ? {} : { [ACCESS_HEADER]: jwt } }), body = await res.json().catch(() => null);
1334
+ return { status: res.status, body };
1129
1335
  }
1130
1336
  }
1131
1337
  export {
1132
- cloudflare_default as default,
1133
- GatewayBoxes
1338
+ GatewayBoxes,
1339
+ cloudflare_default as default
1134
1340
  };