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:
169
src/config/types.ts
Normal file
169
src/config/types.ts
Normal 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
119
src/crypto/mod.ts
Normal 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
27
src/logger/mod.ts
Normal 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
6
src/mod.ts
Normal 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";
|
||||
Reference in New Issue
Block a user