Skip to content

Commit

Permalink
feat: add toolbox APIs
Browse files Browse the repository at this point in the history
  • Loading branch information
kitsonk committed Sep 12, 2024
1 parent 38a7cbb commit 1e0842f
Show file tree
Hide file tree
Showing 5 changed files with 1,377 additions and 2 deletions.
2 changes: 1 addition & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"deno.enable": true,
"deno.unstable": true,
"deno.unstable": ["kv"],
"deno.lint": true
}
37 changes: 37 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,43 @@

A set of tools for working with Deno KV.

## Toolbox

The default export of the library is the encapsulation of major functionality of
the library into classes which enhance the capabilities of working with a Deno
KV store, which are also available as individual named exports of the the
library.

There are to variants of the toolbox: `KvToolbox` and `CryptoKvToolbox`. These
provide all the APIs of a `Deno.Kv` and the additional APIs offered by the rest
of the library. The `CryptoKvToolbox` also attempts to encrypt and decrypt blob
values.

Opening a toolbox is similar to opening a `Deno.Kv` store:

```ts
import { openKvToolbox } from "jsr:@kitsonk/kv-toolbox";

const kv = await openKvToolbox();
```

If an encryption key is passed as an option, a `CryptoKvToolbox` instance will
be returned, where when storing and retrieving blobs in the store, they will be
encrypted and decrypted by default:

```ts
import { generateKey, openKvToolbox } from "jsr:@kitsonk/kv-toolbox";

const encryptWith = generateKey();
const kv = await openKvToolbox({ encryptWith });
```

> [!NOTE]
> In practice, encryption keys would need to be persisted from session to
> session. The code above would generate a new key every execution and any
> values stored could not be decrypted. To be practical, generated encryption
> keys need to be stored securely as a secret.
## Batched Atomic

A set of APIs for dealing with the limitation of atomic commit sizes in Deno KV.
Expand Down
2 changes: 1 addition & 1 deletion deno.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@kitsonk/kv-toolbox",
"version": "0.18.0",
"version": "0.19.0-beta.1",
"exports": {
"./batched_atomic": "./batched_atomic.ts",
"./blob": "./blob.ts",
Expand Down
146 changes: 146 additions & 0 deletions toolbox.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
import {
assert,
assertEquals,
cleanup,
getPath,
timingSafeEqual,
} from "./_test_util.ts";
import { generateKey } from "./crypto.ts";

import { openKvToolbox } from "./toolbox.ts";

Deno.test({
name: "kvToolbox - open and close",
async fn() {
const path = await getPath();
const kv = await openKvToolbox({ path });
kv.close();
return cleanup();
},
});

Deno.test({
name: "kvToolbox - get and set functionality",
async fn() {
const path = await getPath();
const kv = await openKvToolbox({ path });
const result = await kv.set(["key"], "value");
assert(result.ok);
const maybeEntry = await kv.get(["key"]);
assert(maybeEntry.versionstamp);
assertEquals(maybeEntry.value, "value");
kv.close();
return cleanup();
},
});

Deno.test({
name: "kvToolbox - .keys()",
async fn() {
const path = await getPath();
const kv = await openKvToolbox({ path });
const res = await kv.atomic()
.set(["a"], "a")
.set(["a", "b"], "b")
.set(["a", "b", "c"], "c")
.set(["a", "d", "f", "g"], "g")
.set(["a", "h"], "h")
.set(["e"], "e")
.commit();
assert(res[0].ok);

const actual = await kv.tree();

assertEquals(actual, {
children: [
{
part: "a",
hasValue: true,
children: [
{
part: "b",
hasValue: true,
children: [{ part: "c", hasValue: true }],
},
{
part: "d",
children: [{
part: "f",
children: [{ part: "g", hasValue: true }],
}],
},
{ part: "h", hasValue: true },
],
},
{ part: "e", hasValue: true },
],
});
kv.close();

return cleanup();
},
});

Deno.test({
name: "kvToolbox - open and close with encryption",
async fn() {
const path = await getPath();
const key = generateKey();
const kv = await openKvToolbox({ path, encryptWith: key });
kv.close();
return cleanup();
},
});

Deno.test({
name: "kvToolbox - encrypt/decrypt blob - Uint8Array",
async fn() {
const path = await getPath();
const encryptWith = generateKey();
const kv = await openKvToolbox({ path, encryptWith });
const value = globalThis.crypto.getRandomValues(new Uint8Array(65_536));
const res = await kv.setBlob(["example"], value);
assert(res.ok);
const actual = await kv.getBlob(["example"]);
assert(actual.value);
assertEquals(actual.versionstamp, res.versionstamp);
assert(timingSafeEqual(actual.value, value));
kv.close();
return cleanup();
},
});

Deno.test({
name: "kvToolbox - encrypt/decrypt blob - Uint8Array - bypass encryption set",
async fn() {
const path = await getPath();
const encryptWith = generateKey();
const kv = await openKvToolbox({ path, encryptWith });
const value = globalThis.crypto.getRandomValues(new Uint8Array(65_536));
const res = await kv.setBlob(["example"], value, { encrypted: false });
assert(res.ok);
const actual = await kv.getBlob(["example"]);
assertEquals(actual.value, null);
kv.close();
return cleanup();
},
});

Deno.test({
name:
"kvToolbox - encrypt/decrypt blob - Uint8Array - bypass encryption get and set",
async fn() {
const path = await getPath();
const encryptWith = generateKey();
const kv = await openKvToolbox({ path, encryptWith });
const value = globalThis.crypto.getRandomValues(new Uint8Array(65_536));
const res = await kv.setBlob(["example"], value, { encrypted: false });
assert(res.ok);
const actual = await kv.getBlob(["example"], { encrypted: false });
assert(actual.value);
assertEquals(actual.versionstamp, res.versionstamp);
assert(timingSafeEqual(actual.value, value));
kv.close();
return cleanup();
},
});
Loading

0 comments on commit 1e0842f

Please sign in to comment.