All files / src/handlers room-redirects.ts

100% Statements 16/16
100% Branches 12/12
100% Functions 1/1
100% Lines 14/14

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                                                                                            13x 13x 13x 13x   4x 4x     2x 2x     2x 2x             5x 5x 3x 2x        
/**
 * `GET /:room/edit`, `/:room/view`, `/:room/app`, and `/:room` (entry).
 *
 * Legacy (src/main.ls:277-305):
 *   - `/:room/edit` → 302 `/<room>?auth=<hmac>`
 *   - `/:room/view` → 302 `/<room>?auth=<hmac>&view=1`
 *   - `/:room/app`  → 302 `/<room>?auth=<hmac>&app=1`
 *   - `/:room` when KEY set and `?auth` is missing → 302 `/<room>?auth=0`
 *   - `/:room` when KEY set and `?auth` present → serve `index.html`
 *   - `/:room` when no KEY → serve `index.html`
 *
 * Under no-KEY identity HMAC (see `auth.ts` + FINDINGS F-03), the `edit`
 * redirect becomes `?auth=<room>` — the oracle recording
 * `misc/get-edit-no-key-redirect.json` confirms this. We preserve it.
 *
 * Pure slice: returns a `RedirectInfo` for the three 302 paths and `null`
 * for `/:room` entry when the raw index page should be served. The Hono
 * glue either returns the redirect or hands off to the assets handler.
 */
import { computeAuth } from '../lib/auth.ts';
import { encodeRoom } from '../lib/room-name.ts';
import type { RedirectInfo } from './new-room.ts';
 
export type RoomMode = 'edit' | 'view' | 'app' | 'entry';
 
export interface BuildRoomRedirectOpts {
  readonly basepath?: string;
  readonly room: string;
  readonly mode: RoomMode;
  /** Undefined/empty → identity HMAC. */
  readonly key?: string;
  /**
   * Only consulted for `mode: 'entry'`. When KEY is set and no `auth` is
   * present, we 302 to `?auth=0`. Otherwise null → serve index.
   */
  readonly authQuery?: string | undefined;
}
 
/**
 * Returns a redirect to the 302 target, or `null` to mean "serve the
 * index page". The caller (Hono glue) handles the null by falling
 * through to the Workers Assets fetch for `index.html`.
 */
export async function buildRoomRedirect(
  opts: BuildRoomRedirectOpts,
): Promise<RedirectInfo | null> {
  const basepath = opts.basepath ?? '';
  const room = encodeRoom(opts.room);
  const base = `${basepath}/${room}`;
  switch (opts.mode) {
    case 'edit': {
      const auth = await computeAuth(opts.key, room);
      return { status: 302, headers: { Location: `${base}?auth=${auth}` } };
    }
    case 'view': {
      const auth = await computeAuth(opts.key, room);
      return { status: 302, headers: { Location: `${base}?auth=${auth}&view=1` } };
    }
    case 'app': {
      const auth = await computeAuth(opts.key, room);
      return { status: 302, headers: { Location: `${base}?auth=${auth}&app=1` } };
    }
    case 'entry': {
      // Legacy: no KEY → always serve index; KEY set → require ?auth=…
      // present (any length, per `@query.auth?length`) else 302 to
      // `?auth=0` (view-only sentinel). Note that legacy does NOT
      // validate the supplied auth here — that happens on the WS path.
      const keyed = !!opts.key;
      if (!keyed) return null;
      if (opts.authQuery !== undefined && opts.authQuery.length > 0) return null;
      return { status: 302, headers: { Location: `${base}?auth=0` } };
    }
  }
}