Setup repo: migrate architecture specs, code stubs, and tasks from alkhub_ts

Copy architecture docs, ADRs, storage domain specs, research, reviews,
and 56 storage architecture tasks from the alkhub_ts monorepo. Adapt for
standalone @alkdev/hub repo structure (src/ not packages/hub/).

Sanitize all sensitive information:
- Replace private IPs (10.0.0.1) with localhost defaults
- Remove internal server hostnames (dev1, ns528096)
- Replace /workspace/ private paths with npm package references
- Remove hardcoded credentials from examples
- Rewrite infrastructure.md without private network details

Add Deno project scaffolding: deno.json (pinned deps), .gitignore,
AGENTS.md, entry point. Migrate existing code stubs (crypto, config
types, logger) with updated import paths.
This commit is contained in:
2026-05-25 10:56:32 +00:00
parent 3e3f12d2d5
commit 2b63cda1c7
120 changed files with 11714 additions and 2 deletions

169
src/config/types.ts Normal file
View File

@@ -0,0 +1,169 @@
/**
* Configuration type schemas for alkhub.
*
* The canonical architecture specification is docs/architecture/hub-config.md.
* This file defines the TypeBox schemas that match that spec.
*
* Schema hierarchy:
* BaseConfig — shared by hub + spoke (logLevel, mcpServers, operationDirectories)
* HubConfig — extends BaseConfig with infrastructure (postgres, redis, http, auth, encryptionKeys)
* SpokeConfig — extends BaseConfig with hub connection (url, auth.tokenFile)
*
* Important: These schemas define the DECRYPTED shapes. In the config file,
* postgres, redis, and encryptionKeys are { _encrypted: EncryptedData } objects.
* The loadConfig function decrypts them before validating against these schemas.
*/
import { Type, type Static } from "@alkdev/typebox";
// ---------------------------------------------------------------------------
// MCP Server Config (shared — used by both hub and spoke)
// ---------------------------------------------------------------------------
/**
* MCP server connection config. Two transports:
* - stdio: spawn a child process (command, args, env, cwd)
* - HTTP: connect to a URL (url, headers)
*
* Important: The `env` field passes environment variables to the MCP server
* CHILD PROCESS, not to the hub. The hub never reads Deno.env for secrets.
* See hub-config.md §D6.
*/
export const MCPServerConfig = Type.Union([
Type.Object({
command: Type.String(),
args: Type.Optional(Type.Array(Type.String())),
/** Env vars for the MCP server child process. NOT the hub's env. */
env: Type.Optional(Type.Record(Type.String(), Type.String())),
cwd: Type.Optional(Type.String()),
}),
Type.Object({
url: Type.String(),
headers: Type.Optional(Type.Record(Type.String(), Type.String())),
}),
]);
export type MCPServerConfig = Static<typeof MCPServerConfig>;
// ---------------------------------------------------------------------------
// BaseConfig (shared: hub + spoke)
// ---------------------------------------------------------------------------
export const BaseConfig = Type.Object({
$schema: Type.Optional(Type.String()),
logLevel: Type.Optional(Type.Union([
Type.Literal("DEBUG"),
Type.Literal("INFO"),
Type.Literal("WARN"),
Type.Literal("ERROR"),
])),
mcpServers: Type.Optional(Type.Record(Type.String(), MCPServerConfig)),
operationDirectories: Type.Optional(Type.Array(Type.String())),
});
export type BaseConfig = Static<typeof BaseConfig>;
// ---------------------------------------------------------------------------
// Hub-specific config sections (decrypted shapes)
// ---------------------------------------------------------------------------
export const PostgresConfig = Type.Object({
host: Type.String({ default: "127.0.0.1" }),
port: Type.Number({ default: 5432 }),
database: Type.String({ default: "alkdev" }),
user: Type.String(),
password: Type.String(),
ssl: Type.Optional(Type.Boolean()), // true = enable SSL with default CA verification; detailed config TBD
maxConnections: Type.Optional(Type.Number({ default: 10 })),
});
export type PostgresConfig = Static<typeof PostgresConfig>;
export const RedisConfig = Type.Object({
host: Type.String({ default: "127.0.0.1" }),
port: Type.Number({ default: 6379 }),
password: Type.Optional(Type.String()),
db: Type.Optional(Type.Number({ default: 0 })),
});
export type RedisConfig = Static<typeof RedisConfig>;
export const HttpConfig = Type.Object({
host: Type.String({ default: "0.0.0.0" }),
port: Type.Number({ default: 3000 }),
});
export type HttpConfig = Static<typeof HttpConfig>;
export const AuthConfig = Type.Object({
apiKeyCacheTtl: Type.Number({ default: 300 }),
sessionTokenTtl: Type.Number({ default: 3600 }),
});
export type AuthConfig = Static<typeof AuthConfig>;
// ---------------------------------------------------------------------------
// HubConfig (extends BaseConfig)
// ---------------------------------------------------------------------------
/**
* Full hub configuration, after decryption and validation.
*
* In the config file, postgres, redis, and encryptionKeys are
* { _encrypted: EncryptedData } objects. After loadConfig decrypts them,
* the shapes match PostgresConfig, RedisConfig, and the multi-key format
* string respectively.
*/
export const HubConfig = Type.Intersect([
BaseConfig,
Type.Object({
http: Type.Optional(HttpConfig),
postgres: PostgresConfig,
redis: Type.Optional(RedisConfig),
/** Multi-key encryption format: "v1:base64,v2:base64,..." */
encryptionKeys: Type.String(),
auth: Type.Optional(AuthConfig),
/** Development mode: pretty-print logging, stricter error handling. NOT an env var. */
development: Type.Optional(Type.Boolean({ default: false })),
}),
]);
export type HubConfig = Static<typeof HubConfig>;
// ---------------------------------------------------------------------------
// SpokeConfig (extends BaseConfig)
// ---------------------------------------------------------------------------
/**
* Spoke configuration. The spoke does NOT use encrypted config fields —
* it reads its auth token from a file reference (Docker secret or local file).
*
* This shape is subject to change based on spoke auth design (see spoke-runner.md).
*/
export const SpokeConfig = Type.Intersect([
BaseConfig,
Type.Object({
hub: Type.Object({
url: Type.String(),
auth: Type.Object({
/** Path to file containing auth token (Docker secret or mounted file). */
tokenFile: Type.String(),
}),
}),
}),
]);
export type SpokeConfig = Static<typeof SpokeConfig>;
// ---------------------------------------------------------------------------
// Backwards compatibility: Config alias (was spoke-only, now BaseConfig)
// ---------------------------------------------------------------------------
/**
* @deprecated Use BaseConfig, HubConfig, or SpokeConfig instead.
* Kept for backwards compatibility with existing code that references `Config`.
*/
export const Config = BaseConfig;
/** @deprecated Use BaseConfig instead. */
export type Config = BaseConfig;

119
src/crypto/mod.ts Normal file
View File

@@ -0,0 +1,119 @@
import { encodeBase64, decodeBase64 } from "@std/encoding";
/**
* Encrypted data structure with key version support
*/
export interface EncryptedData {
keyVersion: number;
salt: string;
iv: string;
data: string;
}
/**
* Generate a random salt (16 bytes recommended for PBKDF2)
*/
function generateSalt(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(16));
}
/**
* Generate a random IV (12 bytes recommended for AES-GCM)
*/
function generateIV(): Uint8Array {
return crypto.getRandomValues(new Uint8Array(12));
}
/**
* Derive a key from a password using PBKDF2
*/
async function deriveKey(
password: string,
salt: Uint8Array,
keyVersion: number = 1
): Promise<CryptoKey> {
const encoder = new TextEncoder();
const keyMaterial = await crypto.subtle.importKey(
"raw",
encoder.encode(password),
{ name: "PBKDF2" },
false,
["deriveBits", "deriveKey"]
);
// Adjust iterations based on key version (for future rotation)
const iterations = keyVersion === 1 ? 100000 : 200000;
return crypto.subtle.deriveKey(
{
name: "PBKDF2",
salt: new Uint8Array(salt),
iterations,
hash: "SHA-256"
},
keyMaterial,
{ name: "AES-GCM", length: 256 },
true,
["encrypt", "decrypt"]
);
}
/**
* Encrypt data using AES-GCM with key version support
*/
export async function encrypt(
plaintext: string,
password:string,
keyVersion: number = 1
): Promise<EncryptedData> {
const encoder = new TextEncoder();
const salt = generateSalt();
const iv = generateIV();
const key = await deriveKey(password, salt, keyVersion);
const encrypted = await crypto.subtle.encrypt(
{ name: "AES-GCM", iv: new Uint8Array(iv) },
key,
encoder.encode(plaintext)
);
return {
keyVersion,
salt: encodeBase64(salt),
iv: encodeBase64(iv),
data: encodeBase64(new Uint8Array(encrypted))
};
}
/**
* Decrypt data using AES-GCM with key version support
*/
export async function decrypt(encryptedData: EncryptedData, password:string): Promise<string> {
const decoder = new TextDecoder();
const salt = decodeBase64(encryptedData.salt);
const iv = decodeBase64(encryptedData.iv);
const data = decodeBase64(encryptedData.data);
const key = await deriveKey(password, salt, encryptedData.keyVersion);
try {
const decrypted = await crypto.subtle.decrypt(
{ name: "AES-GCM", iv: new Uint8Array(iv) },
key,
data
);
return decoder.decode(decrypted);
} catch (_error) {
throw new Error("Decryption failed: Invalid data or key");
}
}
/**
* Generate a secure random encryption key
*/
export function generateEncryptionKey(): string {
const bytes = crypto.getRandomValues(new Uint8Array(32));
return encodeBase64(bytes);
}

27
src/logger/mod.ts Normal file
View File

@@ -0,0 +1,27 @@
import {
configure as configureLogTape,
getLogger as getLoggerLogTape,
getConsoleSink,
type Logger
} from "@logtape/logtape";
// Re-export Logger type
export type { Logger };
export async function configure(): Promise<void> {
await configureLogTape({
sinks: {
console: getConsoleSink()
},
loggers: [
{
category: ["logtape", "meta"],
sinks: ["console"]
}
]
});
}
export function getLogger(category: string|string[]): Logger {
return getLoggerLogTape(category);
}

6
src/mod.ts Normal file
View File

@@ -0,0 +1,6 @@
// @alkdev/hub — Hub API server for the alk.dev platform
// Hono HTTP server, storage (Drizzle+Postgres), auth, coordination, Redis events
export * from "./config/types.ts";
export * from "./logger/mod.ts";
export * from "./crypto/mod.ts";