Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 | 3x 12x 12x 12x 12x 12x 5x 7x 2x 5x 5x 5x 7x 7x 7x 7x 7x 2x 2x 5x 5x 5x | /**
* `GET /:room`, `GET /:template/form`, `GET /:template/appeditor` —
* Phase 4.1 entry routes.
*
* Legacy (src/main.ls:277-294):
*
* ```livescript
* @get "#BASEPATH/:room": ->
* room = encodeURI(@params.room)
* ui-file = if room is /^=/ then \multi/index.html else \index.html
* if KEY then
* if @query.auth?length
* sendFile(ui-file).call @
* else @response.redirect "#BASEPATH/#{ @params.room }?auth=0"
* else sendFile(ui-file).call @
*
* @get "#BASEPATH/:template/form": ->
* template = encodeURI(@params.template)
* room = template + \_ + new-room!
* delete SC[room]
* {snapshot} <~ SC._get template, IO
* <~ SC._put room, snapshot
* @response.redirect "#BASEPATH/#room/app"
*
* @get "#BASEPATH/:template/appeditor": sendFile \panels.html
* ```
*
* This module owns the two pure-logic parts:
*
* 1. `buildRoomEntry` — decides "serve `/index.html`", "serve
* `/multi/index.html`", or "302 to `?auth=0`" based on the KEY /
* `?auth` query state.
*
* 2. `buildTemplateFormRedirect` — produces the shape legacy emits
* when `/some-template/form` is hit. Full implementation depends
* on Phase 5 Room CRUD — specifically, cloning the template's
* snapshot into `<template>_<newid>`. Since that lands in a
* parallel agent, we STUB for now: return a 503 with a body
* describing the deferral. When Phase 5 wires the DO-to-DO fetch,
* this function's stubbed branch swaps to produce a 302.
*
* `GET /:template/appeditor` is just a static `panels.html` serve — no
* pure logic needed here; the route layer hands off to ASSETS directly.
*
* Encoding note: legacy applies `encodeURI(@params.room)` everywhere.
* Hono decodes the param before we see it, so we re-encode via
* `encodeRoom` to keep storage keys byte-identical with the oracle.
*/
import { encodeRoom, generateRoomId } from '../lib/room-name.ts';
/** Marker statuses for the template-form stub. Exported so the route
* layer can read `TEMPLATE_FORM_STUB_STATUS` as its `init.status`.
*/
export const TEMPLATE_FORM_STUB_STATUS = 503;
export interface BuildRoomEntryOpts {
readonly basepath?: string;
readonly room: string;
/** The `?auth=` query value, undefined if absent. Empty string = present but empty. */
readonly authQuery?: string | undefined;
/** `ETHERCALC_KEY`. When unset, we serve the index page regardless of `authQuery`. */
readonly key?: string;
}
/** The decision carried back from `buildRoomEntry`. */
export type RoomEntryDecision =
| {
readonly kind: 'redirect';
readonly status: 302;
readonly body: string;
readonly headers: {
readonly Location: string;
readonly 'Content-Type': string;
readonly 'Content-Length': string;
readonly Vary: string;
};
}
| {
readonly kind: 'serve';
/** The asset path to hand off to `env.ASSETS.fetch` — `/index.html` or `/multi/index.html`. */
readonly path: string;
};
/**
* Decide how to handle a `GET /:room` request. See file header for the
* legacy semantics. Returns a discriminated union so the route layer
* can either return a 302 or forward to `env.ASSETS.fetch(path)`.
*
* Defense-in-depth: `:room` path params that collide with the reserved
* prefixes (`_rooms`, `_exists`, `_from`, etc.) would already be
* routed to those specific handlers by Hono's trie (static wins over
* `/:room`). We do NOT re-check that here — double-guarding would just
* diverge from Hono's routing table and create subtle bugs if a new
* reserved prefix lands. Ordering is enforced at registration time in
* `src/routes/assets.ts`.
*/
export function buildRoomEntry(opts: BuildRoomEntryOpts): RoomEntryDecision {
const basepath = opts.basepath ?? '';
const encoded = encodeRoom(opts.room);
const isMulti = encoded.startsWith('=');
const path = isMulti ? '/multi/index.html' : '/index.html';
// KEY unset → always serve the index page.
if (!opts.key) {
return { kind: 'serve', path };
}
// KEY set and a non-empty `?auth=…` value was supplied → serve index.
// The auth value itself is validated later on the WS connect path;
// legacy doesn't gate the page serve on HMAC match (§6.4).
if (opts.authQuery !== undefined && opts.authQuery.length > 0) {
return { kind: 'serve', path };
}
// KEY set, no `?auth` → 302 to the view-only sentinel. Express's
// redirect body shape is `Found. Redirecting to <url>` with a
// `text/plain; charset=UTF-8` body; we reproduce it so oracle replays
// stay byte-aligned with the rest of the redirect family.
const location = `${basepath}/${encoded}?auth=0`;
const body = `Found. Redirecting to ${location}`;
return {
kind: 'redirect',
status: 302,
body,
headers: {
Location: location,
'Content-Type': 'text/plain; charset=UTF-8',
'Content-Length': String(body.length),
Vary: 'Accept',
},
};
}
export interface BuildTemplateFormRedirectOpts {
readonly basepath?: string;
readonly template: string;
/** Injectable for deterministic tests. Defaults to `generateRoomId`. */
readonly idGen?: () => string;
/**
* Signals whether Phase 5 Room CRUD is ready. When `false` (default),
* we return the stub. When `true`, we produce the redirect — the
* route layer is expected to have already cloned the snapshot via
* `env.ROOM.get(…).fetch('/_do/clone')` before calling this builder.
*/
readonly phase5Ready?: boolean;
}
export type TemplateFormResult =
| {
readonly status: 302;
readonly body: string;
readonly headers: Readonly<Record<string, string>>;
}
| {
readonly status: typeof TEMPLATE_FORM_STUB_STATUS;
readonly body: string;
readonly headers: Readonly<Record<string, string>>;
};
/**
* Legacy-shape redirect for `/:template/form` → 302 `/<template>_<uuid>/app`.
* The new room id is the template name plus an underscore plus a fresh
* 12-char id — preserving the legacy `_formdata` naming convention used
* by the `submitform` WS command (see §7 item 22).
*
* Behavior controlled by `phase5Ready`:
* - unset / false → 503 stub explaining the dependency
* - true → 302 redirect with Express-style body
*/
export function buildTemplateFormRedirect(
opts: BuildTemplateFormRedirectOpts,
): TemplateFormResult {
const basepath = opts.basepath ?? '';
const template = encodeRoom(opts.template);
const id = (opts.idGen ?? generateRoomId)();
const newRoom = `${template}_${id}`;
if (!opts.phase5Ready) {
const body =
'Template duplication is not yet available — Phase 5 (Room CRUD) ' +
'has not landed. Once DO-to-DO fetches are wired, this endpoint ' +
'will 302 to /' +
newRoom +
'/app.';
return {
status: TEMPLATE_FORM_STUB_STATUS,
body,
headers: {
'Content-Type': 'text/plain; charset=UTF-8',
'Content-Length': String(body.length),
},
};
}
const location = `${basepath}/${newRoom}/app`;
const body = `Found. Redirecting to ${location}`;
return {
status: 302,
body,
headers: {
Location: location,
'Content-Type': 'text/plain; charset=UTF-8',
'Content-Length': String(body.length),
Vary: 'Accept',
},
};
}
|