mirror of
https://github.com/tiennm99/rplace.git
synced 2026-10-11 03:13:48 +00:00
- resolveIdentity prefers an opaque rplace_id cookie; falls back to a
cf-connecting-ip hash; in production a request with neither now returns
500 no_identity instead of bucketing all such traffic together
- /api/canvas issues Set-Cookie when no cookie is present so subsequent
requests escape NAT-shared IP buckets (mobile/CGNAT users)
- DO maintains an in-memory monotonic broadcast counter; broadcast frames
carry { seq } so the client can detect missed pixels and refetch
- client tracks lastSeq, refetches on gap, resets on every (re)connect
NAT/CGNAT users previously shared a single 1Hz bucket per egress IP. With
cookie identity they each get their own bucket. Cookie is HttpOnly, Secure,
SameSite=Lax, 1y Max-Age. Stripped/cleared cookies fall through to IP.
The seq counter resets on DO hibernation rehydrate; client always refetches
on reconnect, so a reset is indistinguishable from a fresh connect.
Plan: plans/260510-0232-fix-do-migration-followups/phase-02-cookie-ip-identity.md
46 lines
1.4 KiB
JavaScript
46 lines
1.4 KiB
JavaScript
/**
|
|
* Minimal cookie helpers — no external dependency.
|
|
* Used to issue and read the opaque rplace_id rate-limit identity cookie.
|
|
*/
|
|
|
|
/**
|
|
* Parse a Cookie header into a Map<name, value>. Tolerant of missing header,
|
|
* malformed pairs, and surrounding whitespace.
|
|
* @param {string|null|undefined} header
|
|
* @returns {Map<string, string>}
|
|
*/
|
|
export function parseCookie(header) {
|
|
const out = new Map();
|
|
if (!header) return out;
|
|
for (const part of header.split(';')) {
|
|
const eq = part.indexOf('=');
|
|
if (eq <= 0) continue;
|
|
const name = part.slice(0, eq).trim();
|
|
const value = part.slice(eq + 1).trim();
|
|
if (name) out.set(name, value);
|
|
}
|
|
return out;
|
|
}
|
|
|
|
/**
|
|
* Format a Set-Cookie header value.
|
|
* @param {string} name
|
|
* @param {string} value
|
|
* @param {object} [opts]
|
|
* @param {boolean} [opts.httpOnly]
|
|
* @param {boolean} [opts.secure]
|
|
* @param {'Strict'|'Lax'|'None'} [opts.sameSite]
|
|
* @param {string} [opts.path]
|
|
* @param {number} [opts.maxAge] — seconds
|
|
* @returns {string}
|
|
*/
|
|
export function formatSetCookie(name, value, opts = {}) {
|
|
const parts = [`${name}=${value}`];
|
|
if (opts.path) parts.push(`Path=${opts.path}`);
|
|
if (opts.maxAge != null) parts.push(`Max-Age=${opts.maxAge}`);
|
|
if (opts.httpOnly) parts.push('HttpOnly');
|
|
if (opts.secure) parts.push('Secure');
|
|
if (opts.sameSite) parts.push(`SameSite=${opts.sameSite}`);
|
|
return parts.join('; ');
|
|
}
|