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
import { extractValInfo } from "https://esm.town/v/pomdtr/extractValInfo?v=26";
import { openKv } from "https://esm.town/v/pomdtr/kv?v=3";
import { sqlite } from "https://esm.town/v/std/sqlite?v=6";
import { DenoSyntheticKV } from "https://esm.town/v/stevekrouse/DenoSyntheticKV?v=10";
import { createStore, sqliteStore } from "https://esm.town/v/vladimyr/keyvhqSqlite";
import Keyv from "npm:@keyvhq/core";
type TestObj = { test: number };
// setup openKv
const kv1 = openKv<TestObj>();
// setup Keyv
const { slug: namespace } = extractValInfo(import.meta.url);
// const store = sqliteStore; /* same as: const store = createStore({ table: "keyv" }); */
const store = createStore({ table: "keyv" });
const kv2 = new Keyv<TestObj>({ namespace, store });
// setup DenoSyntheticKV
const kv3 = new DenoSyntheticKV("keyv");
await kv1.set("foo", { test: 42 });
console.log("@std/sqlite:", (await sqlite.execute("select * from keyv")).rows);
console.log("openKv:", await kv1.get("foo"));
console.log("Keyv:", await kv2.get("foo"));
// doesn't work because DenoSyntheticKV serializes key to {"json":"vladimyr/kv_example:foo"}
console.log("DenoSyntheticKV:", await kv3.get(`${namespace}:foo`));
let data = await kv2.get("foo");
data.test += 1;
await kv2.set("foo", data);
console.log("\n@std/sqlite:", (await sqlite.execute("select * from keyv")).rows);
console.log("openKv:", await kv1.get("foo"));
console.log("Keyv:", await kv2.get("foo"));
// doesn't work because DenoSyntheticKV serializes key to {"json":"vladimyr/kv_example:foo"}
console.log("DenoSyntheticKV:", await kv3.get(`${namespace}:foo`));

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>

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>
1
Next