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/jsx **/
import { createDate, isWithinExpirationDate, TimeSpan } from "npm:oslo";
import codeOnValTown from "https://esm.town/v/andreterron/codeOnValTown?v=50";
import { ValTownAdapter } from "https://esm.town/v/pomdtr/lucia_adapter";
import { createUser, getUser, verifyPassword } from "https://esm.town/v/pomdtr/lucia_sqlite";
import { Hono } from "npm:hono";
import { getCookie } from "npm:hono/cookie";
import { HTTPException } from "npm:hono/http-exception";
import { jsxRenderer } from "npm:hono/jsx-renderer";
import { Lucia, Session, User, verifyRequestOrigin } from "npm:lucia@3.0.1";
const userTable = "user";
const sessionTable = "session";
const adapter = new ValTownAdapter({
user: "user",
session: "session",
});
export const lucia = new Lucia(adapter, {
getUserAttributes: (attributes) => {
return {
username: attributes.username,
};
},
});
declare module "npm:lucia" {
interface Register {
Lucia: typeof lucia;
DatabaseUserAttributes: DatabaseUserAttributes;
}
}
interface DatabaseUserAttributes {
username: string;
}
const router = new Hono<{
Variables: {
user: User | null;
session: Session | null;
};
}>();
router.use("*", async (c, next) => {
const sessionId = getCookie(c, lucia.sessionCookieName) ?? null;
if (!sessionId) {
c.set("user", null);
c.set("session", null);
return next();
}
const { session, user } = await lucia.validateSession(sessionId);
if (session && session.fresh) {
// use `header()` instead of `setCookie()` to avoid TS errors
c.header("Set-Cookie", lucia.createSessionCookie(session.id).serialize(), {
append: true,
});
}
if (!session) {
c.header("Set-Cookie", lucia.createBlankSessionCookie().serialize(), {
append: true,
});
}
c.set("user", user);
c.set("session", session);
return next();
});
router.use(jsxRenderer(({ children }) => (
<html lang="en">
<head>
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/@picocss/pico@2/css/pico.min.css"
/>
</head>
<body>
<header class="container">
<nav>
<ul>
<li>
<a href="/" class="contrast">
<strong>Lucia Demo</strong>
</a>
</li>
</ul>
</nav>
</header>
{children}
</body>
</html>
)));
router.get("/", c => {
const user = c.get("user");
if (!user) {
return c.render(

Authentication middleware

Guards your public http vals behind a login page.

c2a79825e9d89429014a036c29887c670806ee3f0188e01cde09adad193a6407.png

This val use a json web token stored as an http-only cookie to persist authentication.

Usage

Set an AUTH_SECRET_KEY env variable (used to sign/verify jwt tokens) to a random string.

Then use an API token to authenticate.

import { auth } from "https://esm.town/v/pomdtr/auth_middleware";

async function handler(req: Request): Promise<Response> {
  return new Response("You are authenticated!");
}

export default auth(handler);

See @pomdtr/test_auth for an example

⚠️ Make sure to only provides your api token to vals you trust (i.e. your own), as it gives access to your whole account.

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
import * as cookie from "https://deno.land/std/http/cookie.ts";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";
import jwt from "npm:jsonwebtoken";
const loginPage = `<html>
<head>
<link rel="icon" href="https://fav.farm/🔒" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
</head>
<body>
<main class="container">
<article>
<form method="post">
<label for="token"><a href="https://www.val.town/settings/api">API Token</a></label>
<input id="token" name="token" type="password" />
<button type="submit">Submit</button>
</form>
</article>
</main>
</body>
</html>`;
export function redirect(location: string): Response {
return new Response(null, {
headers: {
location,
},
status: 302,
});
}
type User = {
id: string;
username: string;
};
async function fetchUser(token: string): Promise<User> {
const resp = await fetch("https://api.val.town/v1/me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (resp.status !== 200) {
throw new Error("Could not fetch user");
}
return resp.json();
}
async function isCurrentUser(userID: string) {
const currentUser = await fetchUser(Deno.env.get("valtown"));
return userID == currentUser.id;
}
function signout() {
const res = redirect("/");
cookie.setCookie(res.headers, {
name: "user-jwt",
value: "",
httpOnly: true,
secure: true,
sameSite: "None",
maxAge: 0,
});
return res;
}
export function auth(handler) {
const secretKey = Deno.env.get("AUTH_SECRET_KEY");
if (!secretKey) {
throw new Error("env var AUTH_SECRET_KEY not set");
}
return async (req: Request) => {
const { pathname, hostname, origin } = new URL(req.url);
if (pathname == "/signin" && req.method == "GET") {
return html(loginPage);
}
if (pathname == "/signin" && req.method == "POST") {
const formData = await req.formData();
const token = formData.get("token").toString();
const user = await fetchUser(token);
if (await isCurrentUser(user.id)) {
return new Response("Unauthorized", {
status: 403,
});
}
const webToken = jwt.sign(user, secretKey, { expiresIn: "7d" });
const res = redirect("/");
cookie.setCookie(res.headers, {
name: "user-jwt",
value: webToken,
httpOnly: true,
secure: true,
sameSite: "None",
});
return res;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo?v=26";
import { sqliteStore } from "https://esm.town/v/vladimyr/keyvhqSqlite";
import Keyv from "npm:@keyvhq/core";
const { slug } = extractValInfo(import.meta.url);
const keyv = new Keyv({ store: sqliteStore, namespace: slug });
await keyv.set("example", "hello keyv!");
console.log(await keyv.has("thisKeyDoesNotExist"));
console.log(await keyv.has("example"));
console.log(await keyv.get("example"));
{
const { sqlite } = await import("https://esm.town/v/std/sqlite?v=5");
const { zip } = await import("https://esm.town/v/pomdtr/sql");
const result = await sqlite.execute("select * from keyv");
console.table(zip(result));
}

Val Town To SQLite

Dump all public vals to an sqlite table

b9cc77435c582eb4594a9bc95e9fa23b73a7d3e85c83354c914f387f7b4fa54f.png

TODO

  • Upsert instead of insert
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
import { bulk, raw, sql } from "https://esm.town/v/pomdtr/sql";
import type { SqliteInterface } from "https://esm.town/v/postpostscript/sqliteTypes";
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
type Val = {
author: {
id: string;
username: string;
};
code: string;
createdAt: string;
id: string;
name: string;
privacy: string;
public: boolean;
runStartAt: string;
runEndAt: string;
version: number;
};
const createQuery = `CREATE TABLE IF NOT EXISTS vals (
id TEXT PRIMARY KEY,
name TEXT,
code TEXT,
version NUMBER,
privacy TEXT,
public INTEGER,
run_start_at TEXT,
run_end_at TEXT,
created_at TEXT,
author_id TEXT,
author_username TEXT
);`;
function valToRow(val: Val) {
return [
val.id,
val.name,
val.code,
val.version,
val.privacy,
val.public,
val.createdAt,
val.runEndAt,
val.runStartAt,
val.author.id,
val.author.username,
];
}
async function insertRows(rows, options: { sqlite: SqliteInterface } = { sqlite }) {
const query =
sql`INSERT OR REPLACE INTO vals (id, name, code, version, privacy, public, run_start_at, run_end_at, created_at, author_id, author_username) VALUES ${
bulk(rows)
}`;
await options.sqlite.execute(query);
}
export async function valtownToSQLite(options: { sqlite: SqliteInterface } = { sqlite }) {
await options.sqlite.execute(createQuery);
let url = "https://api.val.town/v1/search/vals?query=%20&limit=100";
while (true) {
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(await resp.text());
}
const res = await resp.json();
const rows = res.data.map(valToRow);
await insertRows(rows, options);
if (!res.links.next) {
break;
}
url = res.links.next;
}
}
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
import { Command } from "https://esm.town/v/pomdtr/cmdk";
import { zip } from "https://esm.town/v/pomdtr/sql";
import { sqlite } from "https://esm.town/v/std/sqlite?v=6";
export const listTables: Command = async () => {
const res = await sqlite.execute("SELECT name FROM sqlite_master WHERE type='table'");
const tables = zip(res);
return {
type: "list",
list: {
items: tables.map(table => ({
title: table.name,
actions: [
{
title: "Copy Name",
type: "copy",
copy: {
text: table.name,
},
},
],
})),
},
};
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo?v=26";
import { sqliteStore } from "https://esm.town/v/vladimyr/keyvhqSqlite";
import Keyv from "npm:@keyvhq/core";
const { slug } = extractValInfo(import.meta.url);
const keyv = new Keyv({ store: sqliteStore, namespace: slug });
await keyv.set("example", "hello keyv!");
console.log(await keyv.has("thisKeyDoesNotExist"));
console.log(await keyv.has("example"));
console.log(await keyv.get("example"));
{
const { sqlite } = await import("https://esm.town/v/std/sqlite?v=5");
const { zip } = await import("https://esm.town/v/pomdtr/sql");
const result = await sqlite.execute("select * from keyv");
console.table(zip(result));
}

User with Most Vals

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
import { zip } from "https://esm.town/v/pomdtr/sql";
import { db } from "https://esm.town/v/sqlite/db?v=9";
import { renderTable } from "npm:console-table-printer";
import stripAnsi from "npm:strip-ansi";
export default async function() {
const res = await db.execute(
"SELECT author_username, COUNT(*) AS val_count FROM vals GROUP BY author_username ORDER BY 2 DESC LIMIT 10",
);
const table = stripAnsi(renderTable(zip(res)));
const resp = await fetch("https://sourcecodeshots.com/api/image", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
{ code: table, language: "text" },
),
});
if (!resp.ok) {
throw new Error(await resp.text());
}
return new Response(resp.body, {
headers: {
"Content-Type": "image/png",
},
});
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo?v=26";
import { blobStore } from "https://esm.town/v/vladimyr/keyvhqBlob";
import Keyv from "npm:@keyvhq/core";
const { slug } = extractValInfo(import.meta.url);
const keyv = new Keyv({ store: blobStore, namespace: slug });
await keyv.set("example", "hello keyv!");
console.log(await keyv.has("thisKeyDoesNotExist"));
console.log(await keyv.has("example"));
console.log(await keyv.get("example"));
{
const { blob } = await import("https://esm.town/v/std/blob?v=11");
const result = await blob.getJSON("keyv");
console.log(result);
}

Val Town To SQLite

Dump all public vals to an sqlite table

b9cc77435c582eb4594a9bc95e9fa23b73a7d3e85c83354c914f387f7b4fa54f.png

TODO

  • Upsert instead of insert
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
import { bulk, raw, sql } from "https://esm.town/v/pomdtr/sql";
import type { SqliteInterface } from "https://esm.town/v/postpostscript/sqliteTypes";
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
type Val = {
author: {
id: string;
username: string;
};
code: string;
createdAt: string;
id: string;
name: string;
privacy: string;
public: boolean;
runStartAt: string;
runEndAt: string;
version: number;
};
const createQuery = `CREATE TABLE IF NOT EXISTS vals (
id TEXT PRIMARY KEY,
name TEXT,
code TEXT,
version NUMBER,
privacy TEXT,
public INTEGER,
run_start_at TEXT,
run_end_at TEXT,
created_at TEXT,
author_id TEXT,
author_username TEXT
);`;
function valToRow(val: Val) {
return [
val.id,
val.name,
val.code,
val.version,
val.privacy,
val.public,
val.createdAt,
val.runEndAt,
val.runStartAt,
val.author.id,
val.author.username,
];
}
async function insertRows(rows, options: { sqlite: SqliteInterface } = { sqlite }) {
const query =
sql`INSERT OR REPLACE INTO vals (id, name, code, version, privacy, public, run_start_at, run_end_at, created_at, author_id, author_username) VALUES ${
bulk(rows)
}`;
await options.sqlite.execute(query);
}
export async function valtownToSQLite(options: { sqlite: SqliteInterface } = { sqlite }) {
await options.sqlite.execute(createQuery);
let url = "https://api.val.town/v1/search/vals?query=%20&limit=100";
while (true) {
const resp = await fetch(url);
if (!resp.ok) {
throw new Error(await resp.text());
}
const res = await resp.json();
const rows = res.data.map(valToRow);
await insertRows(rows, options);
if (!res.links.next) {
break;
}
url = res.links.next;
}
}
1
2
3
4
5
6
7
import { zip } from "https://esm.town/v/pomdtr/sql";
import { db } from "https://esm.town/v/sqlite/db";
const res = await db.execute(
"SELECT author_username, COUNT(*) as val_count FROM vals GROUP BY author_username ORDER BY 2 DESC LIMIT 10",
);
console.table(zip(res));

Authentication middleware

Guards your public http vals behind a login page.

c2a79825e9d89429014a036c29887c670806ee3f0188e01cde09adad193a6407.png

This val use a json web token stored as an http-only cookie to persist authentication.

Usage

Set an AUTH_SECRET_KEY env variable (used to sign/verify jwt tokens). Use an API token to authenticate.

import { auth } from "https://esm.town/v/pomdtr/auth_middleware";

async function handler(req: Request): Promise<Response> {
  return new Response("You are authenticated!");
}

export default auth(handler);

See @pomdtr/test_auth for an example

⚠️ Make sure to only provides your api token to vals you trust (i.e. your own), as it gives access to your whole account.

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
import * as cookie from "https://deno.land/std/http/cookie.ts";
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo";
import { callerRef } from "https://esm.town/v/pomdtr/refs";
import { sql } from "https://esm.town/v/pomdtr/sql";
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
import { html } from "https://esm.town/v/stevekrouse/html?v=5";
import process from "node:process";
import jwt from "npm:jsonwebtoken";
const cookieName = "user-jwt";
const loginPage = `<html>
<head>
<link rel="icon" href="https://fav.farm/🔒" />
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@picocss/pico@1/css/pico.min.css">
</head>
<body>
<main class="container">
<article>
<form method="post">
<label for="token">API Token</label>
<input id="token" name="token" type="password" />
<button type="submit">Submit</button>
</form>
</article>
</main>
</body>
</html>`;
export function redirect(location: string): Response {
return new Response(null, {
headers: {
location,
},
status: 302,
});
}
function generateAccessToken(user) {
return jwt.sign(user, process.env.ACCESS_TOKEN_SECRET, { expiresIn: "1800s" });
}
type User = {
id: string;
username: string;
};
async function fetchUser(token: string): Promise<User> {
const resp = await fetch("https://api.val.town/v1/me", {
headers: {
Authorization: `Bearer ${token}`,
},
});
if (resp.status !== 200) {
throw new Error("Could not fetch user");
}
return resp.json();
}
function signout() {
const res = redirect("/");
cookie.setCookie(res.headers, {
name: "user-jwt",
value: "",
httpOnly: true,
secure: true,
sameSite: "None",
maxAge: 0,
});
return res;
}
export function auth(handler) {
const secretKey = Deno.env.get("AUTH_SECRET_KEY");
if (!secretKey) {
throw new Error("env var AUTH_SECRET_KEY not set");
}
return async (req: Request) => {
const { pathname, hostname, origin } = new URL(req.url);
const val = {
username: `@${hostname.split("-")[0]}`,
};
if (pathname == "/signin" && req.method == "GET") {
return html(loginPage);
}
if (pathname == "/signin" && req.method == "POST") {
const formData = await req.formData();
const token = formData.get("token").toString();
const user = await fetchUser(token);
if (user.username != val.username) {
return new Response("Unauthorized", {
status: 403,
});
}
const webToken = jwt.sign(user, secretKey, { expiresIn: "7d" });
1
2
3
4
5
6
7
8
9
10
import { db } from "https://esm.town/v/sqlite/db?v=9";
import { zip } from "https://esm.town/v/pomdtr/sql";
const res = await db.execute("SELECT id, code from vals")
const rows = zip(res)
const imports = []
for (const row of rows) {
}

User with Most Vals

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
import { zip } from "https://esm.town/v/pomdtr/sql";
import { db } from "https://esm.town/v/sqlite/db?v=9";
import { renderTable } from "npm:console-table-printer";
import stripAnsi from "npm:strip-ansi";
export default async function() {
const res = await db.execute(
"SELECT author_username, COUNT(*) AS val_count FROM vals GROUP BY author_username ORDER BY 2 DESC LIMIT 10",
);
const table = stripAnsi(renderTable(zip(res)));
const resp = await fetch("https://sourcecodeshots.com/api/image", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(
{ code: table, language: "text" },
),
});
if (!resp.ok) {
throw new Error(await resp.text());
}
return new Response(resp.body, {
headers: {
"Content-Type": "image/png",
"Cache-Control": "fsjkdfdsjkj"
},
});
}
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
import { bulk, sql } from "https://esm.town/v/pomdtr/sql";
import { sqlite } from "https://esm.town/v/std/sqlite?v=4";
import { zip } from "npm:lodash-es";
export type HistoryEntry = {
val_slug: string;
test_name: string;
status: string;
last_run_at: string;
};
export async function createHistory() {
const resp = await sqlite.execute(`CREATE TABLE test_history (
val_slug TEXT,
test_name TEXT,
status TEXT,
last_run_at DATE,
PRIMARY KEY (val_slug, test_name)
);`);
}
export async function readHistory(): Promise<HistoryEntry[]> {
const { columns, rows } = await sqlite.execute("SELECT * from test_history");
const history = rows.map(row =>
Object.fromEntries(zip(
columns,
row,
))
);
return history as HistoryEntry[];
}
export async function writeHistory(entries: HistoryEntry[]) {
const resp = await sqlite.execute(
sql`INSERT OR REPLACE INTO test_history (val_slug, test_name, status, last_run_at) VALUES ${
bulk(entries.map(entry => [
entry.val_slug,
entry.test_name,
entry.status,
entry.last_run_at,
]))
}`,
);
}
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
import { zip } from "https://esm.town/v/pomdtr/sql";
import { db } from "https://esm.town/v/sqlite/db?v=9";
export default async function(req: Request) {
const url = new URL(req.url);
if (!url.searchParams.has("query")) {
return new Response("No query provided", { status: 400 });
}
const query = url.searchParams.get("query");
const res = await db.execute(query);
const rows = zip(res);
const vals = rows.map(row => ({
name: row.name,
code: row.code,
id: row.id,
createdAt: row.created_at,
runEndAt: row.run_end_at,
runStartAt: row.run_start_at,
version: row.version,
public: row.public,
privacy: row.privacy,
author: {
username: row.author_username,
id: row.author_id,
},
}));
return Response.json({ data: vals });
}