keri-ts 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/README.md +70 -0
- package/esm/_dnt.polyfills.js +127 -0
- package/esm/_dnt.shims.js +61 -0
- package/esm/app/cli/agent.js +37 -0
- package/esm/app/cli/cli-node.js +9 -0
- package/esm/app/cli/cli.js +195 -0
- package/esm/app/cli/db-dump.js +68 -0
- package/esm/app/cli/init.js +75 -0
- package/esm/app/server.js +77 -0
- package/esm/core/bytes.js +39 -0
- package/esm/core/errors.js +26 -0
- package/esm/core/index.js +7 -0
- package/esm/db/basing.js +168 -0
- package/esm/db/core/db.js +19 -0
- package/esm/db/core/lmdber.js +474 -0
- package/esm/db/core/path-manager.js +450 -0
- package/esm/db/index.js +4 -0
- package/esm/npm/index.js +4 -0
- package/esm/package.json +3 -0
- package/package.json +57 -0
- package/types/_dnt.polyfills.d.ts +101 -0
- package/types/_dnt.polyfills.d.ts.map +1 -0
- package/types/_dnt.shims.d.ts +6 -0
- package/types/_dnt.shims.d.ts.map +1 -0
- package/types/app/cli/agent.d.ts +9 -0
- package/types/app/cli/agent.d.ts.map +1 -0
- package/types/app/cli/cli-node.d.ts +2 -0
- package/types/app/cli/cli-node.d.ts.map +1 -0
- package/types/app/cli/cli.d.ts +7 -0
- package/types/app/cli/cli.d.ts.map +1 -0
- package/types/app/cli/db-dump.d.ts +11 -0
- package/types/app/cli/db-dump.d.ts.map +1 -0
- package/types/app/cli/init.d.ts +3 -0
- package/types/app/cli/init.d.ts.map +1 -0
- package/types/app/server.d.ts +8 -0
- package/types/app/server.d.ts.map +1 -0
- package/types/core/bytes.d.ts +17 -0
- package/types/core/bytes.d.ts.map +1 -0
- package/types/core/errors.d.ts +19 -0
- package/types/core/errors.d.ts.map +1 -0
- package/types/core/index.d.ts +8 -0
- package/types/core/index.d.ts.map +1 -0
- package/types/db/basing.d.ts +80 -0
- package/types/db/basing.d.ts.map +1 -0
- package/types/db/core/db.d.ts +5 -0
- package/types/db/core/db.d.ts.map +1 -0
- package/types/db/core/lmdber.d.ts +135 -0
- package/types/db/core/lmdber.d.ts.map +1 -0
- package/types/db/core/path-manager.d.ts +92 -0
- package/types/db/core/path-manager.d.ts.map +1 -0
- package/types/db/index.d.ts +5 -0
- package/types/db/index.d.ts.map +1 -0
- package/types/npm/index.d.ts +5 -0
- package/types/npm/index.d.ts.map +1 -0
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
import * as dntShim from "../../_dnt.shims.js";
|
|
2
|
+
import { Command } from "commander";
|
|
3
|
+
import { action } from "effection";
|
|
4
|
+
import { agentCommand } from "./agent.js";
|
|
5
|
+
import { dumpEvts } from "./db-dump.js";
|
|
6
|
+
import { initCommand } from "./init.js";
|
|
7
|
+
/**
|
|
8
|
+
* Promise to Structured Concurrency Helper: Convert Promise to Effection Operation
|
|
9
|
+
* This allows us to integrate promise-based APIs (like Commander) into Effection's structured concurrency
|
|
10
|
+
*/
|
|
11
|
+
function* toOp(promise) {
|
|
12
|
+
return yield* action((resolve, reject) => {
|
|
13
|
+
promise.then(resolve, reject);
|
|
14
|
+
return () => { }; // Cleanup function (can add abort logic if needed)
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create the CLI program with action handlers that signal command execution
|
|
19
|
+
*/
|
|
20
|
+
function createCLIProgram(context) {
|
|
21
|
+
const program = new Command();
|
|
22
|
+
program.name("kli").version("0.0.2").description("KERI TypeScript CLI");
|
|
23
|
+
// Prevent Commander from exiting automatically so we can run Effection operations
|
|
24
|
+
program.exitOverride();
|
|
25
|
+
program
|
|
26
|
+
.command("init")
|
|
27
|
+
.description("Create a database and keystore")
|
|
28
|
+
.option("-n, --name <name>", "Keystore name and file location of KERI keystore (required)")
|
|
29
|
+
.option("-b, --base <base>", "Additional optional prefix to file location of KERI keystore")
|
|
30
|
+
.option("-t, --temp", "Create a temporary keystore, used for testing")
|
|
31
|
+
.option("-s, --salt <salt>", "Qualified base64 salt for creating key pairs")
|
|
32
|
+
.option("-c, --config-dir <dir>", "Directory override for configuration data")
|
|
33
|
+
.option("--config-file <file>", "Configuration filename override")
|
|
34
|
+
.option("-p, --passcode <passcode>", "22 character encryption passcode for keystore (is not saved)")
|
|
35
|
+
.option("--nopasscode", "Create an unencrypted keystore")
|
|
36
|
+
.option("-a, --aeid <aeid>", "Qualified base64 of non-transferable identifier prefix for authentication and encryption of secrets in keystore")
|
|
37
|
+
.option("-e, --seed <seed>", "Qualified base64 private-signing key (seed) for the aeid from which the private decryption key may be derived")
|
|
38
|
+
.action((options) => {
|
|
39
|
+
// Store command info in context for execution within Effection
|
|
40
|
+
context.command = "init";
|
|
41
|
+
context.args = {
|
|
42
|
+
name: options.name,
|
|
43
|
+
base: options.base,
|
|
44
|
+
temp: options.temp || false,
|
|
45
|
+
salt: options.salt,
|
|
46
|
+
configDir: options.configDir,
|
|
47
|
+
configFile: options.configFile,
|
|
48
|
+
passcode: options.passcode,
|
|
49
|
+
nopasscode: options.nopasscode || false,
|
|
50
|
+
aeid: options.aeid,
|
|
51
|
+
seed: options.seed,
|
|
52
|
+
};
|
|
53
|
+
// Return immediately - actual execution happens in kli operation
|
|
54
|
+
return Promise.resolve();
|
|
55
|
+
});
|
|
56
|
+
program
|
|
57
|
+
.command("agent")
|
|
58
|
+
.description("Start the KERI agent server")
|
|
59
|
+
.option("-p, --port <port>", "Port number for the server (default: 8000)", "8000")
|
|
60
|
+
.action(function () {
|
|
61
|
+
const options = this.opts();
|
|
62
|
+
context.command = "agent";
|
|
63
|
+
context.args = {
|
|
64
|
+
port: options.port ? Number(options.port) : 8000,
|
|
65
|
+
};
|
|
66
|
+
return Promise.resolve();
|
|
67
|
+
});
|
|
68
|
+
program
|
|
69
|
+
.command("incept")
|
|
70
|
+
.description("Create a new identifier")
|
|
71
|
+
.action(() => {
|
|
72
|
+
context.command = "incept";
|
|
73
|
+
context.args = {};
|
|
74
|
+
return Promise.resolve();
|
|
75
|
+
});
|
|
76
|
+
program
|
|
77
|
+
.command("rotate")
|
|
78
|
+
.description("Rotate keys for an identifier")
|
|
79
|
+
.action(() => {
|
|
80
|
+
context.command = "rotate";
|
|
81
|
+
context.args = {};
|
|
82
|
+
return Promise.resolve();
|
|
83
|
+
});
|
|
84
|
+
program
|
|
85
|
+
.command("interact")
|
|
86
|
+
.description("Create an interaction event")
|
|
87
|
+
.action(() => {
|
|
88
|
+
context.command = "interact";
|
|
89
|
+
context.args = {};
|
|
90
|
+
return Promise.resolve();
|
|
91
|
+
});
|
|
92
|
+
program
|
|
93
|
+
.command("witness")
|
|
94
|
+
.description("Start a witness server")
|
|
95
|
+
.action(() => {
|
|
96
|
+
context.command = "witness";
|
|
97
|
+
context.args = {};
|
|
98
|
+
return Promise.resolve();
|
|
99
|
+
});
|
|
100
|
+
// Create db command with dump subcommand using chained .command() pattern
|
|
101
|
+
const dbCommand = program.command("db").description("Database operations");
|
|
102
|
+
dbCommand
|
|
103
|
+
.command("dump")
|
|
104
|
+
.description("Dump database contents")
|
|
105
|
+
.requiredOption("-n, --name <name>", "Database name")
|
|
106
|
+
.option("-b, --base <base>", "Additional optional prefix to database path")
|
|
107
|
+
.option("-t, --temp", "Use temporary database")
|
|
108
|
+
.action((options) => {
|
|
109
|
+
context.command = "db.dump";
|
|
110
|
+
context.args = {
|
|
111
|
+
name: options.name,
|
|
112
|
+
base: options.base,
|
|
113
|
+
temp: options.temp || false,
|
|
114
|
+
};
|
|
115
|
+
return Promise.resolve();
|
|
116
|
+
});
|
|
117
|
+
return program;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Stub command operations (to be implemented)
|
|
121
|
+
* These are placeholder operations that will be fully implemented later
|
|
122
|
+
*/
|
|
123
|
+
// deno-lint-ignore require-yield
|
|
124
|
+
function* inceptCommand(_args) {
|
|
125
|
+
console.log("kli incept command - coming soon!");
|
|
126
|
+
}
|
|
127
|
+
// deno-lint-ignore require-yield
|
|
128
|
+
function* rotateCommand(_args) {
|
|
129
|
+
console.log("kli rotate command - coming soon!");
|
|
130
|
+
}
|
|
131
|
+
// deno-lint-ignore require-yield
|
|
132
|
+
function* interactCommand(_args) {
|
|
133
|
+
console.log("kli interact command - coming soon!");
|
|
134
|
+
}
|
|
135
|
+
// deno-lint-ignore require-yield
|
|
136
|
+
function* witnessCommand(_args) {
|
|
137
|
+
console.log("kli witness command - coming soon!");
|
|
138
|
+
}
|
|
139
|
+
/**
|
|
140
|
+
* Command handler registry - maps command names to Effection operations
|
|
141
|
+
*/
|
|
142
|
+
const commandHandlers = new Map([
|
|
143
|
+
["init", (args) => initCommand(args)],
|
|
144
|
+
["agent", (args) => agentCommand(args)],
|
|
145
|
+
["incept", (args) => inceptCommand(args)],
|
|
146
|
+
["rotate", (args) => rotateCommand(args)],
|
|
147
|
+
["interact", (args) => interactCommand(args)],
|
|
148
|
+
["witness", (args) => witnessCommand(args)],
|
|
149
|
+
["db.dump", (args) => dumpEvts(args)],
|
|
150
|
+
]);
|
|
151
|
+
/**
|
|
152
|
+
* Main CLI operation - runs within Effection's structured concurrency
|
|
153
|
+
* This is the outermost runtime, not JavaScript's event loop
|
|
154
|
+
*/
|
|
155
|
+
export function* kli(args = []) {
|
|
156
|
+
// Create a context for command execution
|
|
157
|
+
const context = {};
|
|
158
|
+
// Use Commander.js for all command parsing
|
|
159
|
+
const program = createCLIProgram(context);
|
|
160
|
+
try {
|
|
161
|
+
// Parse arguments - Commander expects full argv or args array
|
|
162
|
+
// In Deno, Deno.args gives us the arguments without the executable info
|
|
163
|
+
const argsToParse = args.length > 0 ? args : dntShim.Deno.args;
|
|
164
|
+
program.parse(argsToParse, { from: "user" });
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
// Handle Commander-specific errors
|
|
168
|
+
if (error && typeof error === "object" && "code" in error) {
|
|
169
|
+
const commanderError = error;
|
|
170
|
+
// Help was requested - Commander already printed it, just return
|
|
171
|
+
if (commanderError.code === "commander.help" ||
|
|
172
|
+
commanderError.code === "commander.helpDisplayed") {
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
// Unknown command or other parsing errors - Commander already printed the error
|
|
176
|
+
if (commanderError.code === "commander.unknownCommand" ||
|
|
177
|
+
commanderError.code === "commander.missingArgument") {
|
|
178
|
+
// Commander already printed the error message, just exit gracefully
|
|
179
|
+
return;
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
// For other errors, log and rethrow
|
|
183
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
184
|
+
console.error(`Error: ${message}`);
|
|
185
|
+
throw error;
|
|
186
|
+
}
|
|
187
|
+
// Execute the appropriate command operation based on context
|
|
188
|
+
if (context.command && context.args) {
|
|
189
|
+
const handler = commandHandlers.get(context.command);
|
|
190
|
+
if (handler) {
|
|
191
|
+
// Execute the command operation within Effection's structured concurrency
|
|
192
|
+
yield* handler(context.args);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Database dump command for CLI
|
|
3
|
+
*
|
|
4
|
+
* Dumps the contents of Baser.evts sub-database to console in prettified table format
|
|
5
|
+
*/
|
|
6
|
+
import { displayStr } from "../../core/bytes.js";
|
|
7
|
+
import { createBaser } from "../../db/basing.js";
|
|
8
|
+
/**
|
|
9
|
+
* Dump the evts sub-database to console
|
|
10
|
+
*/
|
|
11
|
+
export function* dumpEvts(args) {
|
|
12
|
+
const name = args.name;
|
|
13
|
+
const base = args.base;
|
|
14
|
+
const temp = args.temp;
|
|
15
|
+
const readonly = true; // Always open readonly for dump
|
|
16
|
+
if (!name) {
|
|
17
|
+
console.error("Error: --name is required");
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
console.log(`Dumping database ${name} from ${base} in temp mode: ${temp}`);
|
|
21
|
+
const options = {
|
|
22
|
+
name,
|
|
23
|
+
base,
|
|
24
|
+
temp,
|
|
25
|
+
reopen: true,
|
|
26
|
+
readonly,
|
|
27
|
+
dupsort: false,
|
|
28
|
+
};
|
|
29
|
+
const baser = yield* createBaser(options);
|
|
30
|
+
try {
|
|
31
|
+
// get database version
|
|
32
|
+
const version = baser.getVer();
|
|
33
|
+
console.log(`Database version: ${version}`);
|
|
34
|
+
// Get count
|
|
35
|
+
const count = baser.cntEvts();
|
|
36
|
+
console.log(`\nBaser.evts sub-database dump (${count} entries)\n`);
|
|
37
|
+
console.log("=".repeat(137));
|
|
38
|
+
// Print header
|
|
39
|
+
console.log(`${"Key".padEnd(89)} | ${"Value (UTF-8)".padEnd(45)}`);
|
|
40
|
+
console.log("-".repeat(137));
|
|
41
|
+
// Iterate and print entries (empty top = all items)
|
|
42
|
+
const iter = baser.getAllEvtsIter(new Uint8Array(0));
|
|
43
|
+
let entryCount = 0;
|
|
44
|
+
let keyColSize = 50;
|
|
45
|
+
let valColSize = 50;
|
|
46
|
+
for (const [keyBytes, valBytes] of iter) {
|
|
47
|
+
entryCount++;
|
|
48
|
+
const keyStr = displayStr(keyBytes);
|
|
49
|
+
if (keyStr.length > keyColSize) {
|
|
50
|
+
keyColSize = keyStr.length;
|
|
51
|
+
}
|
|
52
|
+
const valStr = displayStr(valBytes, 45);
|
|
53
|
+
if (valStr.length > valColSize) {
|
|
54
|
+
valColSize = valStr.length;
|
|
55
|
+
}
|
|
56
|
+
console.log(`${keyStr.padEnd(50)} | ${valStr.padEnd(45)}`);
|
|
57
|
+
}
|
|
58
|
+
console.log("=".repeat(137));
|
|
59
|
+
console.log(`\nTotal entries: ${entryCount}`);
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`Error dumping database: ${error}`);
|
|
63
|
+
throw error;
|
|
64
|
+
}
|
|
65
|
+
finally {
|
|
66
|
+
yield* baser.close();
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
function printInitHelp() {
|
|
2
|
+
console.log(`
|
|
3
|
+
kli init - Create a database and keystore
|
|
4
|
+
|
|
5
|
+
Usage: kli init [options]
|
|
6
|
+
|
|
7
|
+
Options:
|
|
8
|
+
--name, -n <name> Keystore name and file location of KERI keystore (required)
|
|
9
|
+
--base, -b <base> Additional optional prefix to file location of KERI keystore
|
|
10
|
+
--temp, -t Create a temporary keystore, used for testing
|
|
11
|
+
--salt, -s <salt> Qualified base64 salt for creating key pairs
|
|
12
|
+
--config-dir, -c <dir> Directory override for configuration data
|
|
13
|
+
--config-file <file> Configuration filename override
|
|
14
|
+
--passcode, -p <passcode> 22 character encryption passcode for keystore (is not saved)
|
|
15
|
+
--nopasscode Create an unencrypted keystore
|
|
16
|
+
--aeid, -a <aeid> Qualified base64 of non-transferable identifier prefix for authentication and encryption of secrets in keystore
|
|
17
|
+
--seed, -e <seed> Qualified base64 private-signing key (seed) for the aeid from which the private decryption key may be derived
|
|
18
|
+
--help, -h Show this help message
|
|
19
|
+
`);
|
|
20
|
+
}
|
|
21
|
+
// TODO remove this ignore once init is finished
|
|
22
|
+
// deno-lint-ignore require-yield
|
|
23
|
+
export function* initCommand(args) {
|
|
24
|
+
// Check for help flag
|
|
25
|
+
if (args.help || args.h) {
|
|
26
|
+
printInitHelp();
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
// Extract values from args (already parsed by Cliffy or test mocks)
|
|
30
|
+
const initArgs = {
|
|
31
|
+
name: args.name,
|
|
32
|
+
base: args.base,
|
|
33
|
+
temp: args.temp,
|
|
34
|
+
salt: args.salt,
|
|
35
|
+
configDir: args.configDir,
|
|
36
|
+
configFile: args.configFile,
|
|
37
|
+
passcode: args.passcode,
|
|
38
|
+
nopasscode: args.nopasscode,
|
|
39
|
+
aeid: args.aeid,
|
|
40
|
+
seed: args.seed,
|
|
41
|
+
};
|
|
42
|
+
// Validate required name
|
|
43
|
+
const name = initArgs.name;
|
|
44
|
+
if (!name || name === "") {
|
|
45
|
+
throw new Error("Name is required and cannot be empty");
|
|
46
|
+
}
|
|
47
|
+
const base = initArgs.base || "";
|
|
48
|
+
const temp = initArgs.temp || false;
|
|
49
|
+
let bran = initArgs.passcode;
|
|
50
|
+
const configFile = initArgs.configFile;
|
|
51
|
+
const configDir = initArgs.configDir;
|
|
52
|
+
const nopasscode = initArgs.nopasscode || false;
|
|
53
|
+
// Handle passcode input if not provided and not using nopasscode
|
|
54
|
+
if (!nopasscode && !bran) {
|
|
55
|
+
console.log("Creating encrypted keystore, please enter your 22 character passcode:");
|
|
56
|
+
// For now, we'll use a simple prompt since Deno doesn't have getpass equivalent
|
|
57
|
+
// In a real implementation, you'd want to use a proper password input library
|
|
58
|
+
const passcode = prompt("Passcode: ");
|
|
59
|
+
const retry = prompt("Re-enter passcode: ");
|
|
60
|
+
if (passcode !== retry) {
|
|
61
|
+
throw new Error("Passcodes do not match");
|
|
62
|
+
}
|
|
63
|
+
bran = passcode || undefined;
|
|
64
|
+
}
|
|
65
|
+
// TODO: Implement actual keystore and database creation
|
|
66
|
+
// This is a stub implementation that mirrors the KERIpy structure
|
|
67
|
+
console.log("KERI Keystore created at: [stub - keystore path]");
|
|
68
|
+
console.log("KERI Database created at: [stub - database path]");
|
|
69
|
+
console.log("KERI Credential Store created at: [stub - credential store path]");
|
|
70
|
+
if (initArgs.aeid) {
|
|
71
|
+
console.log("\taeid:", initArgs.aeid);
|
|
72
|
+
}
|
|
73
|
+
console.log("\nInitialization complete!");
|
|
74
|
+
console.log("Note: This is a stub implementation. Full keystore and database creation will be implemented in future versions.");
|
|
75
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import * as dntShim from "../_dnt.shims.js";
|
|
2
|
+
import { action } from "effection";
|
|
3
|
+
import { openDB, readValue, writeValue } from "../db/core/db.js";
|
|
4
|
+
/**
|
|
5
|
+
* Start HTTP server with Effection as the outermost runtime.
|
|
6
|
+
* Each request is spawned as a separate Effection task, ensuring
|
|
7
|
+
* proper structured concurrency and cleanup.
|
|
8
|
+
*/
|
|
9
|
+
export function* startServer(port = 8000) {
|
|
10
|
+
// openDB is synchronous, so call it directly
|
|
11
|
+
let db;
|
|
12
|
+
try {
|
|
13
|
+
db = openDB();
|
|
14
|
+
}
|
|
15
|
+
catch (error) {
|
|
16
|
+
console.error("Error opening database:", error);
|
|
17
|
+
throw error;
|
|
18
|
+
}
|
|
19
|
+
// Use Deno.serve
|
|
20
|
+
return yield* action((resolve, reject) => {
|
|
21
|
+
const controller = new AbortController();
|
|
22
|
+
const { signal } = controller;
|
|
23
|
+
const server = dntShim.Deno.serve({
|
|
24
|
+
port,
|
|
25
|
+
hostname: "127.0.0.1",
|
|
26
|
+
signal,
|
|
27
|
+
onListen: ({ port }) => console.log(`Server running on http://localhost:${port}`),
|
|
28
|
+
onError: (error) => {
|
|
29
|
+
console.error("Server error:", error);
|
|
30
|
+
return new Response("Internal Server Error", { status: 500 });
|
|
31
|
+
},
|
|
32
|
+
}, (req) => {
|
|
33
|
+
try {
|
|
34
|
+
const url = new URL(req.url);
|
|
35
|
+
if (url.pathname.startsWith("/echo/")) {
|
|
36
|
+
// Extract value from path (everything after "/echo/")
|
|
37
|
+
let val = url.pathname.slice(6); // Trim '/echo/'
|
|
38
|
+
// If no value in path, read from database
|
|
39
|
+
if (!val) {
|
|
40
|
+
const oldVal = readValue(db, "echo");
|
|
41
|
+
val = oldVal ? oldVal : "initial echo";
|
|
42
|
+
}
|
|
43
|
+
// Write the value to database (persist it)
|
|
44
|
+
writeValue(db, "echo", val);
|
|
45
|
+
return new Response(val, {
|
|
46
|
+
status: 200,
|
|
47
|
+
headers: { "Content-Type": "text/plain" },
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
return new Response("Not Found", {
|
|
52
|
+
status: 404,
|
|
53
|
+
headers: { "Content-Type": "text/plain" },
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch (error) {
|
|
58
|
+
return new Response(String(error), { status: 500 });
|
|
59
|
+
}
|
|
60
|
+
});
|
|
61
|
+
// Wait for server to finish
|
|
62
|
+
server.finished.then(resolve).catch(reject);
|
|
63
|
+
// Graceful shutdown logic using signals
|
|
64
|
+
const shutdown = () => {
|
|
65
|
+
console.log("Shutting down server...");
|
|
66
|
+
controller.abort();
|
|
67
|
+
};
|
|
68
|
+
// Deno signal listeners
|
|
69
|
+
dntShim.Deno.addSignalListener("SIGINT", shutdown);
|
|
70
|
+
dntShim.Deno.addSignalListener("SIGTERM", shutdown);
|
|
71
|
+
return () => {
|
|
72
|
+
dntShim.Deno.removeSignalListener("SIGINT", shutdown);
|
|
73
|
+
dntShim.Deno.removeSignalListener("SIGTERM", shutdown);
|
|
74
|
+
controller.abort();
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Check if keyBytes starts with prefixBytes
|
|
3
|
+
*/
|
|
4
|
+
export function startsWith(keyBytes, prefixBytes) {
|
|
5
|
+
if (prefixBytes.length === 0) {
|
|
6
|
+
return true; // Empty prefix matches everything
|
|
7
|
+
}
|
|
8
|
+
if (keyBytes.length < prefixBytes.length) {
|
|
9
|
+
return false;
|
|
10
|
+
}
|
|
11
|
+
return prefixBytes.every((byte, i) => keyBytes[i] === byte);
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Convert bytes to a displayable string for terminal output.
|
|
15
|
+
*
|
|
16
|
+
* Decodes bytes as UTF-8 and replaces non-printable control characters
|
|
17
|
+
* with '.' for readable terminal display. UTF-8 can represent control
|
|
18
|
+
* characters (like \x00, \x09, \x0A) which are valid but not printable.
|
|
19
|
+
*
|
|
20
|
+
* @param bytes - Bytes to convert to display string
|
|
21
|
+
* @param maxLength - Optional maximum length; if exceeded, truncates and adds "..."
|
|
22
|
+
* @returns Displayable string with control characters replaced
|
|
23
|
+
*/
|
|
24
|
+
export function displayStr(bytes, maxLength) {
|
|
25
|
+
try {
|
|
26
|
+
const decoder = new TextDecoder("utf-8", { fatal: false });
|
|
27
|
+
let str = decoder.decode(bytes);
|
|
28
|
+
// Replace control characters (non-printable) for readable terminal output
|
|
29
|
+
str = str.replace(/[\x00-\x1F\x7F-\x9F]/g, ".");
|
|
30
|
+
// Truncate if maxLength is specified
|
|
31
|
+
if (maxLength !== undefined && str.length > maxLength) {
|
|
32
|
+
str = str.substring(0, maxLength - 3) + "...";
|
|
33
|
+
}
|
|
34
|
+
return str;
|
|
35
|
+
}
|
|
36
|
+
catch {
|
|
37
|
+
return `[${bytes.length} bytes]`;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
export class AppError extends Error {
|
|
2
|
+
constructor(message, context) {
|
|
3
|
+
super(message);
|
|
4
|
+
Object.defineProperty(this, "context", {
|
|
5
|
+
enumerable: true,
|
|
6
|
+
configurable: true,
|
|
7
|
+
writable: true,
|
|
8
|
+
value: context
|
|
9
|
+
});
|
|
10
|
+
this.name = globalThis[Symbol.for("import-meta-ponyfill-esmodule")](import.meta).name;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
export class ValidationError extends AppError {
|
|
14
|
+
}
|
|
15
|
+
export class PathError extends AppError {
|
|
16
|
+
}
|
|
17
|
+
export class InvalidPathNameError extends ValidationError {
|
|
18
|
+
}
|
|
19
|
+
export class DatabaseError extends AppError {
|
|
20
|
+
}
|
|
21
|
+
export class DatabaseNotOpenError extends DatabaseError {
|
|
22
|
+
}
|
|
23
|
+
export class DatabaseKeyError extends DatabaseError {
|
|
24
|
+
}
|
|
25
|
+
export class DatabaseOperationError extends DatabaseError {
|
|
26
|
+
}
|
package/esm/db/basing.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Baser - KERI Event Log Database
|
|
3
|
+
*
|
|
4
|
+
* Manages KEL events and related data using composition with LMDBer.
|
|
5
|
+
* Sets up named sub-databases for key event logs.
|
|
6
|
+
*/
|
|
7
|
+
import { DatabaseNotOpenError, DatabaseOperationError, } from "../core/errors.js";
|
|
8
|
+
import { LMDBer } from "./core/lmdber.js";
|
|
9
|
+
/**
|
|
10
|
+
* Baser manages KERI event logs
|
|
11
|
+
* Uses composition with LMDBer instead of inheritance
|
|
12
|
+
*/
|
|
13
|
+
export class Baser {
|
|
14
|
+
constructor(options = {}) {
|
|
15
|
+
Object.defineProperty(this, "lmdber", {
|
|
16
|
+
enumerable: true,
|
|
17
|
+
configurable: true,
|
|
18
|
+
writable: true,
|
|
19
|
+
value: void 0
|
|
20
|
+
});
|
|
21
|
+
// Named sub-databases
|
|
22
|
+
Object.defineProperty(this, "evts", {
|
|
23
|
+
enumerable: true,
|
|
24
|
+
configurable: true,
|
|
25
|
+
writable: true,
|
|
26
|
+
value: void 0
|
|
27
|
+
}); // Events sub-database (dgKey: serialized KEL events)
|
|
28
|
+
// Create LMDBer with composition
|
|
29
|
+
this.lmdber = new LMDBer(options, {
|
|
30
|
+
tailDirPath: Baser.TailDirPath,
|
|
31
|
+
altTailDirPath: Baser.AltTailDirPath,
|
|
32
|
+
tempPrefix: Baser.TempPrefix,
|
|
33
|
+
maxNamedDBs: Baser.MaxNamedDBs,
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
get name() {
|
|
37
|
+
return this.lmdber.name;
|
|
38
|
+
}
|
|
39
|
+
get base() {
|
|
40
|
+
return this.lmdber.base;
|
|
41
|
+
}
|
|
42
|
+
get opened() {
|
|
43
|
+
return this.lmdber.opened;
|
|
44
|
+
}
|
|
45
|
+
get temp() {
|
|
46
|
+
return this.lmdber.temp;
|
|
47
|
+
}
|
|
48
|
+
get path() {
|
|
49
|
+
return this.lmdber.path;
|
|
50
|
+
}
|
|
51
|
+
get env() {
|
|
52
|
+
return this.lmdber.env;
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Reopen the database and initialize sub-databases
|
|
56
|
+
*/
|
|
57
|
+
*reopen(options = {}) {
|
|
58
|
+
const opened = yield* this.lmdber.reopen(options);
|
|
59
|
+
if (!opened) {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
// Open named sub-databases
|
|
63
|
+
// Names end with "." to avoid namespace collisions with Base64 identifier prefixes
|
|
64
|
+
try {
|
|
65
|
+
this.evts = this.lmdber.openDB("evts.", false);
|
|
66
|
+
return this.opened;
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
console.error(`Failed to open Baser sub-databases: ${error}`);
|
|
70
|
+
throw new DatabaseOperationError("Failed to open Baser sub-databases", { cause: error instanceof Error ? error.message : String(error) });
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Close the database
|
|
75
|
+
*/
|
|
76
|
+
*close(clear = false) {
|
|
77
|
+
return yield* this.lmdber.close(clear);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Get version
|
|
81
|
+
*/
|
|
82
|
+
getVer() {
|
|
83
|
+
return this.lmdber.getVer();
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Set version
|
|
87
|
+
*/
|
|
88
|
+
setVer(val) {
|
|
89
|
+
this.lmdber.setVer(val);
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Count entries in evts sub-database
|
|
93
|
+
*/
|
|
94
|
+
cntEvts() {
|
|
95
|
+
return this.lmdber.cnt(this.evts);
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Put value in evts sub-database
|
|
99
|
+
*/
|
|
100
|
+
putEvt(key, val) {
|
|
101
|
+
return this.lmdber.putVal(this.evts, key, val);
|
|
102
|
+
}
|
|
103
|
+
/**
|
|
104
|
+
* Set value in evts sub-database
|
|
105
|
+
*/
|
|
106
|
+
setEvt(key, val) {
|
|
107
|
+
return this.lmdber.setVal(this.evts, key, val);
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Get value from evts sub-database
|
|
111
|
+
*/
|
|
112
|
+
getEvt(key) {
|
|
113
|
+
return this.lmdber.getVal(this.evts, key);
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Delete value from evts sub-database
|
|
117
|
+
*/
|
|
118
|
+
delEvt(key) {
|
|
119
|
+
return this.lmdber.delVal(this.evts, key);
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* Get iterator over items in evts sub-database
|
|
123
|
+
*
|
|
124
|
+
* @param top - Key prefix to filter by (empty to get all items)
|
|
125
|
+
* @returns Generator yielding (key, val) tuples
|
|
126
|
+
*/
|
|
127
|
+
*getAllEvtsIter(top = new Uint8Array(0)) {
|
|
128
|
+
yield* this.lmdber.getTopItemIter(this.evts, top);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
// Class constants
|
|
132
|
+
Object.defineProperty(Baser, "TailDirPath", {
|
|
133
|
+
enumerable: true,
|
|
134
|
+
configurable: true,
|
|
135
|
+
writable: true,
|
|
136
|
+
value: "keri/db"
|
|
137
|
+
});
|
|
138
|
+
Object.defineProperty(Baser, "AltTailDirPath", {
|
|
139
|
+
enumerable: true,
|
|
140
|
+
configurable: true,
|
|
141
|
+
writable: true,
|
|
142
|
+
value: ".keri/db"
|
|
143
|
+
});
|
|
144
|
+
Object.defineProperty(Baser, "TempPrefix", {
|
|
145
|
+
enumerable: true,
|
|
146
|
+
configurable: true,
|
|
147
|
+
writable: true,
|
|
148
|
+
value: "keri_db_"
|
|
149
|
+
});
|
|
150
|
+
Object.defineProperty(Baser, "MaxNamedDBs", {
|
|
151
|
+
enumerable: true,
|
|
152
|
+
configurable: true,
|
|
153
|
+
writable: true,
|
|
154
|
+
value: 96
|
|
155
|
+
});
|
|
156
|
+
/**
|
|
157
|
+
* Create and open a Baser instance.
|
|
158
|
+
*
|
|
159
|
+
* Constructors cannot be async, so call this factory where an opened Baser is required.
|
|
160
|
+
*/
|
|
161
|
+
export function* createBaser(options = {}) {
|
|
162
|
+
const baser = new Baser(options);
|
|
163
|
+
const opened = yield* baser.reopen(options);
|
|
164
|
+
if (!opened) {
|
|
165
|
+
throw new DatabaseNotOpenError("Failed to open Baser");
|
|
166
|
+
}
|
|
167
|
+
return baser;
|
|
168
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { open } from "lmdb";
|
|
2
|
+
// Singleton db-lazy open - not thread safe
|
|
3
|
+
let db = null;
|
|
4
|
+
export function openDB(path = "./data.mdb") {
|
|
5
|
+
if (!db) {
|
|
6
|
+
db = open({ path, mapSize: 2e9 }); // 2GB map; tune for KERI events.
|
|
7
|
+
}
|
|
8
|
+
return db;
|
|
9
|
+
}
|
|
10
|
+
export function readValue(db, key) {
|
|
11
|
+
// sync get
|
|
12
|
+
return db.get(key) ?? null;
|
|
13
|
+
}
|
|
14
|
+
export function writeValue(db, key, value) {
|
|
15
|
+
// sync write
|
|
16
|
+
db.transactionSync(() => {
|
|
17
|
+
db.putSync(key, value);
|
|
18
|
+
});
|
|
19
|
+
}
|