Avatar

@easrng

36 likes44 public vals
Joined June 5, 2023
👩‍💻🏳️‍⚧️ she/her

playground

edit, run, and embed vals without requiring an account (or even js enabled!)

open

caveats:

  • logs don't stream
  • I haven't set up codemirror
  • only script vals supported

everything else should be fully functional.

you can prefill the editor with

Readme
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
// (c) easrng 2024 all rights reserved
/** @jsx jsx */
/** @jsxFrag Fragment */
import {
createContext,
type FC,
Fragment,
jsx,
type PropsWithChildren,
useContext,
} from "https://deno.land/x/hono@v4.0.10/jsx/index.ts";
import { Hono } from "https://deno.land/x/hono@v4.0.10/mod.ts";
import { API_URL } from "https://esm.town/v/std/API_URL?v=5";
import { create } from "https://esm.town/v/websandbox/create";
import { parse as parseStack } from "npm:error-stack-parser-es@0.1.1";
import * as stylis from "npm:stylis@4.3.1";
import * as SuperJSON from "npm:superjson@2.2.1";
globalThis.addEventListener("unhandledrejection", (event) => {
event.preventDefault();
console.error("Uncaught (in promise) %o", event.reason)
});
const app = new Hono();
const Layout: FC<PropsWithChildren<{ title: string }>> = (props) => {
return (
<html>
<head>
<title>{props.title}</title>
<meta name="viewport" content="width=device-width, initial-scale=1" />
<style>
{String.raw`
* {
box-sizing: border-box;
}
body, html {
width: 100%;
height: 100%;
margin: 0;
padding: 0;
color-scheme: light dark;
}
pre {
font-size: 1rem;
}
`}
</style>
</head>
<body>{props.children}</body>
</html>
);
};
const StacktraceLink: FC<{ href: string; line?: number; col?: number }> = ({
href,
line,
col,
}) => {
let link: string | JSX.Element = href;
try {
const url = new URL(href);
if (url.protocol === "https:" || url.protocol === "http:") {
link = <a href={url.href}>{url.href}</a>;
}
} catch {}
return (
<>
{link}
{line ? `:${line}${col ? ":" + col : ""}` : ""}
</>
);
};
const defaultFormat = (args: unknown[]) => {
const first = args[0];
let a = 0;
let out: string[] = [];
let styled: JSX.Element[] = [];
let css: string = "";
function flush() {
if (out.length) {
styled.push(css ? <span style={css}>{out}</span> : <>{out}</>);
out = [];
}
}
if (typeof first == "string" && args.length > 1) {
a++;
// Index of the first not-yet-appended character. Use this so we only
// have to append to `string` when a substitution occurs / at the end.
let appendedChars = 0;
for (let i = 0; i < first.length - 1; i++) {
if (first[i] == "%") {
const char = first[++i];
if (a < args.length) {
let formattedArg = "";
if (char == "s") {
// Format as a string.
formattedArg = String(args[a++]);
} else if (Array.prototype.includes.call(["d", "i"], char)) {
// Format as an integer.
const value = args[a++];
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
import { upgradeExpress } from "https://esm.town/v/easrng/upgradeExpress";
async function inlineUrls(cssString) {
const urlRegex = /url\(['"]?(.*?)['"]?\)/g;
let result = cssString;
for (const urlMatch of cssString.matchAll(urlRegex)) {
const originalUrl = urlMatch[1];
const response = await fetch(originalUrl);
const data = new Uint8Array(await response.arrayBuffer());
result = result.replace(
urlMatch[0],
`url("data:${response.headers.get("content-type")};base64,${
btoa(data.reduce((a, b) => a + String.fromCharCode(b), ""))
}")`,
);
}
return result;
}
import IBMPlexSansMetrics from "https://easrng.github.io/IBMPlexSansMetrics.js";
import esc from "npm:escape-html@1.0.3";
import { init } from "npm:server-text-width@1.0.2";
export const button = upgradeExpress(async function button(req) {
const { getTextWidth } = init(
IBMPlexSansMetrics,
);
const text = decodeURIComponent(new URL(req.url).pathname.slice(1) || "Button");
let fontCss = "";
try {
const r = await fetch(
"https://fonts.googleapis.com/css2?family="
+ encodeURIComponent("IBM Plex Sans:wght@600")
+ "&text="
+ encodeURIComponent(text),
{ headers: { "user-agent": req.headers.get("user-agent") } },
);
if (!r.ok)
throw new Error();
fontCss = await inlineUrls(await r.text());
}
catch (e) {}
const width = Math.ceil(getTextWidth(text) + 16);
return new Response(
`<svg viewBox="0 0 ${width} 28" width="${width}" height="28" xmlns="http://www.w3.org/2000/svg"><defs><style>${fontCss}</style></defs><foreignObject width="100%" height="100%"><span xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'IBM Plex San
esc(text)
}</span></foreignObject></svg>`,
{
headers: {
"content-type": "image/svg+xml",
},
},
);
});

upload to 0x0.st

usage:

Create val@easrng.uploadTo0x0(data, "filename")

data can be a string, a TypedArray, an ArrayBuffer, or a Blob
returns a url to a file that expires after an hour

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
import { fetch } from "https://esm.town/v/std/fetch";
export async function uploadTo0x0(data, name) {
const blob = new Blob([data]);
const formData = new FormData();
formData.append("file", blob, name);
formData.append("expires", "1");
const response = await fetch("https://0x0.st", {
method: "POST",
body: formData,
});
return await response.text();
}
1
2
3
export const whoami = () =>
new Error().stack.match(/@@({.+?})@@/g).map((e) => JSON.parse(e.slice(2, -2)))
.map((e) => "@" + e.userHandle + "." + e.valName).slice(1);

resilientFetch: faster @std/fetch

Try to fetch unproxied first, and fall back to proxied fetch if that fails.

Readme
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
import { API_URL } from "https://esm.town/v/std/API_URL";
async function proxiedFetch(req: Request): Promise<Response> {
let query = new URLSearchParams({
url: req.url,
});
const headers = new Headers(req.headers);
headers.set("X-Valtown-Authorization", `Bearer ${Deno.env.get("valtown")}`);
return fetch(`${API_URL}/v1/fetch?${query}`, {
...req,
headers,
});
}
import isNetworkError from "npm:is-network-error@1.0.1";
export const resilientFetch: typeof fetch = async (input: RequestInfo | URL, init?: RequestInit): Promise<Response> => {
const req = new Request(input, init);
try {
const res = await fetch(req);
if (res.ok) {
return res;
}
} catch (e) {
if (!isNetworkError(e)) {
throw e;
}
}
return await proxiedFetch(req);
};

upgradeExpress

Upgrade Express vals to the Web API without breaking existing consumers

How it works

upgradeExpress wraps a Web handler into a function that works as normal when called by the Web API, and redirects to the Web API when called by the Express API.

Example

I don't have an example for this per se but I used it to upgrade @easrng/button.

Readme
1
2
3
4
5
6
7
8
9
10
11
12
13
export const upgradeExpress = (handler: (request: Request) => Response | PromiseLike<Response>) =>
function(request: Request | express.Request, response?: express.Response): Response | Promise<Response> {
if (request instanceof Request) {
const response = handler(request);
if (response instanceof Response) {
return response;
} else {
return Promise.resolve(response);
}
}
const domain: string = request.get("host").replace(/\.express\.val\.run$/, ".web.val.run");
response!.redirect(`https://${domain}${request.originalUrl}`);
};

secure signatures with vals

setup

you'll need to make 2 new vals:

  1. generate a keypair (keep this val private)
    Create vallet vsExportedKeys = @easrng.generateKeys();
  2. publish your public key (make this val public)
    Create valconst vsPublicKey = () => @me.exportedKeys.publicKey;

usage

sign

call @easrng.valSign to get a signature.

Create valconst signature = await @easrng.valSign({ keys: @me.vsExportedKeys, data: {hello: "world"}})

the result will look something like this:

@easrng.htVgaVWWtvnz5AK0DnDaNON5gar5qJeaorfsTCiIr7ua_-D4HPmFrIrPMfwmCaMvI0CxKlYCUe9XTGm7r5s5C3siZGF0YSI6eyJoZWxsbyI6IndvcmxkIn0sInVzZXIiOiJlYXNybmciLCJleHByIjpudWxsfQ

you can also set an expiration date:

Create valconst signature = await @easrng.valSign({ keys: @me.vsExportedKeys, data: "this expires in 1 second", expireIn: 1000 })

verify

call @easrng.valSignVerify to verify a signature

Create valconst { data, handle, expiresAt } = await @easrng.valSignVerify(signature)

with the example signature from earlier, data would be {hello: "world"}, handle would be easrng, and expiresAt would be null

Readme
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 { whoami } from "https://esm.town/v/easrng/whoami";
import { Buffer } from "node:buffer";
export async function valSign({ keys, data, expireIn }: {
keys: {
publicKey: string;
secretKey: string;
};
data: any;
expireIn?: number;
}) {
const topUser = whoami().at(-1).slice(1).split(".")[0];
const verifyPublicKey = await __utils__.api(topUser + ".vsPublicKey");
if (verifyPublicKey !== keys.publicKey)
throw new Error("keypair doesn't match @" + topUser + ".vsPublicKey()");
const { default: nacl } = await import("npm:tweetnacl@1.0.3");
const keyPair = {
publicKey: Buffer.from(keys.publicKey, "base64"),
secretKey: Buffer.from(keys.secretKey, "base64"),
};
const message = new TextEncoder().encode(JSON.stringify({
data,
user: topUser,
expr: expireIn ? Date.now() + expireIn : null,
}));
return "@" + topUser + "."
+ Buffer.from(nacl.sign(message, keyPair.secretKey)).toString("base64url");
}
1
2
3
4
5
6
7
8
export const thisReference = (): {
userHandle: string;
valName: string;
callNumber?: number;
} =>
new Error().stack.match(/@@({.+?})@@/g).map((e) =>
JSON.parse(e.slice(2, -2))
)[1];
1
2
3
4
5
6
7
8
9
10
11
12
export const dumbingOfAgeServerTime = async () => {
const before = Date.now();
const serverTs = new Date(
(await fetch(
"https://www.dumbingofage.com/wp-content/uploads/css/custom_style.css?v=" +
Date.now()
)).headers.get("date"),
).valueOf();
const after = Date.now();
const offset = serverTs - (before + after) / 2;
return Math.floor(offset);
};
1
2
3
4
5
6
7
8
9
10
11
import { httpLink, createTRPCProxyClient } from "npm:@trpc/client";
export const trpc = createTRPCProxyClient({
links: [
httpLink({
url: "https://www.val.town/api/trpc",
}),
],
})
export const getValTownTrpc = (async () => {
return trpc;
});