From cca06343baa3ba9bc74d51f7cd38f74b69f24884 Mon Sep 17 00:00:00 2001 From: Goop Date: Wed, 23 Sep 2026 05:39:32 +0000 Subject: [PATCH 1/3] Serve go.fart.tools shortlink service on Deno Deploy v2 Export a default fetch handler so the new Deno Deploy runtime can serve the router, and make the Deno KV layer optional: the service now boots with the committed links.json ruleset, merges KV entries when a database is bound, and returns 503 from the write endpoints when it is not. Commit the FartLabs shortlinks (chat, twitter, instagram, in) so the ruleset is versioned. --- cli/main.tsx | 87 ++++++++++++++++++++++++++++++++++++++++++++++------ links.json | 4 +++ 2 files changed, 81 insertions(+), 10 deletions(-) diff --git a/cli/main.tsx b/cli/main.tsx index 4ebdd2c..b6d7c3a 100644 --- a/cli/main.tsx +++ b/cli/main.tsx @@ -20,23 +20,38 @@ type Shortlinks = Record; class GoService { public constructor( - private readonly kv: Deno.Kv, + private readonly kv: Deno.Kv | undefined, + private readonly staticShortlinks: Shortlinks = {}, private readonly kvNamespace: Deno.KvKey = ["go"], ) {} + public get writable(): boolean { + return this.kv !== undefined; + } + + private requireKv(): Deno.Kv { + if (!this.kv) { + throw new Error( + "Shortlinks are read-only: no Deno KV database is bound to this deployment.", + ); + } + return this.kv; + } + public async add( alias: string, destination: string, force = false, ): Promise { - const shortlinksResult = await this.kv.get(this.kvNamespace); + const kv = this.requireKv(); + const shortlinksResult = await kv.get(this.kvNamespace); const shortlinks = shortlinksResult.value ?? {}; if (!force && shortlinks[alias]) { throw new Error("Shortlink already exists."); } shortlinks[alias] = destination; - const result = await this.kv.atomic() + const result = await kv.atomic() .check(shortlinksResult) .set(this.kvNamespace, shortlinks) .commit(); @@ -46,14 +61,15 @@ class GoService { } public async delete(alias: string): Promise { - const shortlinksResult = await this.kv.get(this.kvNamespace); + const kv = this.requireKv(); + const shortlinksResult = await kv.get(this.kvNamespace); const shortlinks = shortlinksResult.value ?? {}; if (!shortlinks[alias]) { throw new Error("Shortlink does not exist."); } delete shortlinks[alias]; - const result = await this.kv.atomic() + const result = await kv.atomic() .check(shortlinksResult) .set(this.kvNamespace, shortlinks) .commit(); @@ -63,8 +79,11 @@ class GoService { } public async shortlinks(): Promise { + if (!this.kv) { + return this.staticShortlinks; + } const shortlinksResult = await this.kv.get(this.kvNamespace); - return shortlinksResult.value ?? {}; + return { ...this.staticShortlinks, ...(shortlinksResult.value ?? {}) }; } } @@ -85,6 +104,15 @@ function GoRouter(props: { service: GoService }) { if (!isAuthorized(ctx.request.headers)) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } + if (!props.service.writable) { + return Response.json( + { + error: + "Shortlinks are read-only: no Deno KV database is bound.", + }, + { status: 503 }, + ); + } const body = await ctx.request.json(); await props.service.add(body.alias, body.destination, body.force); @@ -100,6 +128,15 @@ function GoRouter(props: { service: GoService }) { if (!isAuthorized(ctx.request.headers)) { return Response.json({ error: "Unauthorized" }, { status: 401 }); } + if (!props.service.writable) { + return Response.json( + { + error: + "Shortlinks are read-only: no Deno KV database is bound.", + }, + { status: 503 }, + ); + } const body = await ctx.request.json(); await props.service.delete(body.alias); @@ -196,9 +233,39 @@ function GoRouter(props: { service: GoService }) { ); } +const staticShortlinks: Shortlinks = JSON.parse( + Deno.readTextFileSync(new URL("../links.json", import.meta.url)), +); + +async function openKvIfAvailable(): Promise { + try { + return await Deno.openKv(); + } catch (error) { + console.warn( + "Deno KV is unavailable, serving links.json read-only:", + String(error), + ); + return undefined; + } +} + +const kv = await openKvIfAvailable(); + +let serviceRouter: ReturnType | undefined; + +function getServiceRouter(): ReturnType { + if (!serviceRouter) { + serviceRouter = ; + } + return serviceRouter; +} + +export default { + fetch: (request: Request) => getServiceRouter().fetch(request), +} satisfies Deno.ServeDefaultExport; + if (import.meta.main) { - const kv = await Deno.openKv(); - const goService = new GoService(kv); - const router = ; - Deno.serve((request) => router.fetch(request)); + const port = parseInt(Deno.env.get("PORT") ?? "8000"); + console.info(`go.fart.tools development server: http://localhost:${port}/`); + Deno.serve({ port }, (request) => getServiceRouter().fetch(request)); } diff --git a/links.json b/links.json index 86c2cdb..0317f44 100644 --- a/links.json +++ b/links.json @@ -1,6 +1,10 @@ { "api": "https://api.example.com", + "chat": "https://discord.gg/ZwHaZ63yq3", "docs": "/documentation", "github": "https://github.com", + "in": "https://linkedin.com/company/fartlabs", + "instagram": "https://instagram.com/fartlabs", + "twitter": "https://x.com/fartlabs_org", "yt": "https://youtube.com" } From ab8724a6cf135e7d6906797d3daf4a0e12c5aeb9 Mon Sep 17 00:00:00 2001 From: Goop Date: Wed, 23 Sep 2026 05:39:33 +0000 Subject: [PATCH 2/3] style: apply deno fmt to README.md and demo/browser.html --- README.md | 26 +++++++-------- demo/browser.html | 81 +++++++++++++++++++++++------------------------ 2 files changed, 53 insertions(+), 54 deletions(-) diff --git a/README.md b/README.md index 110e639..4dffb13 100644 --- a/README.md +++ b/README.md @@ -102,24 +102,24 @@ The library runs in the browser via esm.sh — no build step required: ```html ``` diff --git a/demo/browser.html b/demo/browser.html index 6e0b7f2..e10af27 100644 --- a/demo/browser.html +++ b/demo/browser.html @@ -4,54 +4,53 @@ go — browser demo From fa17b55660249f2f466e29f8bbea37195ffefbeb Mon Sep 17 00:00:00 2001 From: Goop Date: Wed, 23 Sep 2026 05:40:19 +0000 Subject: [PATCH 3/3] fix: serve shortlinks on Deno Deploy v2 and unblock go.fart.tools The CLI called Deno.openKv() and Deno.serve() at module scope, which made the service fail to boot on Deno Deploy v2: the platform needs a default export and no database was bound. Now: - export a fetch handler so Deno Deploy can serve the app, keeping the import.meta.main path for local deno run - treat Deno KV as an optional overlay on links.json, so the service boots and resolves links even when no KV database is assigned (writes answer 503 instead of crashing the process) - answer 404 for paths that do not resolve to a shortlink instead of redirecting a request to itself Also commits the FartLabs shortlinks (chat, in, instagram, twitter) to links.json so the deployed service resolves them without KV. --- cli/main.tsx | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/cli/main.tsx b/cli/main.tsx index b6d7c3a..0d5ecb5 100644 --- a/cli/main.tsx +++ b/cli/main.tsx @@ -220,6 +220,12 @@ function GoRouter(props: { service: GoService }) { const shortlinks = await props.service.shortlinks(); const url = new URL(ctx.request.url); const destination = go(url, shortlinks); + if (destination.href === url.href) { + return Response.json( + { error: `Shortlink not found: ${url.pathname}` }, + { status: 404 }, + ); + } return new Response( null, {