From 20cc8388c1235dbd7411d6bfbdcccf9ef7cfbd6b Mon Sep 17 00:00:00 2001 From: madmti Date: Thu, 23 Jul 2026 22:07:32 -0400 Subject: [PATCH 1/2] chore: added wrangler.toml --- wrangler.toml | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 wrangler.toml diff --git a/wrangler.toml b/wrangler.toml new file mode 100644 index 0000000..1f7a539 --- /dev/null +++ b/wrangler.toml @@ -0,0 +1,8 @@ +name = "ramolibrelab" +compatibility_date = "2024-09-01" +pages_build_output_dir = "build" + +[[kv_namespaces]] +binding = "LAB_KV" +id = "f2f5863c15c84287bf3221a857f5f3b1" +preview_id = "404143a88e464c519370c47aa65f7827" From 02c27223f7de86200afbafaaca71aab3cefa4a07 Mon Sep 17 00:00:00 2001 From: madmti Date: Thu, 23 Jul 2026 23:03:40 -0400 Subject: [PATCH 2/2] feat: added URL shortener --- .github/workflows/deploy.yml | 2 +- functions/api/lab/[id].js | 28 +++++++++++ functions/api/lab/index.js | 52 +++++++++++++++++++++ src/lib/share.ts | 51 ++++++++++++++++++++ src/routes/(app)/_components/Sidebar.svelte | 28 +++-------- wrangler.toml | 2 +- 6 files changed, 140 insertions(+), 23 deletions(-) create mode 100644 functions/api/lab/[id].js create mode 100644 functions/api/lab/index.js create mode 100644 src/lib/share.ts diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 88eead7..89992b2 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -56,4 +56,4 @@ jobs: with: apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} - command: pages deploy build --project-name=ramolibrelab --branch=main + command: pages deploy --project-name=ramolibrelab --branch=main diff --git a/functions/api/lab/[id].js b/functions/api/lab/[id].js new file mode 100644 index 0000000..2af9357 --- /dev/null +++ b/functions/api/lab/[id].js @@ -0,0 +1,28 @@ +const TTL_SECONDS = 60 * 60 * 24 * 180; // 180 días + +export async function onRequestGet(context) { + const { params, env } = context; + const id = params.id; + + if (!id || !/^[A-Za-z0-9]{4,12}$/.test(id)) { + return Response.json({ error: 'invalid_id' }, { status: 400 }); + } + + const raw = await env.LAB_KV.get(id); + if (raw === null) { + return Response.json({ error: 'not_found' }, { status: 404 }); + } + + const response = new Response(raw, { + status: 200, + headers: { 'content-type': 'application/json' } + }); + + context.waitUntil( + env.LAB_KV.put(id, raw, { expirationTtl: TTL_SECONDS }).catch((err) => { + console.error('[lab] TTL renewal failed', err); + }) + ); + + return response; +} diff --git a/functions/api/lab/index.js b/functions/api/lab/index.js new file mode 100644 index 0000000..10b7359 --- /dev/null +++ b/functions/api/lab/index.js @@ -0,0 +1,52 @@ +const ALPHABET = 'ABCDEFGHJKMNPQRSTUVWXYZabcdefghijkmnpqrstuvwxyz23456789'; +const ID_LENGTH = 6; +const MAX_PAYLOAD_BYTES = 100_000; +const TTL_SECONDS = 60 * 60 * 24 * 180; // 180 días + +function generateId() { + const bytes = new Uint8Array(ID_LENGTH); + crypto.getRandomValues(bytes); + let id = ''; + for (let i = 0; i < ID_LENGTH; i++) { + id += ALPHABET[bytes[i] % ALPHABET.length]; + } + return id; +} + +export async function onRequestPost(context) { + const { request, env } = context; + + let payload; + try { + payload = await request.json(); + } catch { + return Response.json({ error: 'invalid_json' }, { status: 400 }); + } + + const serialized = JSON.stringify(payload); + if (serialized.length > MAX_PAYLOAD_BYTES) { + return Response.json({ error: 'payload_too_large' }, { status: 413 }); + } + + let id; + for (let attempt = 0; attempt < 3; attempt++) { + const candidate = generateId(); + const existing = await env.LAB_KV.get(candidate); + if (!existing) { + id = candidate; + break; + } + } + + if (!id) { + return Response.json({ error: 'id_generation_failed' }, { status: 500 }); + } + + try { + await env.LAB_KV.put(id, serialized, { expirationTtl: TTL_SECONDS }); + } catch { + return Response.json({ error: 'kv_write_failed' }, { status: 507 }); + } + + return Response.json({ id }, { status: 201 }); +} diff --git a/src/lib/share.ts b/src/lib/share.ts new file mode 100644 index 0000000..de7bcc0 --- /dev/null +++ b/src/lib/share.ts @@ -0,0 +1,51 @@ +import { decodeUrlData, encodeUrlData } from '$lib/utils/url_data'; +import type { Simulacion } from '$lib/state/simulaciones.svelte'; + +export async function shareLab(labData: unknown): Promise { + const origin = window.location.origin; + + try { + const res = await fetch('/api/lab', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify(labData) + }); + + if (!res.ok) throw new Error(`status ${res.status}`); + + const { id } = (await res.json()) as { id: string }; + return `${origin}/?s=${id}`; + } catch (err) { + console.warn('[share] fallback a Base64 por fallo de red/KV', err); + const payload = encodeUrlData(labData); + return `${origin}/?share=${payload}`; + } +} + +export async function hydrateFromUrl(): Promise { + const params = new URLSearchParams(window.location.search); + + const shortId = params.get('s'); + if (shortId) { + try { + const res = await fetch(`/api/lab/${shortId}`); + if (res.ok) { + return (await res.json()) as Simulacion; + } + } catch (err) { + console.warn('[share] error hidratando desde enlace corto', err); + } + return null; + } + + const shared = params.get('share'); + if (shared) { + try { + return decodeUrlData(shared); + } catch (err) { + console.warn('[share] enlace share inválido', err); + } + } + + return null; +} diff --git a/src/routes/(app)/_components/Sidebar.svelte b/src/routes/(app)/_components/Sidebar.svelte index 8ffa07e..0265f8d 100644 --- a/src/routes/(app)/_components/Sidebar.svelte +++ b/src/routes/(app)/_components/Sidebar.svelte @@ -1,8 +1,7 @@