@tesser-sh/gateway 0.0.0-stage → 0.53.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Tesser Labs
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/index.d.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { DurableObject } from "cloudflare:workers";
2
+
3
+ export interface GatewayEnv {
4
+ readonly BOXES: DurableObjectNamespace<GatewayBoxes>;
5
+ readonly GATEWAY_HOST: string;
6
+ readonly TESSER_URL: string;
7
+ readonly TESSER_ORG: string;
8
+ readonly ACCESS_TEAM: string;
9
+ readonly ACCESS_AUD: string;
10
+ }
11
+
12
+ declare const gateway: { fetch(request: Request, env: GatewayEnv): Promise<Response> };
13
+ export default gateway;
14
+
15
+ export declare class GatewayBoxes extends DurableObject<GatewayEnv> {}
package/index.js ADDED
@@ -0,0 +1,1277 @@
1
+ // ../front/src/page.ts
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>')}`;
3
+ function escapeHtml(text) {
4
+ return text.replace(/[&<>"]/g, (c) => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;" })[c]);
5
+ }
6
+ function shell(title, main, script = "", panel = !0) {
7
+ return `<!doctype html>
8
+ <html lang="en">
9
+ <meta charset="utf-8">
10
+ <meta name="viewport" content="width=device-width, initial-scale=1">
11
+ <title>${escapeHtml(title)}</title>
12
+ <link rel="icon" href="${FAVICON}">
13
+ <style>
14
+ :root {
15
+ --ground: #171717; --well: #1f1f1f; --rule: #2e2e2e; --rule-strong: #444444;
16
+ --bone: #f0f1e9; --dim: #a8a8a8; --faint: #8f8f8f; --remote: #fbffc9; --bad: #f87171;
17
+ --sans: ui-sans-serif, system-ui, -apple-system, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
18
+ --mono: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace;
19
+ color-scheme: dark;
20
+ }
21
+ * { box-sizing: border-box; margin: 0; }
22
+ body { background: var(--ground); color: var(--bone); font: 14px/1.5 var(--sans); -webkit-font-smoothing: antialiased; }
23
+ .page { max-width: 520px; margin: 0 auto; padding: 18vh 16px 96px; }
24
+ header { display: flex; align-items: center; gap: 8px; margin-bottom: 28px; font-size: 13px; font-weight: 500; }
25
+ .mark { width: 16px; height: 16px; fill: var(--bone); }
26
+ .mark .remote { fill: var(--remote); }
27
+ h1 { font-size: 22px; line-height: 1.25; font-weight: 500; letter-spacing: -0.01em; text-wrap: balance; overflow-wrap: anywhere; }
28
+ .sq { display: inline-block; width: 8px; height: 8px; margin-right: 10px; vertical-align: 2px; background: var(--remote); animation: wait 1.2s steps(2, jump-none) infinite; }
29
+ @keyframes wait { to { opacity: 0.25; } }
30
+ @media (prefers-reduced-motion: reduce) { .sq { animation: none; } }
31
+ code, .mono { font-family: var(--mono); font-size: 13px; }
32
+ :focus-visible { outline: 2px solid var(--remote); outline-offset: 2px; }
33
+ .cmd { display: flex; margin-top: 24px; background: var(--well); border: 1px solid var(--rule); }
34
+ .cmd code { flex: 1; min-width: 0; padding: 11px 14px; overflow-x: auto; white-space: nowrap; }
35
+ .cmd code::before { content: "$ "; color: var(--faint); }
36
+ .cmd button { padding: 0 14px; background: none; border: 0; border-left: 1px solid var(--rule); color: var(--dim); font: 12.5px var(--sans); cursor: pointer; }
37
+ .cmd button:hover { color: var(--bone); background: var(--rule); }
38
+ ul { list-style: none; padding: 0; margin-top: 24px; border: 1px solid var(--rule); }
39
+ li { display: flex; align-items: center; gap: 16px; min-height: 48px; padding: 8px 8px 8px 14px; border-top: 1px solid var(--rule); }
40
+ li:first-child { border-top: 0; }
41
+ li .name { flex: 1; min-width: 0; overflow-wrap: anywhere; }
42
+ .ports { color: var(--dim); white-space: nowrap; padding-right: 6px; }
43
+ .ports a { color: inherit; text-decoration: none; }
44
+ .ports a:hover { color: var(--bone); text-decoration: underline; text-decoration-color: var(--remote); text-underline-offset: 3px; }
45
+ tesser-use { color: var(--faint); }
46
+ tesser-wake { display: block; margin-top: 24px; }
47
+ .bad { margin-top: 24px; color: var(--bad); font-size: 13px; }
48
+ .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
+ a.open { display: inline-block; text-decoration: none; }
51
+ .sub { margin-top: 8px; color: var(--dim); }
52
+ .steps { list-style: none; padding: 0; margin-top: 24px; }
53
+ .steps li { display: flex; align-items: flex-start; gap: 0; min-height: 0; padding: 4px 0; border: 0; color: var(--dim); overflow-wrap: anywhere; }
54
+ .steps li.now { color: var(--bone); }
55
+ .steps .done { flex: none; width: 8px; margin-right: 10px; color: var(--faint); font-size: 11px; line-height: 21px; }
56
+ .steps .failed { color: var(--bad); }
57
+ .steps .sq { flex: none; margin-top: 7px; }
58
+ .elapsed { flex: none; margin-left: auto; padding-left: 16px; color: var(--faint); white-space: nowrap; }
59
+ .code { margin-top: 28px; padding: 18px 0; background: var(--well); border: 1px solid var(--rule); font: 500 32px/1 var(--mono); letter-spacing: 0.12em; text-align: center; }
60
+ .hint { margin-top: 16px; color: var(--faint); font-size: 13px; }
61
+ .who { margin-top: 8px; color: var(--faint); font-size: 13px; }
62
+ </style>
63
+ <div class="page">
64
+ <header><svg class="mark" viewBox="0 0 24 24" aria-hidden="true"><path 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 class="remote" d="M12.5 2H22V11.5H12.5Z"/></svg>tesser</header>
65
+ ${main}
66
+ </div>
67
+ ${script}
68
+ ${panel ? '<script src="/__tesser/panel.js" defer></script>' : ""}
69
+ </html>`;
70
+ }
71
+ var RETRY = `<script>
72
+ document.addEventListener("click", (event) => {
73
+ const button = event.target.closest("[data-copy]");
74
+ if (!button) return;
75
+ navigator.clipboard.writeText(button.dataset.copy).then(() => {
76
+ button.textContent = "Copied";
77
+ setTimeout(() => { button.textContent = "Copy"; }, 1200);
78
+ });
79
+ });
80
+ (async () => {
81
+ let shown = document.querySelector("main").outerHTML;
82
+ for (;;) {
83
+ await new Promise((resolve) => setTimeout(resolve, 1500));
84
+ try {
85
+ const response = await fetch(location.href, { headers: { accept: "text/html" }, cache: "no-store" });
86
+ if (!response.headers.has("x-tesser-status")) return location.reload();
87
+ const page = new DOMParser().parseFromString(await response.text(), "text/html");
88
+ const next = page.querySelector("main");
89
+ if (!next || next.outerHTML === shown) continue;
90
+ shown = next.outerHTML;
91
+ document.title = page.title;
92
+ document.querySelector("main").replaceWith(next);
93
+ } catch {}
94
+ }
95
+ })();
96
+ </script>`, STATUS_HEADERS = { "content-type": "text/html; charset=utf-8", "cache-control": "no-store", "x-tesser-status": "1" };
97
+
98
+ // ../front/src/viewer.ts
99
+ var STATUS = {
100
+ waking: 503,
101
+ booting: 503,
102
+ "not-answering": 504,
103
+ stopped: 502,
104
+ "wake-failed": 503,
105
+ "no-port": 404,
106
+ "not-shared": 404,
107
+ unavailable: 503
108
+ };
109
+ function viewerStatus(state) {
110
+ return STATUS[state.kind];
111
+ }
112
+ function viewerText(state) {
113
+ switch (state.kind) {
114
+ case "waking":
115
+ return `${state.box.name} is waking up; retry shortly`;
116
+ case "booting":
117
+ return `${state.box.name} is booting; retry shortly`;
118
+ case "not-answering":
119
+ return `${state.box.name} isn't answering on port ${state.port}`;
120
+ case "stopped":
121
+ return `${state.box.name} has stopped`;
122
+ case "wake-failed":
123
+ return `${state.box.name} didn't wake up: ${state.reason}`;
124
+ case "no-port":
125
+ return `${state.box.name} has no port ${state.port}`;
126
+ case "not-shared":
127
+ return "nothing is shared here";
128
+ case "unavailable":
129
+ return "this gateway can't reach tesser to check what is shared";
130
+ }
131
+ }
132
+ var STAGES = ["machine", "app", "port"];
133
+ function steps(stage, port, seconds) {
134
+ let label = { machine: "Starting the machine", app: "Starting the app", port: `Waiting for port ${port}` }, at = STAGES.indexOf(stage);
135
+ return `<ol class="steps">${STAGES.map((step, i) => {
136
+ if (i < at)
137
+ return `<li><span class="done">✓</span><span>${label[step]}</span></li>`;
138
+ if (i === at)
139
+ return `<li class="now"><span class="sq"></span><span>${escapeHtml(label[step])}</span><span class="elapsed mono">${seconds}s</span></li>`;
140
+ return `<li><span class="done"></span><span>${escapeHtml(label[step])}</span></li>`;
141
+ }).join("")}</ol>`;
142
+ }
143
+ function who(box) {
144
+ return box.sharedBy === null ? "" : `<p class="who">Shared by ${escapeHtml(box.sharedBy)}</p>`;
145
+ }
146
+ function body(state) {
147
+ switch (state.kind) {
148
+ case "waking":
149
+ return {
150
+ headline: `Waking ${state.box.name}`,
151
+ spinner: !0,
152
+ retry: !0,
153
+ 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
+ };
155
+ case "booting":
156
+ 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
+ case "not-answering":
158
+ return {
159
+ headline: `${state.box.name} isn't answering on port ${state.port}`,
160
+ spinner: !0,
161
+ retry: !0,
162
+ rest: `${who(state.box)}<p class="hint">The box is up but the app hasn't answered yet. Still trying.</p>`
163
+ };
164
+ case "stopped":
165
+ return {
166
+ headline: `${state.box.name} has stopped`,
167
+ spinner: !1,
168
+ 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>`
170
+ };
171
+ case "wake-failed":
172
+ return {
173
+ headline: `${state.box.name} didn't wake up`,
174
+ spinner: !1,
175
+ 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>`
177
+ };
178
+ case "no-port":
179
+ return {
180
+ headline: `${state.box.name} has no port ${state.port}`,
181
+ spinner: !1,
182
+ retry: !1,
183
+ rest: `${who(state.box)}<ul>${state.ports.map((p) => `<li><span class="name ports mono"><a href="${escapeHtml(p.href)}">:${p.port}</a></span></li>`).join("")}</ul>`
184
+ };
185
+ case "not-shared":
186
+ return { headline: "Nothing is shared here", spinner: !1, retry: !1, rest: '<p class="sub">This link was never shared, or it was unshared. Ask whoever sent it for a new one.</p>' };
187
+ case "unavailable":
188
+ 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
+ }
190
+ }
191
+ function viewerPage(state) {
192
+ let { headline, spinner, retry, rest } = body(state), main = `<main>
193
+ <h1>${spinner ? '<span class="sq"></span>' : ""}${escapeHtml(headline)}</h1>
194
+ ${rest}
195
+ </main>`;
196
+ return shell(`${headline} · tesser`, main, retry ? RETRY : "", !1);
197
+ }
198
+
199
+ // ../protocol/src/table.ts
200
+ function boxIdFromLabel(label) {
201
+ let m = /^box(?:[-_]([0-9a-z]+)|([0-9a-f]+))$/i.exec(label);
202
+ return m === null ? null : `box_${(m[1] ?? m[2]).toLowerCase()}`;
203
+ }
204
+
205
+ // ../protocol/src/gateway.ts
206
+ var GATEWAY_DIAL_PATH = "/__tesser/box";
207
+ function gatewayLabel(boxId, port) {
208
+ return `box-${boxId.slice(4)}${port === null ? "" : `--${port}`}`;
209
+ }
210
+ function parseGatewayLabel(label) {
211
+ let [head, port, ...rest] = label.split("--"), boxId = boxIdFromLabel(head);
212
+ if (boxId === null || rest.length > 0)
213
+ return null;
214
+ if (port === void 0)
215
+ return { boxId, port: null };
216
+ if (!/^[1-9]\d{0,4}$/.test(port) || Number(port) > 65535)
217
+ return null;
218
+ return { boxId, port: Number(port) };
219
+ }
220
+ function gatewayHostBox(gatewayHost, hostname) {
221
+ let suffix = `.${gatewayHost.toLowerCase()}`, host = hostname.toLowerCase().replace(/\.$/, "");
222
+ if (!host.endsWith(suffix))
223
+ return null;
224
+ return parseGatewayLabel(host.slice(0, -suffix.length).split(".").pop())?.boxId ?? null;
225
+ }
226
+
227
+ // ../protocol/src/mux.ts
228
+ var MUX_WINDOW = 262144, MUX_CHUNK = 65536;
229
+ var MUX_PING = "tesser:ping", MUX_PONG = "tesser:pong", CODES = { open: 1, head: 2, data: 3, end: 4, reset: 5, credit: 6, text: 7, binary: 8, close: 9 }, TYPES = Object.fromEntries(Object.entries(CODES).map(([k, v]) => [v, k])), encoder = /* @__PURE__ */ new TextEncoder, decoder = new TextDecoder("utf-8", { fatal: !0, ignoreBOM: !1 });
230
+ function frame(type, stream, payload) {
231
+ let out = new Uint8Array(5 + payload.byteLength);
232
+ return out[0] = CODES[type], new DataView(out.buffer).setUint32(1, stream), out.set(payload, 5), out;
233
+ }
234
+ function encodeFrame(f) {
235
+ switch (f.type) {
236
+ case "open":
237
+ return frame(f.type, f.stream, encoder.encode(JSON.stringify({ port: f.port, method: f.method, path: f.path, headers: f.headers, ws: f.ws })));
238
+ case "head":
239
+ return frame(f.type, f.stream, encoder.encode(JSON.stringify({ status: f.status, statusText: f.statusText, headers: f.headers })));
240
+ case "data":
241
+ case "binary":
242
+ return frame(f.type, f.stream, f.data);
243
+ case "end":
244
+ return frame(f.type, f.stream, new Uint8Array);
245
+ case "reset":
246
+ return frame(f.type, f.stream, encoder.encode(f.reason));
247
+ case "credit": {
248
+ let payload = new Uint8Array(4);
249
+ return new DataView(payload.buffer).setUint32(0, f.bytes), frame(f.type, f.stream, payload);
250
+ }
251
+ case "text":
252
+ return frame(f.type, f.stream, encoder.encode(f.text));
253
+ case "close":
254
+ return frame(f.type, f.stream, encoder.encode(JSON.stringify({ code: f.code, reason: f.reason })));
255
+ }
256
+ }
257
+ function json(payload, what) {
258
+ let value = JSON.parse(decoder.decode(payload));
259
+ if (typeof value !== "object" || value === null || Array.isArray(value))
260
+ throw Error(`mux ${what}: expected an object`);
261
+ return value;
262
+ }
263
+ function int(o, key, what, min, max) {
264
+ let v = o[key];
265
+ if (typeof v !== "number" || !Number.isInteger(v) || v < min || v > max)
266
+ throw Error(`mux ${what}.${key}: expected an integer in ${min}..${max}`);
267
+ return v;
268
+ }
269
+ function text(o, key, what) {
270
+ let v = o[key];
271
+ if (typeof v !== "string")
272
+ throw Error(`mux ${what}.${key}: expected a string`);
273
+ return v;
274
+ }
275
+ function headers(o, what) {
276
+ let v = o.headers;
277
+ if (!Array.isArray(v) || !v.every((h) => Array.isArray(h) && h.length === 2 && typeof h[0] === "string" && typeof h[1] === "string"))
278
+ throw Error(`mux ${what}.headers: expected [name, value] pairs`);
279
+ return v;
280
+ }
281
+ function decodeFrame(buf) {
282
+ if (buf.byteLength < 5)
283
+ throw Error("mux frame: shorter than its header");
284
+ let type = TYPES[buf[0]];
285
+ if (type === void 0)
286
+ throw Error(`mux frame: unknown type ${buf[0]}`);
287
+ let stream = new DataView(buf.buffer, buf.byteOffset, buf.byteLength).getUint32(1), payload = buf.subarray(5);
288
+ switch (type) {
289
+ case "open": {
290
+ let o = json(payload, type);
291
+ if (typeof o.ws !== "boolean")
292
+ throw Error("mux open.ws: expected a boolean");
293
+ let path = text(o, "path", type);
294
+ if (!path.startsWith("/"))
295
+ throw Error("mux open.path: must start with /");
296
+ return { type, stream, port: int(o, "port", type, 1, 65535), method: text(o, "method", type), path, headers: headers(o, type), ws: o.ws };
297
+ }
298
+ case "head": {
299
+ let o = json(payload, type);
300
+ return { type, stream, status: int(o, "status", type, 100, 599), statusText: text(o, "statusText", type), headers: headers(o, type) };
301
+ }
302
+ case "data":
303
+ case "binary":
304
+ return { type, stream, data: payload };
305
+ case "end":
306
+ return { type, stream };
307
+ case "reset":
308
+ return { type, stream, reason: decoder.decode(payload) };
309
+ case "credit":
310
+ if (payload.byteLength !== 4)
311
+ throw Error("mux credit: expected 4 bytes");
312
+ return { type, stream, bytes: new DataView(payload.buffer, payload.byteOffset, 4).getUint32(0) };
313
+ case "text":
314
+ return { type, stream, text: decoder.decode(payload) };
315
+ case "close": {
316
+ let o = json(payload, type);
317
+ return { type, stream, code: int(o, "code", type, 1000, 4999), reason: text(o, "reason", type) };
318
+ }
319
+ }
320
+ }
321
+
322
+ // src/cloudflare.ts
323
+ import { Effect as Effect4, Schema as Schema3 } from "effect";
324
+
325
+ // src/jwt.ts
326
+ import { Clock, Effect, Option } from "effect";
327
+ var KEYS_TTL_MS = 600000, REFETCH_MIN_MS = 30000, CLOCK_SKEW_S = 60, keySets = /* @__PURE__ */ new Map;
328
+ function base64url(text) {
329
+ let b64 = text.replace(/-/g, "+").replace(/_/g, "/").padEnd(Math.ceil(text.length / 4) * 4, "=");
330
+ return Uint8Array.from(atob(b64), (c) => c.charCodeAt(0));
331
+ }
332
+ function json2(part) {
333
+ try {
334
+ let value = JSON.parse((/* @__PURE__ */ new TextDecoder()).decode(base64url(part)));
335
+ return typeof value === "object" && value !== null && !Array.isArray(value) ? value : null;
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+ var fetchKeys = (jwksUri, now) => Effect.tryPromise(async () => {
341
+ let res = await fetch(jwksUri);
342
+ if (!res.ok)
343
+ throw Error(`${jwksUri} answered ${res.status}`);
344
+ let body = await res.json(), keys = /* @__PURE__ */ new Map;
345
+ for (let jwk of body.keys ?? []) {
346
+ if (typeof jwk.kid !== "string" || jwk.kty !== "RSA")
347
+ continue;
348
+ keys.set(jwk.kid, await crypto.subtle.importKey("jwk", jwk, { name: "RSASSA-PKCS1-v1_5", hash: "SHA-256" }, !1, ["verify"]));
349
+ }
350
+ let set = { keys, fetchedAt: now };
351
+ return keySets.set(jwksUri, set), set;
352
+ }).pipe(Effect.option), keyFor = Effect.fn("Jwt.keyFor")(function* (jwksUri, kid, now) {
353
+ let set = keySets.get(jwksUri);
354
+ if (set === void 0 || now - set.fetchedAt > KEYS_TTL_MS)
355
+ set = Option.getOrElse(yield* fetchKeys(jwksUri, now), () => set);
356
+ if (set !== void 0 && !set.keys.has(kid) && now - set.fetchedAt > REFETCH_MIN_MS)
357
+ set = Option.getOrElse(yield* fetchKeys(jwksUri, now), () => set);
358
+ return set?.keys.get(kid) ?? null;
359
+ }), verifyRs256 = Effect.fn("Jwt.verifyRs256")(function* (token, issuer, jwksUri) {
360
+ let parts = token.split(".");
361
+ if (parts.length !== 3)
362
+ return null;
363
+ let [h, p, s] = parts, header = json2(h), claims = json2(p);
364
+ if (header === null || claims === null || header.alg !== "RS256" || typeof header.kid !== "string" || claims.iss !== issuer)
365
+ return null;
366
+ let nowMs = yield* Clock.currentTimeMillis, key = yield* keyFor(jwksUri, header.kid, nowMs);
367
+ if (key === null)
368
+ return null;
369
+ let signature = base64url(s);
370
+ if (!(yield* Effect.tryPromise(() => crypto.subtle.verify("RSASSA-PKCS1-v1_5", key, signature, (/* @__PURE__ */ new TextEncoder()).encode(`${h}.${p}`))).pipe(Effect.orElseSucceed(() => !1))))
371
+ return null;
372
+ let now = Math.floor(nowMs / 1000);
373
+ if (typeof claims.exp !== "number" || claims.exp + CLOCK_SKEW_S < now)
374
+ return null;
375
+ if (typeof claims.nbf === "number" && claims.nbf - CLOCK_SKEW_S > now)
376
+ return null;
377
+ return claims;
378
+ });
379
+ function audienceIncludes(aud, expected) {
380
+ return aud === expected || Array.isArray(aud) && aud.includes(expected);
381
+ }
382
+
383
+ // src/relay.ts
384
+ import { DurableObject } from "cloudflare:workers";
385
+ import { Clock as Clock3, Duration as Duration2, Effect as Effect3, Layer, Schema as Schema2 } from "effect";
386
+
387
+ // ../front/src/csp.ts
388
+ var SCRIPT_DIRECTIVES = ["connect-src", "script-src"];
389
+ function directiveIndex(directives, name) {
390
+ return directives.findIndex((directive) => {
391
+ let lower = directive.toLowerCase();
392
+ return lower === name || lower.startsWith(`${name} `);
393
+ });
394
+ }
395
+ function allowSelfScripts(policy) {
396
+ return policy.split(",").map((single) => {
397
+ let directives = single.split(";").map((directive) => directive.trim()).filter(Boolean), fallback = directives[directiveIndex(directives, "default-src")];
398
+ for (let name of SCRIPT_DIRECTIVES) {
399
+ let index = directiveIndex(directives, name), current = index === -1 ? fallback?.replace(/^default-src/i, name) : directives[index];
400
+ if (!current)
401
+ continue;
402
+ let values = current.split(/\s+/).slice(1);
403
+ if (values.some((value) => value === "'self'" || value === "*"))
404
+ continue;
405
+ let next = [name, ...values.filter((value) => value !== "'none'"), "'self'"].join(" ");
406
+ if (index === -1)
407
+ directives.push(next);
408
+ else
409
+ directives[index] = next;
410
+ }
411
+ return directives.join("; ");
412
+ }).join(", ");
413
+ }
414
+ function scriptNonce(policy) {
415
+ for (let single of policy.split(",")) {
416
+ let directives = single.split(";").map((directive) => directive.trim()), nonce = (directives[directiveIndex(directives, "script-src")] ?? directives[directiveIndex(directives, "default-src")])?.match(/'nonce-([^']+)'/)?.[1];
417
+ if (nonce)
418
+ return nonce;
419
+ }
420
+ return;
421
+ }
422
+
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
+ // ../front/src/shim.ts
446
+ function rewriteUrl(input, ws, ctx) {
447
+ let url;
448
+ try {
449
+ url = new URL(input, ctx.href);
450
+ } catch {
451
+ return input;
452
+ }
453
+ if (!["http:", "https:", "ws:", "wss:"].includes(url.protocol))
454
+ return input;
455
+ let page = new URL(ctx.href);
456
+ if (url.host === page.host)
457
+ return input;
458
+ let name = url.hostname, loopback = name === "localhost" || name === "127.0.0.1" || name === "[::1]" || name === "0.0.0.0", sub = name.endsWith(".localhost") ? name.slice(0, -10) : null;
459
+ if (!loopback && sub === null && name !== page.hostname)
460
+ return input;
461
+ let port = Number(url.port || (url.protocol === "https:" || url.protocol === "wss:" ? 443 : 80));
462
+ if (!ctx.ports.includes(port))
463
+ return input;
464
+ 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;
465
+ }
466
+ function rewriteSrcset(value, to) {
467
+ let changed = !1, candidates = value.split(",").map((candidate) => {
468
+ let [url = "", ...descriptors] = candidate.trim().split(/\s+/), rewritten = to(url);
469
+ if (rewritten !== url)
470
+ changed = !0;
471
+ return [rewritten, ...descriptors].join(" ");
472
+ });
473
+ return changed ? candidates.join(", ") : value;
474
+ }
475
+ function shimSource() {
476
+ return `(() => {
477
+ if (window.__tesserShim) return;
478
+ window.__tesserShim = true;
479
+ const script = document.currentScript;
480
+ const params = new URL(script.src).searchParams;
481
+ script.remove();
482
+ const host = params.get("host");
483
+ const ports = (params.get("ports") || "").split(",").filter(Boolean).map(Number);
484
+ const port = Number(params.get("port"));
485
+ const rewriteUrl = ${rewriteUrl.toString()};
486
+ const to = (input, ws) => rewriteUrl(String(input), ws, { href: location.href, host, ports, port });
487
+ const rewriteSrcset = ${rewriteSrcset.toString()};
488
+ const fetch = window.fetch;
489
+ window.fetch = function (input, init) {
490
+ if (!(input instanceof Request)) return fetch.call(this, to(input, false), init);
491
+ const url = to(input.url, false);
492
+ if (url === input.url) return fetch.call(this, input, init);
493
+ const { method, headers, credentials, cache, redirect, referrer, referrerPolicy, integrity, keepalive, signal } = input;
494
+ const rebuilt = { method, headers, credentials, cache, redirect, referrer, referrerPolicy, integrity, keepalive, signal };
495
+ if (method === "GET" || method === "HEAD") return fetch.call(this, new Request(url, rebuilt), init);
496
+ return input.blob().then((body) => fetch.call(this, new Request(url, { ...rebuilt, body }), init));
497
+ };
498
+ const open = XMLHttpRequest.prototype.open;
499
+ XMLHttpRequest.prototype.open = function (method, url, ...rest) {
500
+ return open.call(this, method, to(url, false), ...rest);
501
+ };
502
+ const beacon = navigator.sendBeacon?.bind(navigator);
503
+ if (beacon) navigator.sendBeacon = (url, data) => beacon(to(url, false), data);
504
+ const wrap = (name, ws) => {
505
+ if (!window[name]) return;
506
+ window[name] = new Proxy(window[name], {
507
+ construct(target, args, newTarget) {
508
+ args[0] = to(args[0], ws);
509
+ return Reflect.construct(target, args, newTarget);
510
+ },
511
+ });
512
+ };
513
+ wrap("WebSocket", true);
514
+ wrap("EventSource", false);
515
+ const loads = [
516
+ ["HTMLImageElement", ["src", "srcset"]],
517
+ ["HTMLSourceElement", ["src", "srcset"]],
518
+ ["HTMLScriptElement", ["src"]],
519
+ ["HTMLIFrameElement", ["src"]],
520
+ ["HTMLMediaElement", ["src"]],
521
+ ["HTMLVideoElement", ["poster"]],
522
+ ["HTMLEmbedElement", ["src"]],
523
+ ["HTMLTrackElement", ["src"]],
524
+ ["HTMLLinkElement", ["href"]],
525
+ ].filter(([name]) => window[name]);
526
+ const fix = (name, value) => name === "srcset" ? rewriteSrcset(String(value), (url) => to(url, false)) : to(value, false);
527
+ const loaded = (element, name) => loads.some(([type, names]) => element instanceof window[type] && names.includes(name));
528
+ const setAttribute = Element.prototype.setAttribute;
529
+ Element.prototype.setAttribute = function (name, value) {
530
+ const lower = String(name).toLowerCase();
531
+ return setAttribute.call(this, name, loaded(this, lower) ? fix(lower, value) : value);
532
+ };
533
+ for (const [type, names] of loads) {
534
+ for (const name of names) {
535
+ const proto = window[type].prototype;
536
+ const descriptor = Object.getOwnPropertyDescriptor(proto, name);
537
+ if (!descriptor?.set) continue;
538
+ Object.defineProperty(proto, name, { ...descriptor, set(value) { descriptor.set.call(this, fix(name, value)); } });
539
+ }
540
+ }
541
+ const selector = "img, source, script, iframe, video, audio, embed, track, link";
542
+ const sweep = (element) => {
543
+ for (const name of ["src", "srcset", "poster", "href"]) {
544
+ const value = element.getAttribute(name);
545
+ if (value !== null && loaded(element, name)) {
546
+ const rewritten = fix(name, value);
547
+ if (rewritten !== value) setAttribute.call(element, name, rewritten);
548
+ }
549
+ }
550
+ };
551
+ new MutationObserver((records) => {
552
+ for (const record of records) {
553
+ for (const node of record.addedNodes) {
554
+ if (!(node instanceof Element)) continue;
555
+ if (node.matches(selector)) sweep(node);
556
+ for (const element of node.querySelectorAll(selector)) sweep(element);
557
+ }
558
+ }
559
+ }).observe(document, { childList: true, subtree: true });
560
+ const follow = (element, attribute) => {
561
+ const value = element?.getAttribute(attribute);
562
+ if (!value) return;
563
+ const rewritten = to(value, false);
564
+ if (rewritten !== value) element.setAttribute(attribute, rewritten);
565
+ };
566
+ const click = (event) => follow(event.target instanceof Element ? event.target.closest("a[href], area[href]") : null, "href");
567
+ document.addEventListener("click", click, true);
568
+ document.addEventListener("auxclick", click, true);
569
+ document.addEventListener("submit", (event) => {
570
+ follow(event.target, "action");
571
+ follow(event.submitter, "formaction");
572
+ }, true);
573
+ })();
574
+ `;
575
+ }
576
+ function shimTag(params, nonce) {
577
+ let query = new URLSearchParams({ host: params.host, ports: params.ports.join(","), port: String(params.port) });
578
+ return `<script${nonce ? ` nonce="${nonce}"` : ""} src="/__tesser/shim.js?${query.toString().replace(/&/g, "&amp;")}"></script>`;
579
+ }
580
+
581
+ // src/core.ts
582
+ import { Clock as Clock2, Context, Duration, Effect as Effect2, Schedule, Schema } from "effect";
583
+
584
+ // src/route.ts
585
+ var PORT_PATH = /^\/__tesser\/(?:([a-z0-9-]+(?:\.[a-z0-9-]+)*)\.)?(\d{1,5})(\/.*)?$/;
586
+ function parseTarget(gatewayHost, url) {
587
+ let bare = url.hostname.toLowerCase().replace(/\.$/, ""), suffix = `.${gatewayHost.toLowerCase()}`;
588
+ if (!bare.endsWith(suffix))
589
+ return null;
590
+ let labels = bare.slice(0, -suffix.length).split("."), label = labels[labels.length - 1], parsed = parseGatewayLabel(label);
591
+ if (parsed === null)
592
+ return null;
593
+ 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);
594
+ if (match === null)
595
+ return { ...base, port: null, path: `${url.pathname}${url.search}` };
596
+ return { ...base, sub: match[1] ?? base.sub, port: Number(match[2]), path: `${match[3] ?? "/"}${url.search}` };
597
+ }
598
+ function parseUrl(raw, base) {
599
+ try {
600
+ return new URL(raw, base);
601
+ } catch {
602
+ return null;
603
+ }
604
+ }
605
+ function presentedHost(sub, port) {
606
+ return `${sub === "" ? "localhost" : `${sub}.localhost`}:${port}`;
607
+ }
608
+ function portOrigin(protocol, target, port, first) {
609
+ return `${protocol}//${target.sub === "" ? "" : `${target.sub}.`}${gatewayLabel(target.boxId, port === first ? null : port)}.${target.gateway}`;
610
+ }
611
+
612
+ // src/core.ts
613
+ class BoxNotShared extends Schema.TaggedError()("BoxNotShared", { boxId: Schema.String }) {
614
+ }
615
+
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 };
643
+ }
644
+ function upstreamHeaders(request, target, port, root) {
645
+ let headers = withoutHopByHop(request.headers);
646
+ for (let name of [...headers.keys()])
647
+ if (name.startsWith("cf-access-") || name.startsWith("x-forwarded-"))
648
+ headers.delete(name);
649
+ headers.delete("forwarded"), headers.delete("x-tesser-viewer"), headers.delete("x-tesser-ticket");
650
+ let cookie = headers.get("cookie")?.split(";").filter((c) => !GATEWAY_COOKIES.has(c.trim().split("=")[0])).join(";").trim();
651
+ if (cookie)
652
+ headers.set("cookie", cookie);
653
+ else
654
+ headers.delete("cookie");
655
+ 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");
656
+ let origin = request.headers.get("origin");
657
+ if (origin !== null && parseUrl(origin)?.host === new URL(request.url).host)
658
+ headers.set("origin", `http://${presentedHost(target.pageSub, root)}`);
659
+ return headers;
660
+ }
661
+ function isHtmlDocument(request, response) {
662
+ if (!wantsDocument(request) || response.status < 200 || response.status >= 300)
663
+ return !1;
664
+ return (response.headers.get("content-type")?.toLowerCase() ?? "").includes("text/html") && !response.headers.has("content-range");
665
+ }
666
+ function downstream(request, response, upstreamUrl, ctx, shownOrigin) {
667
+ let headers = withoutHopByHop(response.headers);
668
+ headers.delete("content-encoding"), headers.delete("content-length");
669
+ let location = headers.get("location");
670
+ if (location !== null)
671
+ headers.set("location", rewriteUrl(parseUrl(location) === null ? new URL(location, upstreamUrl).href : location, !1, ctx));
672
+ let origin = request.headers.get("origin");
673
+ if (origin !== null && shownOrigin !== null && headers.get("access-control-allow-origin") === shownOrigin)
674
+ headers.set("access-control-allow-origin", origin);
675
+ let init = { status: response.status, statusText: response.statusText, headers };
676
+ if (!isHtmlDocument(request, response))
677
+ return new Response(response.body, init);
678
+ let csp = headers.get("content-security-policy");
679
+ if (csp !== null)
680
+ headers.set("content-security-policy", allowSelfScripts(csp));
681
+ let tag = shimTag({ host: ctx.host, ports: ctx.ports, port: ctx.port }, csp === null ? void 0 : scriptNonce(csp)), placed = !1, inject = {
682
+ element(element) {
683
+ if (placed)
684
+ return;
685
+ placed = !0, element.prepend(tag, { html: !0 });
686
+ }
687
+ }, link = (attribute, rewrite = (value) => rewriteUrl(value, !1, ctx)) => ({
688
+ element(element) {
689
+ let value = element.getAttribute(attribute);
690
+ if (value === null)
691
+ return;
692
+ let rewritten = rewrite(value);
693
+ if (rewritten !== value)
694
+ element.setAttribute(attribute, rewritten);
695
+ }
696
+ });
697
+ 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
+ }
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));
749
+ }
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
+ })));
758
+
759
+ // src/tunnel.ts
760
+ var UPGRADE_TIMEOUT_MS = 30000, NULL_BODY = /* @__PURE__ */ new Set([101, 204, 205, 304]);
761
+
762
+ class StreamRefused extends Error {
763
+ }
764
+
765
+ class Tunnel {
766
+ socket;
767
+ inUse;
768
+ streams = /* @__PURE__ */ new Map;
769
+ constructor(socket, inUse) {
770
+ this.socket = socket;
771
+ this.inUse = inUse;
772
+ }
773
+ send(frame) {
774
+ let ws = this.socket();
775
+ if (ws === null)
776
+ return !1;
777
+ try {
778
+ return ws.send(encodeFrame(frame)), !0;
779
+ } catch {
780
+ return !1;
781
+ }
782
+ }
783
+ newStream() {
784
+ for (;; ) {
785
+ let id = crypto.getRandomValues(new Uint32Array(1))[0];
786
+ if (id !== 0 && !this.streams.has(id) && !this.inUse(id))
787
+ return id;
788
+ }
789
+ }
790
+ async fetch(port, request) {
791
+ 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 });
792
+ this.streams.set(stream, s);
793
+ let headers = [...request.headers];
794
+ if (!this.send({ type: "open", stream, port, method: request.method, path: `${url.pathname}${url.search}`, headers, ws: !1 }))
795
+ throw this.streams.delete(stream), new StreamRefused("the box is not connected");
796
+ request.signal.addEventListener("abort", () => this.abort(stream, "the viewer went away")), this.pump(stream, s, request.body);
797
+ let h = await head;
798
+ if (NULL_BODY.has(h.status) || request.method === "HEAD")
799
+ return this.abort(stream, "no body expected"), new Response(null, { status: h.status, statusText: h.statusText, headers: h.headers });
800
+ let body = new ReadableStream({
801
+ start: (controller) => {
802
+ s.controller = controller;
803
+ for (let chunk of s.buffered.splice(0))
804
+ controller.enqueue(chunk);
805
+ if (s.closed)
806
+ controller.close();
807
+ },
808
+ pull: () => this.credit(stream, s),
809
+ cancel: () => this.abort(stream, "the viewer stopped reading")
810
+ }, { highWaterMark: MUX_WINDOW, size: (chunk) => chunk.byteLength });
811
+ return new Response(body, { status: h.status, statusText: h.statusText, headers: h.headers });
812
+ }
813
+ async upgrade(port, path, headers) {
814
+ let stream = this.newStream(), head = new Promise((resolve, reject) => this.streams.set(stream, { kind: "ws", resolve, reject }));
815
+ if (!this.send({ type: "open", stream, port, method: "GET", path, headers, ws: !0 }))
816
+ throw this.streams.delete(stream), new StreamRefused("the box is not connected");
817
+ let timeout = setTimeout(() => {
818
+ this.fail(stream, new StreamRefused("the app did not answer the websocket upgrade")), this.send({ type: "reset", stream, reason: "the upgrade timed out" });
819
+ }, UPGRADE_TIMEOUT_MS);
820
+ try {
821
+ return { stream, head: await head };
822
+ } finally {
823
+ clearTimeout(timeout);
824
+ }
825
+ }
826
+ async pump(stream, s, body) {
827
+ if (body === null) {
828
+ this.send({ type: "end", stream });
829
+ return;
830
+ }
831
+ let reader = body.getReader();
832
+ try {
833
+ for (;; ) {
834
+ let { done, value } = await reader.read();
835
+ if (done)
836
+ break;
837
+ for (let at = 0;at < value.byteLength; at += MUX_CHUNK) {
838
+ let piece = value.subarray(at, at + MUX_CHUNK);
839
+ while (s.window < piece.byteLength) {
840
+ if (this.streams.get(stream) !== s)
841
+ return;
842
+ await new Promise((resolve) => s.onCredit = resolve);
843
+ }
844
+ if (this.streams.get(stream) !== s)
845
+ return;
846
+ s.window -= piece.byteLength, this.send({ type: "data", stream, data: piece });
847
+ }
848
+ }
849
+ if (this.streams.get(stream) === s)
850
+ this.send({ type: "end", stream });
851
+ } catch {
852
+ this.abort(stream, "the viewer's request body failed");
853
+ }
854
+ }
855
+ credit(stream, s) {
856
+ if (s.owed === 0 || this.streams.get(stream) !== s)
857
+ return;
858
+ this.send({ type: "credit", stream, bytes: s.owed }), s.owed = 0;
859
+ }
860
+ abort(stream, reason) {
861
+ let s = this.streams.get(stream);
862
+ if (s === void 0)
863
+ return;
864
+ if (this.streams.delete(stream), s.kind === "http")
865
+ s.onCredit?.(), s.head?.reject(new StreamRefused(reason));
866
+ this.send({ type: "reset", stream, reason });
867
+ }
868
+ fail(stream, err) {
869
+ let s = this.streams.get(stream);
870
+ if (s === void 0)
871
+ return;
872
+ if (this.streams.delete(stream), s.kind === "ws")
873
+ return s.reject(err);
874
+ if (s.onCredit?.(), s.head !== null)
875
+ return s.head.reject(err);
876
+ try {
877
+ s.controller?.error(err);
878
+ } catch {}
879
+ }
880
+ onFrame(frame) {
881
+ let s = this.streams.get(frame.stream);
882
+ if (s === void 0)
883
+ return !1;
884
+ if (s.kind === "ws") {
885
+ if (frame.type === "head")
886
+ this.streams.delete(frame.stream), s.resolve(frame);
887
+ else if (frame.type === "reset")
888
+ this.streams.delete(frame.stream), s.reject(new StreamRefused(frame.reason));
889
+ return !0;
890
+ }
891
+ switch (frame.type) {
892
+ case "head":
893
+ return s.head?.resolve(frame), s.head = null, !0;
894
+ case "data":
895
+ if (s.controller === null)
896
+ return s.buffered.push(frame.data), s.owed += frame.data.byteLength, !0;
897
+ try {
898
+ s.controller.enqueue(frame.data);
899
+ } catch {
900
+ return !0;
901
+ }
902
+ if (s.owed += frame.data.byteLength, (s.controller.desiredSize ?? 0) > 0)
903
+ this.credit(frame.stream, s);
904
+ return !0;
905
+ case "end":
906
+ this.streams.delete(frame.stream), s.closed = !0;
907
+ try {
908
+ s.controller?.close();
909
+ } catch {}
910
+ return !0;
911
+ case "reset":
912
+ return this.fail(frame.stream, new StreamRefused(frame.reason)), !0;
913
+ case "credit":
914
+ return s.window += frame.bytes, s.onCredit?.(), s.onCredit = null, !0;
915
+ default:
916
+ return !0;
917
+ }
918
+ }
919
+ closeAll(reason) {
920
+ for (let stream of [...this.streams.keys()])
921
+ this.fail(stream, new StreamRefused(reason));
922
+ }
923
+ }
924
+
925
+ // src/relay.ts
926
+ 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({
927
+ org: Schema2.String,
928
+ box: Schema2.String,
929
+ name: Schema2.String,
930
+ sharedBy: Schema2.NullOr(Schema2.String),
931
+ ports: Schema2.Array(Schema2.Number),
932
+ exp: Schema2.Number
933
+ }), BoxResponse = Schema2.Struct({
934
+ box: Schema2.Struct({
935
+ boxId: Schema2.String,
936
+ name: Schema2.String,
937
+ sharedBy: Schema2.NullOr(Schema2.String),
938
+ privateIp: Schema2.String,
939
+ ports: Schema2.Array(Schema2.Number),
940
+ power: Schema2.Literals(["launching", "awake", "asleep", "removing", "gone"]),
941
+ app: Schema2.NullOr(Schema2.Literals(["none", "running", "unsupervised", "exited"]))
942
+ })
943
+ }), ErrorBody = Schema2.Struct({ error: Schema2.Struct({ message: Schema2.String }) });
944
+ function plain(status, text) {
945
+ return new Response(`${text}
946
+ `, { status, headers: { "content-type": "text/plain; charset=utf-8", "cache-control": "no-store" } });
947
+ }
948
+
949
+ class BoxRelay extends DurableObject {
950
+ tunnel = new Tunnel(() => this.boxSocket(), (stream) => this.ctx.getWebSockets(`v:${stream}`).length > 0);
951
+ waking = null;
952
+ connected = [];
953
+ verifiedAt = /* @__PURE__ */ new Map;
954
+ unstored = /* @__PURE__ */ new Map;
955
+ failingSince = /* @__PURE__ */ new Map;
956
+ constructor(ctx, env) {
957
+ super(ctx, env);
958
+ ctx.setWebSocketAutoResponse(new WebSocketRequestResponsePair(MUX_PING, MUX_PONG));
959
+ }
960
+ boxSocket() {
961
+ return this.ctx.getWebSockets("box").find((ws) => ws.deserializeAttachment()?.replaced !== !0) ?? null;
962
+ }
963
+ live() {
964
+ let ws = this.boxSocket();
965
+ if (ws === null)
966
+ return null;
967
+ let { ticket } = ws.deserializeAttachment();
968
+ if (ticket.exp * 1000 > Date.now())
969
+ return ticket;
970
+ return this.retire(ws, 4001, "share expired"), null;
971
+ }
972
+ retire(ws, code, reason) {
973
+ ws.serializeAttachment({ ...ws.deserializeAttachment(), replaced: !0 }), this.tunnel.closeAll(reason);
974
+ for (let viewer of this.ctx.getWebSockets("viewer"))
975
+ try {
976
+ viewer.close(1012, reason.slice(0, 120));
977
+ } catch {}
978
+ try {
979
+ ws.close(code, reason);
980
+ } catch {}
981
+ }
982
+ async fetch(request) {
983
+ let ticket = request.headers.get(TICKET_HEADER);
984
+ if (ticket !== null) {
985
+ let decoded = Schema2.decodeUnknownOption(Ticket)(JSON.parse(ticket));
986
+ if (decoded._tag === "None" || !this.admits(decoded.value))
987
+ return plain(403, "this box answers for another org here");
988
+ return this.acceptBox(decoded.value);
989
+ }
990
+ let handled = await Effect3.runPromise(handle(request).pipe(Effect3.provide(this.layer(this.viewer(request)))));
991
+ if (handled._tag === "response")
992
+ return handled.response;
993
+ return this.bridge(request, handled);
994
+ }
995
+ acceptBox(ticket) {
996
+ let previous = this.boxSocket();
997
+ if (previous !== null)
998
+ this.retire(previous, 4000, "replaced by a newer connection");
999
+ let [client, server] = Object.values(new WebSocketPair);
1000
+ this.ctx.acceptWebSocket(server, ["box"]), server.serializeAttachment({ ticket }), this.waking = null, this.verifiedAt.clear(), this.failingSince.clear();
1001
+ for (let resolve of this.connected.splice(0))
1002
+ resolve();
1003
+ return this.ctx.waitUntil(this.armAlarm(ticket)), new Response(null, { status: 101, webSocket: client });
1004
+ }
1005
+ socketViewer(ws) {
1006
+ let { stream, viewer } = ws.deserializeAttachment();
1007
+ return { stream, viewer: viewer ?? this.unstored.get(stream) ?? null };
1008
+ }
1009
+ viewers() {
1010
+ return new Set(this.ctx.getWebSockets("viewer").flatMap((ws) => this.socketViewer(ws).viewer ?? []));
1011
+ }
1012
+ async armAlarm(ticket) {
1013
+ let want = this.viewers().size > 0 ? Math.min(ticket.exp * 1000, Date.now() + RECHECK_MS) : ticket.exp * 1000, pending = await this.ctx.storage.getAlarm();
1014
+ if (pending === null || pending <= Date.now() || want < pending)
1015
+ await this.ctx.storage.setAlarm(want);
1016
+ }
1017
+ closeViewer(viewer, reason) {
1018
+ for (let ws of this.ctx.getWebSockets("viewer")) {
1019
+ let { stream, viewer: who } = this.socketViewer(ws);
1020
+ if (who !== viewer)
1021
+ continue;
1022
+ this.tunnel.send({ type: "close", stream, code: 1008, reason });
1023
+ try {
1024
+ ws.close(1008, reason);
1025
+ } catch {}
1026
+ }
1027
+ }
1028
+ verdict(viewer, answer, now, open) {
1029
+ if (answer?.status === 200) {
1030
+ for (let [key, at] of this.verifiedAt)
1031
+ if (now - at > STALE_MS)
1032
+ this.verifiedAt.delete(key);
1033
+ return this.verifiedAt.set(viewer, now), this.failingSince.delete(viewer), "ok";
1034
+ }
1035
+ if (answer?.status === 404 || answer?.status === 403)
1036
+ this.verifiedAt.delete(viewer), this.failingSince.delete(viewer);
1037
+ let box = this.boxSocket();
1038
+ if (answer?.status === 404) {
1039
+ if (box !== null)
1040
+ this.retire(box, 4003, "share revoked");
1041
+ return "revoked";
1042
+ }
1043
+ if (answer?.status === 403)
1044
+ return this.closeViewer(viewer, "access ended"), "refused";
1045
+ let verified = this.verifiedAt.get(viewer);
1046
+ if (open && verified === void 0 && !this.failingSince.has(viewer))
1047
+ this.failingSince.set(viewer, now);
1048
+ let since = verified ?? (open ? this.failingSince.get(viewer) : 0);
1049
+ if (now - since > STALE_MS) {
1050
+ if (open)
1051
+ this.closeViewer(viewer, "tesser can't confirm access right now");
1052
+ return "stale";
1053
+ }
1054
+ return "unknown";
1055
+ }
1056
+ async alarm() {
1057
+ let ws = this.boxSocket();
1058
+ if (ws === null)
1059
+ return;
1060
+ let { ticket } = ws.deserializeAttachment();
1061
+ if (ticket.exp * 1000 <= Date.now()) {
1062
+ this.retire(ws, 4001, "share expired");
1063
+ return;
1064
+ }
1065
+ for (let viewer of this.viewers()) {
1066
+ let answer = await this.lookupBox(ticket.box, viewer).catch(() => null);
1067
+ if (this.verdict(viewer, answer, Date.now(), !0) === "revoked")
1068
+ return;
1069
+ }
1070
+ await this.armAlarm(ticket);
1071
+ }
1072
+ async bridge(request, target) {
1073
+ let headers = [...target.headers].filter(([name]) => !/^sec-websocket-(key|version|extensions)$/i.test(name)), upgraded;
1074
+ try {
1075
+ upgraded = await this.tunnel.upgrade(target.port, target.path, headers);
1076
+ } catch (err) {
1077
+ return plain(502, `${target.box.name} refused the websocket: ${err instanceof Error ? err.message : String(err)}`);
1078
+ }
1079
+ if (upgraded.head.status !== 101)
1080
+ return plain(502, `${target.box.name} answered ${upgraded.head.status} to the websocket`);
1081
+ let [client, server] = Object.values(new WebSocketPair);
1082
+ this.ctx.acceptWebSocket(server, ["viewer", `v:${upgraded.stream}`]);
1083
+ let viewer = this.viewer(request);
1084
+ try {
1085
+ server.serializeAttachment({ stream: upgraded.stream, viewer });
1086
+ } catch {
1087
+ if (server.serializeAttachment({ stream: upgraded.stream, viewer: null }), viewer !== null)
1088
+ this.unstored.set(upgraded.stream, viewer);
1089
+ }
1090
+ let ticket = this.live();
1091
+ if (ticket !== null)
1092
+ await this.armAlarm(ticket);
1093
+ let protocol = upgraded.head.headers.find(([name]) => name.toLowerCase() === "sec-websocket-protocol")?.[1];
1094
+ return new Response(null, { status: 101, webSocket: client, headers: protocol === void 0 ? {} : { "sec-websocket-protocol": protocol } });
1095
+ }
1096
+ webSocketMessage(ws, message) {
1097
+ if (this.ctx.getTags(ws).includes("box")) {
1098
+ if (typeof message === "string")
1099
+ return;
1100
+ let frame;
1101
+ try {
1102
+ frame = decodeFrame(new Uint8Array(message));
1103
+ } catch {
1104
+ return;
1105
+ }
1106
+ if (this.tunnel.onFrame(frame))
1107
+ return;
1108
+ let viewer = this.ctx.getWebSockets(`v:${frame.stream}`)[0];
1109
+ if (viewer === void 0) {
1110
+ if (frame.type === "text" || frame.type === "binary")
1111
+ this.tunnel.send({ type: "close", stream: frame.stream, code: 1001, reason: "the viewer went away" });
1112
+ return;
1113
+ }
1114
+ try {
1115
+ if (frame.type === "text")
1116
+ viewer.send(frame.text);
1117
+ else if (frame.type === "binary")
1118
+ viewer.send(frame.data);
1119
+ else if (frame.type === "close")
1120
+ viewer.close(frame.code, frame.reason);
1121
+ else if (frame.type === "reset")
1122
+ viewer.close(1011, frame.reason.slice(0, 120));
1123
+ } catch {}
1124
+ return;
1125
+ }
1126
+ let { stream } = ws.deserializeAttachment();
1127
+ this.tunnel.send(typeof message === "string" ? { type: "text", stream, text: message } : { type: "binary", stream, data: new Uint8Array(message) });
1128
+ }
1129
+ webSocketClose(ws, code, reason) {
1130
+ if (this.ctx.getTags(ws).includes("box")) {
1131
+ if (ws.deserializeAttachment().replaced === !0)
1132
+ return;
1133
+ this.retire(ws, 1000, "box hung up");
1134
+ return;
1135
+ }
1136
+ let { stream } = ws.deserializeAttachment();
1137
+ this.unstored.delete(stream);
1138
+ let sendable = code === 1005 ? 1000 : code === 1004 || code === 1006 || code === 1015 || code < 1000 || code > 4999 ? 1001 : code;
1139
+ this.tunnel.send({ type: "close", stream, code: sendable, reason: reason.slice(0, 120) });
1140
+ }
1141
+ webSocketError(ws) {
1142
+ this.webSocketClose(ws, 1006, "socket error");
1143
+ }
1144
+ ask(call) {
1145
+ return Effect3.tryPromise({ try: call, catch: (err) => new DirectoryUnavailable({ reason: err instanceof Error ? err.message : String(err) }) });
1146
+ }
1147
+ layer(viewer) {
1148
+ let self = this, remote = Effect3.fn("Relay.remote")(function* (boxId) {
1149
+ let { status, body } = yield* self.ask(() => self.lookupBox(boxId, viewer));
1150
+ if (status === 403 || status === 404)
1151
+ return yield* new BoxNotShared({ boxId });
1152
+ if (status !== 200) {
1153
+ let message = Schema2.decodeUnknownOption(ErrorBody)(body);
1154
+ return yield* new DirectoryUnavailable({ reason: message._tag === "Some" ? message.value.error.message : `tesser answered ${status}` });
1155
+ }
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) {
1173
+ let now = yield* Clock3.currentTimeMillis, current = self.waking;
1174
+ if (current?.inflight)
1175
+ return;
1176
+ if (current?.failure && !retry)
1177
+ return;
1178
+ if (current !== null && current.failure === null && now - current.kickedAt < WAKE_RETRY_MS)
1179
+ 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 };
1181
+ self.waking = waking;
1182
+ let kicked = yield* self.ask(() => self.wakeBox(boxId, viewer)).pipe(Effect3.result, Effect3.ensuring(Effect3.sync(() => waking.inflight = !1)));
1183
+ if (kicked._tag === "Failure")
1184
+ return;
1185
+ let { status, body } = kicked.success;
1186
+ if (status < 300 || status >= 500 || RETRYABLE.has(status))
1187
+ return;
1188
+ let message = Schema2.decodeUnknownOption(ErrorBody)(body);
1189
+ waking.failure = message._tag === "Some" ? message.value.error.message : `tesser answered ${status}`;
1190
+ }), status = Effect3.fn("Relay.status")(function* (_boxId) {
1191
+ let now = yield* Clock3.currentTimeMillis, w = self.waking;
1192
+ if (w === null || w.failure === null && now - w.since > Duration2.toMillis(WAKE_TIMEOUT))
1193
+ return null;
1194
+ 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")
1198
+ yield* dialed(boxId, DIAL_GRACE, "the box is up but hasn't connected to this gateway; its boxd may need an update");
1199
+ else {
1200
+ yield* wake(boxId, !1);
1201
+ let failure = self.waking?.failure;
1202
+ if (failure)
1203
+ return yield* new WakeFailed({ boxId, reason: failure });
1204
+ yield* dialed(boxId, WAKE_TIMEOUT, "it didn't wake up within 5 minutes");
1205
+ }
1206
+ 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 }));
1209
+ }
1210
+ }
1211
+
1212
+ // src/cloudflare.ts
1213
+ var ACCESS_HEADER = "cf-access-jwt-assertion", verifyTicket = Effect4.fn("Gateway.verifyTicket")(function* (env, request) {
1214
+ let token = request.headers.get("authorization")?.match(/^Bearer (\S+)$/)?.[1];
1215
+ if (token === void 0)
1216
+ return null;
1217
+ let issuer = env.TESSER_URL.replace(/\/$/, ""), claims = yield* verifyRs256(token, issuer, `${issuer}/.well-known/jwks.json`);
1218
+ if (claims === null || !audienceIncludes(claims.aud, `https://${env.GATEWAY_HOST}`) || claims.org !== env.TESSER_ORG)
1219
+ return null;
1220
+ if (claims.sub !== `box:${env.TESSER_ORG}:${String(claims.box)}`)
1221
+ return null;
1222
+ return yield* Schema3.decodeUnknownEffect(Ticket)(claims).pipe(Effect4.option);
1223
+ }), verifyViewer = Effect4.fn("Gateway.verifyViewer")(function* (env, request) {
1224
+ let token = request.headers.get(ACCESS_HEADER);
1225
+ if (token === null)
1226
+ return null;
1227
+ let issuer = `https://${env.ACCESS_TEAM}`, claims = yield* verifyRs256(token, issuer, `${issuer}/cdn-cgi/access/certs`);
1228
+ return claims !== null && audienceIncludes(claims.aud, env.ACCESS_AUD) ? claims : null;
1229
+ }), cloudflare_default = {
1230
+ async fetch(request, env) {
1231
+ let url = new URL(request.url), apex = url.hostname.toLowerCase() === env.GATEWAY_HOST.toLowerCase();
1232
+ if (apex && url.pathname === GATEWAY_DIAL_PATH) {
1233
+ if (request.headers.get("upgrade")?.toLowerCase() !== "websocket")
1234
+ return plain(426, "boxes dial this with a websocket");
1235
+ let ticket = await Effect4.runPromise(verifyTicket(env, request));
1236
+ if (ticket === null || ticket._tag === "None")
1237
+ return plain(401, "a box dials in with a share ticket signed by tesser");
1238
+ let headers = new Headers(request.headers);
1239
+ 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 }));
1240
+ }
1241
+ if (apex && url.pathname === "/__tesser/health")
1242
+ return plain(200, "ok");
1243
+ let boxId = gatewayHostBox(env.GATEWAY_HOST, url.hostname);
1244
+ if (boxId === null)
1245
+ return new Response(viewerPage({ kind: "not-shared" }), { status: 404, headers: STATUS_HEADERS });
1246
+ if (await Effect4.runPromise(verifyViewer(env, request)) === null)
1247
+ return plain(403, "this gateway only answers requests Cloudflare Access let through");
1248
+ let headers = new Headers(request.headers);
1249
+ return headers.delete(TICKET_HEADER), env.BOXES.get(env.BOXES.idFromName(boxId)).fetch(new Request(request, { headers }));
1250
+ }
1251
+ };
1252
+
1253
+ class GatewayBoxes extends BoxRelay {
1254
+ host() {
1255
+ return this.env.GATEWAY_HOST;
1256
+ }
1257
+ viewer(request) {
1258
+ return request.headers.get(ACCESS_HEADER);
1259
+ }
1260
+ admits(ticket) {
1261
+ return ticket.org === this.env.TESSER_ORG;
1262
+ }
1263
+ lookupBox(boxId, viewer) {
1264
+ return this.tesser("GET", `/gateway/boxes/${boxId}`, viewer);
1265
+ }
1266
+ wakeBox(boxId, viewer) {
1267
+ return this.tesser("POST", `/gateway/boxes/${boxId}/wake`, viewer);
1268
+ }
1269
+ async tesser(method, path, jwt) {
1270
+ 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);
1271
+ return { status: res.status, body };
1272
+ }
1273
+ }
1274
+ export {
1275
+ GatewayBoxes,
1276
+ cloudflare_default as default
1277
+ };
package/package.json CHANGED
@@ -1,6 +1,29 @@
1
1
  {
2
2
  "name": "@tesser-sh/gateway",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.53.2",
4
+ "description": "Tesser's share gateway as a Cloudflare Worker and Durable Object, deployed in your own account",
5
+ "license": "MIT",
6
+ "type": "module",
7
+ "exports": {
8
+ ".": {
9
+ "types": "./index.d.ts",
10
+ "default": "./index.js"
11
+ }
12
+ },
13
+ "files": [
14
+ "index.js",
15
+ "index.d.ts",
16
+ "LICENSE"
17
+ ],
18
+ "dependencies": {
19
+ "effect": "^4.0.5"
20
+ },
21
+ "peerDependencies": {
22
+ "@cloudflare/workers-types": ">=4"
23
+ },
24
+ "peerDependenciesMeta": {
25
+ "@cloudflare/workers-types": {
26
+ "optional": true
27
+ }
28
+ }
29
+ }
package/README.md DELETED
@@ -1,3 +0,0 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.