Avatar

stevekrouse

mayor of val town
835 public vals
Joined July 11, 2022

Inspector to browser json data in HTTP vals

Screenshot 2024-02-23 at 9.31.42 AM.png

Example: https://val.town/v/stevekrouse/weatherDescription

Thanks @mmcgrana (https://markmcgranaghan.com/) for the idea!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { html } from "https://esm.town/v/stevekrouse/html";
import { accepts } from "https://esm.town/v/vladimyr/accepts";
export const json_viewer = (data) => (req: Request) => {
const accept = accepts(req);
if (!accept.type("html")) {
return Response.json(data);
}
return html(`<!DOCTYPE html>
<html lang="en">
<body>
<div id="json-viewer"></div>
<script src="https://cdn.jsdelivr.net/npm/@textea/json-viewer@3"></script>
<script>
new JsonViewer({
value: ${JSON.stringify(data)}
}).render('#json-viewer')
</script>
</body>
</html>`);
};

Blob Admin

This is a lightweight Blob Admin interface to view and debug your Blob data.

b7321ca2cd80899250589b9aa08bc3cae9c7cea276282561194e7fc537259b46.png

Use this button to install the val:

It uses basic authentication with your Val Town API Token as the password (leave the username field blank).

TODO

  • /new - render a page to write a new blob key and value
  • /edit/:blob - render a page to edit a blob (prefilled with the existing content)
  • /delete/:blob - delete a blob and render success
  • add upload/download buttons
  • Use modals for create/upload/edit/view/delete page (htmx ?)
  • handle non-textual blobs properly
  • use codemirror instead of a textarea for editing text blobs
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
/** @jsxImportSource https://esm.sh/hono@4.0.8/jsx **/
import { modifyFetchHandler } from "https://esm.town/v/andreterron/codeOnValTown?v=50";
import view_route from "https://esm.town/v/pomdtr/blob_admin_blob";
import create_route from "https://esm.town/v/pomdtr/blob_admin_create";
import delete_route from "https://esm.town/v/pomdtr/blob_admin_delete";
import edit_route from "https://esm.town/v/pomdtr/blob_admin_edit";
import upload_route from "https://esm.town/v/pomdtr/blob_admin_upload";
import { passwordAuth } from "https://esm.town/v/pomdtr/password_auth?v=74";
import { blob } from "https://esm.town/v/std/blob?v=11";
import { Hono } from "npm:hono@4.0.8";
import { jsxRenderer } from "npm:hono@4.0.8/jsx-renderer";
const app = new Hono();
app.use(
jsxRenderer(({ children }) => {
return (
<html>
<head>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
<script
type="module"
src="https://cdn.jsdelivr.net/npm/code-mirror-web-component@0.0.8/dist/code-mirror.js"
>
</script>
<title>Blob Admin</title>
</head>
<body>
<main class="container">
{children}
</main>
</body>
</html>
);
}),
);
app.get("/", async (c) => {
let blobs = await blob.list();
return c.render(
<div class="overflow-auto">
<h1>Blob Admin</h1>
<section
style={{
display: "flex",
gap: "0.5em",
}}
>
<a href="/create">New Blob</a>
<a href="/upload">Upload Blob</a>
</section>
<section>
<table>
<thead>
<tr>
<th>Name</th>
<th>Size (kb)</th>
<th>Last Modified</th>
<th
style={{
textAlign: "center",
}}
>
Edit
</th>
<th
style={{
textAlign: "center",
}}
>
Delete
</th>
<th
style={{
textAlign: "center",
}}
>
Download
</th>
</tr>
</thead>
{blobs.map(b => (
<tr>
<td>
<a href={`/view/${encodeURIComponent(b.key)}`}>
{b.key}
</a>
</td>
<td>{b.size / 1000}</td>
<td>{new Date(b.lastModified).toLocaleString()}</td>
<td
style={{
textAlign: "center",
}}
>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import process from "node:process";
export const langchainEx = (async () => {
const { OpenAI } = await import("https://esm.sh/langchain/llms/openai");
const { PromptTemplate } = await import("https://esm.sh/langchain/prompts");
const { LLMChain } = await import("https://esm.sh/langchain/chains");
const model = new OpenAI({
temperature: 0.9,
openAIApiKey: process.env.openai,
maxTokens: 100,
});
const template = "What is a good name for a company that makes {product}?";
const prompt = new PromptTemplate({
template: template,
inputVariables: ["product"],
});
const chain = new LLMChain({ llm: model, prompt: prompt });
const res = await chain.call({ product: "colorful socks" });
return res;
})();
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
import { normalizeURL } from "https://esm.town/v/stevekrouse/normalizeURL";
export const fetchJSON = async (
url: string | URL,
options?: RequestInit & {
bearer?: string;
fetch?: typeof fetch;
},
) => {
let headers = new Headers(options?.headers ?? {});
headers.set("accept", "application/json");
if (options?.bearer) {
headers.set("authorization", `Bearer ${options.bearer}`);
}
let fetch = options?.fetch ?? globalThis.fetch;
let resp = await fetch(normalizeURL(url), {
redirect: "follow",
...options,
headers,
});
let text = await resp.text();
try {
return JSON.parse(text);
}
catch (e) {
throw new Error(`fetchJSON error: ${e.message} in ${url}\n\n"${text}"`);
}
};

Clone of @maxm/staticChess but for Wordle. Every letter is a link. The game state is stored in the URL bar.

You could do silly things like playing collaborative Wordle with your friends by trading links back and forth. Or undo any mistakes by clicking the back button. I also make it easy to generate a new game from any of your current game's guesses – to send to a friend.

They key to these static games like this one and @maxm/staticChess is to figure out:

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
/** @jsxImportSource https://esm.sh/react */
import { render } from "https://deno.land/x/resvg_wasm/mod.ts";
import { html } from "https://esm.town/v/stevekrouse/html";
import { reloadOnSaveFetchMiddleware } from "https://esm.town/v/stevekrouse/reloadOnSave";
import { renderToString } from "npm:react-dom/server";
const GREEN = "#6aaa64";
const YELLOW = "#c9b458";
const GRAY = "#787c7e";
const LIGHT_GRAY = "#d3d6da";
// TODO - more words
const wordleWords = [
"array",
"cache",
"class",
"clone",
"codec",
"debug",
"fetch",
"frame",
"gigot",
"index",
"input",
"logic",
"patch",
"query",
"proxy",
"regex",
"route",
"stack",
"token",
"tuple",
];
interface Game {
answer: string;
guesses: string[];
currentGuess: string;
}
function encodeGame(game: Game): string {
return btoa(JSON.stringify(game));
}
function decodeGame(encoded: string): Game {
return JSON.parse(atob(encoded));
}
function nextStateEncoded(url, game: Game, action: string): string | undefined {
const next = nextState(game, action);
return next ? `https://${url}/${encodeGame(next)}` : undefined;
}
function nextState(game: Game, action: string): Game | undefined {
// TODO - check if action is valid and return null if not, ie no link
const { answer, guesses, currentGuess } = game;
if (guesses.some((guess) => [...guess].some((x, i) => x === action && !answer.includes(action)))) {
return null;
}
if (answer === guesses.at(-1)) return null;
if (action === "Enter" && currentGuess.length !== 5) return null;
if (action === "Enter") {
return {
...game,
guesses: [...game.guesses, currentGuess],
currentGuess: "",
};
}
if (action === "Delete") {
return {
...game,
currentGuess: currentGuess.slice(0, -1),
};
}
if (currentGuess.length === 5) return null;
return {
...game,
currentGuess: currentGuess + action,
};
}
function rightPad(str: string, length: number): string {
return str + " ".repeat(length - str.length);
}
function letterColor({ answer, guesses }: Game, letter: string): string {
if (guesses.some((guess) => [...guess].some((x, i) => x === letter && answer[i] === letter))) {
return GREEN;
}
if (
guesses.some((guess) => [...guess].some((x, i) => x === letter && answer[i] !== letter && answer.includes(letter)))
) {
return YELLOW;
}
if (guesses.some((guess) => [...guess].some((x, i) => x === letter && !answer.includes(letter)))) {
return GRAY;
}
return LIGHT_GRAY;
}
async function handler(req: Request): Promise<Response> {
const url = new URL(req.url);
if (url.pathname === "/") {

Passkeys Demo

Passkeys are pretty neat! I wanted to get a demo working in Val Town so I ported over https://github.com/maximousblk/passkeys-demo.

One challenge was that the original extensively uses DenoKV store with compound keys and values. I created @stevekrouse/DenoSyntheticKV as a replacement for DenoKV. It uses SuperJSON to encode the keys and values.

You can find the client-side script for the main page here: @stevekrouse/passkey_script

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
/** @jsxImportSource npm:hono@3/jsx */
import { deleteCookie, getSignedCookie, setSignedCookie } from "https://deno.land/x/hono@v3.6.3/middleware.ts";
import { Hono } from "https://deno.land/x/hono@v3.6.3/mod.ts";
import { jwtVerify, SignJWT } from "https://deno.land/x/jose@v4.14.6/index.ts";
import {
generateAuthenticationOptions,
generateRegistrationOptions,
verifyAuthenticationResponse,
verifyRegistrationResponse,
} from "https://deno.land/x/simplewebauthn@v10.0.0/deno/server.ts";
import { isoBase64URL, isoUint8Array } from "https://deno.land/x/simplewebauthn@v10.0.0/deno/server/helpers.ts";
import type {
AuthenticationResponseJSON,
RegistrationResponseJSON,
} from "https://deno.land/x/simplewebauthn@v10.0.0/deno/typescript-types.ts";
import { DenoSyntheticKV } from "https://esm.town/v/stevekrouse/DenoSyntheticKV";
// CONSTANTS
const SECRET = new TextEncoder().encode(Deno.env.get("JWT_SECRET") ?? "development");
const RP_ID = "stevekrouse-passkeys_demo.web.val.run";
const RP_NAME = Deno.env.get("WEBAUTHN_RP_NAME") ?? "Deno Passkeys Demo";
const CHALLENGE_TTL = Number(Deno.env.get("WEBAUTHN_CHALLENGE_TTL")) || 60_000;
// UTILS
function generateJWT(userId: string) {
return new SignJWT({ userId }).setProtectedHeader({ alg: "HS256" }).sign(SECRET);
}
function verifyJWT(token: string) {
return jwtVerify(token, SECRET);
}
function generateRandomString() {
return Math.random().toString(36).substring(2, 15) + Math.random().toString(36).substring(2, 15);
}
// DATABASE
const kv = new DenoSyntheticKV("passkeys_example");
type User = {
username: string;
data: string;
credentials: Record<string, Credential>;
};
type Credential = {
credentialID: Uint8Array;
credentialPublicKey: Uint8Array;
counter: number;
};
type Challenge = true;
// RP SERVER
const app = new Hono();
app.get("/", c =>
c.html(
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Passkeys Demo</title>
<link rel="icon" href="https://cdn.jsdelivr.net/gh/shuding/fluentui-emoji-unicode/assets/🦕_color.svg" />
<script src="https://unpkg.com/@simplewebauthn/browser/dist/bundle/index.umd.min.js"></script>
<link rel="stylesheet" href="https://unpkg.com/bamboo.css" />
</head>
<body>
<h1>🦕 Passkeys Demo</h1>
<p>
<a href="https://www.val.town/v/stevekrouse/passkeys_demo">View code</a> on Val Town. Port of{" "}
<a href="https://github.com/maximousblk/passkeys-demo
">
maximousblk/passkeys-demo
</a>.
</p>
<p id="passkeys_check">Passkeys are not supported! ❌</p>
<noscript>
<blockquote>
<p>⚠️ Passkeys require JavaScript to work.</p>
</blockquote>
</noscript>
<form>
<fieldset id="auth" disabled>
<legend>Login</legend>
<label for="name">
Name <span style="opacity: 0.5">(Optional)</span>
</label>
<input type="text" id="name" name="name" autocomplete="username webauthn" placeholder="Anon" />
<hr />
<button type="button" id="register" onclick="handleRegister()">Register</button>
<button type="button" id="login" onclick="handleLogin()">Login</button>
<button type="button" id="logout" onclick="handleLogout()">Logout</button>

Email with GPT-3

Send an email to stevekrouse.emailGPT3@valtown.email, it will forward it to gpt3, and email you back the response.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { thisEmail } from "https://esm.town/v/stevekrouse/thisEmail";
import { mail } from "https://esm.town/v/stevekrouse/mail";
import { runVal } from "https://esm.town/v/std/runVal";
export async function emailGPT3(email) {
let response = await runVal("patrickjm.gpt3", { prompt: email.text });
return mail({
to: email.from,
from: thisEmail(),
subject: "Re: " + email.subject,
text: response,
});
}

dlock - free distributed lock as a service

https://dlock.univalent.net/

Usage

API

Acquire a lock.

The id path segment is the lock ID - choose your own.

https://dlock.univalent.net/lock/arbitrary-string/acquire?ttl=60

{"lease":1,"deadline":1655572186}

Another attempt to acquire the same lock within its TTL will fail with HTTP status code 409.

https://dlock.univalent.net/lock/01899dc0-2742-44f9-9c7b-01830851b299/acquire?ttl=60

{"error":"lock is acquired by another client","deadline":1655572186}

The previous lock can be renewed with its lease number, like a heartbeat

https://dlock.univalent.net/lock/01899dc0-2742-44f9-9c7b-01830851b299/acquire?ttl=60&lease=1

{"lease":1,"deadline":1655572824}

Release a lock

https://dlock.univalent.net/lock/01899dc0-2742-44f9-9c7b-01830851b299/release?lease=42

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
import { searchParams } from "https://esm.town/v/stevekrouse/searchParams";
import { fetchJSON } from "https://esm.town/v/stevekrouse/fetchJSON";
import { parentReference } from "https://esm.town/v/stevekrouse/parentReference";
export async function dlock({ id, ttl, release, lease }: {
id?: string;
ttl?: number;
release?: boolean;
lease?: number;
} = {}): Promise<{
lease?: number;
deadline: number;
error?: "string";
}> {
id = id ??
parentReference().userHandle + "-" +
parentReference().valName;
ttl = ttl ?? 3; // seconds
let method = release ? "release" : "acquire";
return fetchJSON(
`https://dlock.univalent.net/lock/${id}/${method}?${
searchParams({ ttl, lease })
}`,
);
}
1
2
3
4
5
6
import { karma } from "https://esm.town/v/stevekrouse/karma";
export const whatIsValTown = karma.replaceAll(
/karma/gi,
"Val Town",
);

Import a val script in the chrome console

Say you're developing a chrome extension or some other little script and you're iterating in the chrome console. Over time you may write larger pieces of code that you want to save as a group. Or maybe you want to use TypeScript, which isn't supported in browsers natively. Val Town can provide a decent workflow here.

  1. Save your code in a Script val
  2. Ensure the val is Public or Unlisted
  3. In the chrome console, import your val's module url. For example, for this val it would look like this:
Create vallet {test} = await import(`https://esm.town/v/stevekrouse/importValInChromeConsole?${Math.random()}`)

I added a random number to the end of the string to prevent your browser from caching the import. So now you can save in Val Town and then re-run your import line in the Chrome console and the script will re-run in the Chrome console.

1
2
3
4
5
console.log("This runs on import");
export function test() {
console.log("This runs when test() is called");
}