feat(contracts): M1 shared TypeScript contracts and bridge skeleton
This commit is contained in:
parent
4474880562
commit
0042fb3df0
21 changed files with 1653 additions and 0 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,6 +16,7 @@ __pycache__/
|
||||||
node_modules/
|
node_modules/
|
||||||
frontend/dist/
|
frontend/dist/
|
||||||
frontend/.vite/
|
frontend/.vite/
|
||||||
|
packages/*/dist/
|
||||||
coverage/
|
coverage/
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
|
|
||||||
13
package.json
Normal file
13
package.json
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
{
|
||||||
|
"name": "hermes-kanban-miniapp",
|
||||||
|
"private": true,
|
||||||
|
"packageManager": "pnpm@9.15.4",
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"scripts": {
|
||||||
|
"build": "pnpm -r build",
|
||||||
|
"typecheck": "pnpm -r typecheck",
|
||||||
|
"clean": "pnpm -r clean"
|
||||||
|
}
|
||||||
|
}
|
||||||
33
packages/bridge/package.json
Normal file
33
packages/bridge/package.json
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
{
|
||||||
|
"name": "@kanban/bridge",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"dev": "tsx watch src/index.ts",
|
||||||
|
"start": "node dist/index.js",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@kanban/contracts": "workspace:*",
|
||||||
|
"zod": "^3.23.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@types/node": "^20.14.0",
|
||||||
|
"typescript": "^5.5.0",
|
||||||
|
"tsx": "^4.16.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
165
packages/bridge/src/auth.ts
Normal file
165
packages/bridge/src/auth.ts
Normal file
|
|
@ -0,0 +1,165 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Telegram initData validation for bridge
|
||||||
|
// ADR §7: "Server validation steps" (HMAC-SHA256)
|
||||||
|
//
|
||||||
|
// NOTE: The bridge typically trusts the BFF's X-Actor-* headers
|
||||||
|
// rather than validating initData directly. This module exists
|
||||||
|
// for local-dev mode (AUTH_MODE=dev) and as a reference.
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import { createHmac, timingSafeEqual } from 'node:crypto';
|
||||||
|
import type { TelegramInitData } from '@kanban/contracts';
|
||||||
|
|
||||||
|
// ─── Parse initData query string ─────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parse Telegram initData query string into structured object.
|
||||||
|
* initData looks like: "query_id=...&user=...&auth_date=...&hash=..."
|
||||||
|
* The `user` field is URL-encoded JSON.
|
||||||
|
*/
|
||||||
|
export function parseInitData(raw: string): TelegramInitData {
|
||||||
|
const params = new URLSearchParams(raw);
|
||||||
|
|
||||||
|
const hash = params.get('hash');
|
||||||
|
if (!hash) throw new Error('Missing hash in initData');
|
||||||
|
|
||||||
|
const authDateStr = params.get('auth_date');
|
||||||
|
if (!authDateStr) throw new Error('Missing auth_date in initData');
|
||||||
|
const auth_date = parseInt(authDateStr, 10);
|
||||||
|
if (isNaN(auth_date)) throw new Error('Invalid auth_date in initData');
|
||||||
|
|
||||||
|
const userRaw = params.get('user');
|
||||||
|
let user: TelegramInitData['user'];
|
||||||
|
if (userRaw) {
|
||||||
|
try {
|
||||||
|
user = JSON.parse(userRaw);
|
||||||
|
} catch {
|
||||||
|
throw new Error('Invalid user JSON in initData');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
hash,
|
||||||
|
auth_date,
|
||||||
|
query_id: params.get('query_id') ?? undefined,
|
||||||
|
user,
|
||||||
|
chat: params.get('chat') ? JSON.parse(params.get('chat')!) : undefined,
|
||||||
|
chat_type: params.get('chat_type') ?? undefined,
|
||||||
|
chat_instance: params.get('chat_instance') ?? undefined,
|
||||||
|
start_param: params.get('start_param') ?? undefined,
|
||||||
|
can_send_after: params.get('can_send_after')
|
||||||
|
? parseInt(params.get('can_send_after')!, 10)
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Validate initData ───────────────────────────────────────
|
||||||
|
|
||||||
|
export interface ValidateInitDataOptions {
|
||||||
|
/** Telegram bot token (server-side only!) */
|
||||||
|
botToken: string;
|
||||||
|
/** Maximum age of initData in seconds (default: 3600) */
|
||||||
|
maxAgeSeconds?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ValidationResult {
|
||||||
|
valid: boolean;
|
||||||
|
user?: TelegramInitData['user'];
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Validate Telegram initData per the official HMAC-SHA256 flow.
|
||||||
|
*
|
||||||
|
* Steps (from ADR §7):
|
||||||
|
* 1. Parse query-string-like init data.
|
||||||
|
* 2. Remove and store `hash`.
|
||||||
|
* 3. Sort remaining key-value pairs alphabetically.
|
||||||
|
* 4. Join as `key=value` lines separated by \n.
|
||||||
|
* 5. Derive secret key: HMAC-SHA256 with key `WebAppData` and message `botToken`.
|
||||||
|
* 6. Compute HMAC-SHA256 of the data-check string using the derived key.
|
||||||
|
* 7. Compare hex digest to received `hash` using constant-time comparison.
|
||||||
|
* 8. Reject expired data.
|
||||||
|
*/
|
||||||
|
export function validateInitData(
|
||||||
|
raw: string,
|
||||||
|
options: ValidateInitDataOptions
|
||||||
|
): ValidationResult {
|
||||||
|
try {
|
||||||
|
const parsed = parseInitData(raw);
|
||||||
|
const maxAge = options.maxAgeSeconds ?? 3600;
|
||||||
|
|
||||||
|
// Step 8: reject expired data
|
||||||
|
const now = Math.floor(Date.now() / 1000);
|
||||||
|
if (now - parsed.auth_date > maxAge) {
|
||||||
|
return { valid: false, error: 'initData has expired' };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Step 2: remove hash
|
||||||
|
const receivedHash = parsed.hash;
|
||||||
|
|
||||||
|
// Step 3-4: sort remaining pairs and join
|
||||||
|
const params = new URLSearchParams(raw);
|
||||||
|
params.delete('hash');
|
||||||
|
const sortedEntries = [...params.entries()].sort(([a], [b]) =>
|
||||||
|
a.localeCompare(b)
|
||||||
|
);
|
||||||
|
const dataCheckString = sortedEntries
|
||||||
|
.map(([key, value]) => `${key}=${value}`)
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
// Step 5: derive secret key
|
||||||
|
const secretKey = createHmac('sha256', 'WebAppData')
|
||||||
|
.update(options.botToken)
|
||||||
|
.digest();
|
||||||
|
|
||||||
|
// Step 6: compute HMAC
|
||||||
|
const computedHash = createHmac('sha256', secretKey)
|
||||||
|
.update(dataCheckString)
|
||||||
|
.digest('hex');
|
||||||
|
|
||||||
|
// Step 7: constant-time comparison
|
||||||
|
const isValid =
|
||||||
|
computedHash.length === receivedHash.length &&
|
||||||
|
timingSafeEqual(
|
||||||
|
Buffer.from(computedHash, 'hex'),
|
||||||
|
Buffer.from(receivedHash, 'hex')
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
return { valid: false, error: 'HMAC mismatch' };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!parsed.user) {
|
||||||
|
return { valid: false, error: 'Missing user in initData' };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { valid: true, user: parsed.user };
|
||||||
|
} catch (err) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
error: err instanceof Error ? err.message : 'Unknown validation error',
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Dev mode helper ─────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface DevUser {
|
||||||
|
telegramId: number;
|
||||||
|
firstName: string;
|
||||||
|
role: 'operator' | 'admin';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a fake validated user for local dev mode.
|
||||||
|
* Only usable when AUTH_MODE=dev (checked by caller).
|
||||||
|
*/
|
||||||
|
export function createDevUser(overrides?: Partial<DevUser>): DevUser {
|
||||||
|
return {
|
||||||
|
telegramId: 123456789,
|
||||||
|
firstName: 'DevUser',
|
||||||
|
role: 'admin',
|
||||||
|
...overrides,
|
||||||
|
};
|
||||||
|
}
|
||||||
211
packages/bridge/src/hermes-client.ts
Normal file
211
packages/bridge/src/hermes-client.ts
Normal file
|
|
@ -0,0 +1,211 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Hermes CLI client — type-safe wrapper for `hermes kanban ...`
|
||||||
|
// ADR §3.5: "uses stable hermes kanban ... --json CLI commands first"
|
||||||
|
// ADR §3.5: "fixed argv and shell: false"
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import { execFile as execFileCb } from 'node:child_process';
|
||||||
|
import { promisify } from 'node:util';
|
||||||
|
import type {
|
||||||
|
Board,
|
||||||
|
TaskSummary,
|
||||||
|
TaskDetail,
|
||||||
|
} from '@kanban/contracts';
|
||||||
|
|
||||||
|
const execFile = promisify(execFileCb);
|
||||||
|
|
||||||
|
// ─── Types ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface HermesClientConfig {
|
||||||
|
/** Path to hermes binary (default: 'hermes') */
|
||||||
|
bin: string;
|
||||||
|
/** HERMES_HOME for target profile */
|
||||||
|
home: string;
|
||||||
|
/** Default board slug */
|
||||||
|
defaultBoard: string;
|
||||||
|
/** Per-command timeout in ms */
|
||||||
|
timeoutMs: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Allowed commands ────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Command allowlist. Each entry defines:
|
||||||
|
* - subcommand: the `hermes kanban <sub>` string
|
||||||
|
* - requiredArgs: positional args the caller must provide
|
||||||
|
* - optionalFlags: flags that may be appended
|
||||||
|
*
|
||||||
|
* This prevents arbitrary pass-through to the hermes CLI.
|
||||||
|
*/
|
||||||
|
type KanbanSubcommand =
|
||||||
|
| 'boards'
|
||||||
|
| 'list'
|
||||||
|
| 'show'
|
||||||
|
| 'create'
|
||||||
|
| 'comment'
|
||||||
|
| 'assign'
|
||||||
|
| 'promote'
|
||||||
|
| 'block'
|
||||||
|
| 'unblock'
|
||||||
|
| 'archive'
|
||||||
|
| 'dispatch'
|
||||||
|
| 'log';
|
||||||
|
|
||||||
|
const ALLOWED_SUBCOMMANDS = new Set<KanbanSubcommand>([
|
||||||
|
'boards',
|
||||||
|
'list',
|
||||||
|
'show',
|
||||||
|
'create',
|
||||||
|
'comment',
|
||||||
|
'assign',
|
||||||
|
'promote',
|
||||||
|
'block',
|
||||||
|
'unblock',
|
||||||
|
'archive',
|
||||||
|
'dispatch',
|
||||||
|
'log',
|
||||||
|
]);
|
||||||
|
|
||||||
|
// ─── Client ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export class HermesKanbanClient {
|
||||||
|
private readonly config: HermesClientConfig;
|
||||||
|
|
||||||
|
constructor(config: HermesClientConfig) {
|
||||||
|
this.config = config;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Execute a hermes kanban subcommand with --json output.
|
||||||
|
* Uses execFile (shell: false) for safety.
|
||||||
|
*/
|
||||||
|
async exec<T>(
|
||||||
|
subcommand: KanbanSubcommand,
|
||||||
|
args: string[],
|
||||||
|
board?: string
|
||||||
|
): Promise<T> {
|
||||||
|
if (!ALLOWED_SUBCOMMANDS.has(subcommand)) {
|
||||||
|
throw new Error(`Blocked disallowed subcommand: ${subcommand}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const fullArgs = [
|
||||||
|
'kanban',
|
||||||
|
subcommand,
|
||||||
|
'--board', board ?? this.config.defaultBoard,
|
||||||
|
'--json',
|
||||||
|
...args,
|
||||||
|
];
|
||||||
|
|
||||||
|
const env = {
|
||||||
|
...process.env,
|
||||||
|
HERMES_HOME: this.config.home,
|
||||||
|
};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const { stdout, stderr } = await execFile(this.config.bin, fullArgs, {
|
||||||
|
shell: false, // CRITICAL: never use shell
|
||||||
|
timeout: this.config.timeoutMs,
|
||||||
|
env,
|
||||||
|
maxBuffer: 5 * 1024 * 1024, // 5MB
|
||||||
|
});
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.warn(`[hermes-client] stderr: ${redact(stderr)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return JSON.parse(stdout) as T;
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof Error) {
|
||||||
|
// Redact any secrets that might leak into error messages
|
||||||
|
throw new Error(`Hermes CLI error: ${redact(err.message)}`);
|
||||||
|
}
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Convenience methods (M2 will flesh these out) ────────
|
||||||
|
|
||||||
|
async listBoards(): Promise<Board[]> {
|
||||||
|
return this.exec<Board[]>('boards', []);
|
||||||
|
}
|
||||||
|
|
||||||
|
async listTasks(
|
||||||
|
board: string,
|
||||||
|
opts?: { status?: string; assignee?: string; limit?: number }
|
||||||
|
): Promise<TaskSummary[]> {
|
||||||
|
const args: string[] = [];
|
||||||
|
if (opts?.status) args.push('--status', opts.status);
|
||||||
|
if (opts?.assignee) args.push('--assignee', opts.assignee);
|
||||||
|
if (opts?.limit) args.push('--limit', String(opts.limit));
|
||||||
|
return this.exec<TaskSummary[]>('list', args, board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTask(board: string, taskId: string): Promise<TaskDetail> {
|
||||||
|
return this.exec<TaskDetail>('show', [taskId], board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async createTask(
|
||||||
|
board: string,
|
||||||
|
payload: { title: string; body?: string; assignee?: string; priority?: number }
|
||||||
|
): Promise<TaskDetail> {
|
||||||
|
const args = ['--title', payload.title];
|
||||||
|
if (payload.body) args.push('--body', payload.body);
|
||||||
|
if (payload.assignee) args.push('--assignee', payload.assignee);
|
||||||
|
if (payload.priority !== undefined) args.push('--priority', String(payload.priority));
|
||||||
|
return this.exec<TaskDetail>('create', args, board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async addComment(board: string, taskId: string, body: string): Promise<void> {
|
||||||
|
await this.exec('comment', [taskId, '--body', body], board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async assignTask(board: string, taskId: string, assignee: string): Promise<void> {
|
||||||
|
await this.exec('assign', [taskId, '--assignee', assignee], board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async promoteTask(board: string, taskId: string, reason?: string): Promise<void> {
|
||||||
|
const args = [taskId];
|
||||||
|
if (reason) args.push('--reason', reason);
|
||||||
|
await this.exec('promote', args, board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async blockTask(board: string, taskId: string, reason: string): Promise<void> {
|
||||||
|
await this.exec('block', [taskId, '--reason', reason], board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async unblockTask(board: string, taskId: string): Promise<void> {
|
||||||
|
await this.exec('unblock', [taskId], board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async archiveTask(board: string, taskId: string): Promise<void> {
|
||||||
|
await this.exec('archive', [taskId], board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async dispatchBoard(board: string): Promise<void> {
|
||||||
|
await this.exec('dispatch', [], board);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getTaskLog(board: string, taskId: string, lines?: number): Promise<string> {
|
||||||
|
const args = [taskId];
|
||||||
|
if (lines) args.push('--lines', String(lines));
|
||||||
|
const result = await this.exec<{ content?: string; log?: string }>('log', args, board);
|
||||||
|
return result.content ?? result.log ?? '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Redaction helper ────────────────────────────────────────
|
||||||
|
|
||||||
|
const SENSITIVE_PATTERNS = [
|
||||||
|
/token[=:]\s*\S+/gi,
|
||||||
|
/secret[=:]\s*\S+/gi,
|
||||||
|
/password[=:]\s*\S+/gi,
|
||||||
|
/api[_-]?key[=:]\s*\S+/gi,
|
||||||
|
];
|
||||||
|
|
||||||
|
function redact(text: string): string {
|
||||||
|
let result = text;
|
||||||
|
for (const pattern of SENSITIVE_PATTERNS) {
|
||||||
|
result = result.replace(pattern, '[REDACTED]');
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
95
packages/bridge/src/index.ts
Normal file
95
packages/bridge/src/index.ts
Normal file
|
|
@ -0,0 +1,95 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// @kanban/bridge — entry point
|
||||||
|
// Self-hosted Hermes Kanban Bridge (Node.js)
|
||||||
|
// ADR §5: "runs on trusted host near Hermes"
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { HealthData } from '@kanban/contracts';
|
||||||
|
|
||||||
|
// ─── Configuration ───────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface BridgeConfig {
|
||||||
|
host: string;
|
||||||
|
port: number;
|
||||||
|
bridgeToken: string;
|
||||||
|
hermesHome: string;
|
||||||
|
hermesBin: string;
|
||||||
|
commandTimeoutSeconds: number;
|
||||||
|
allowedBoards: string[];
|
||||||
|
logTailMaxLines: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function loadConfig(): BridgeConfig {
|
||||||
|
return {
|
||||||
|
host: process.env.BRIDGE_HOST ?? '127.0.0.1',
|
||||||
|
port: parseInt(process.env.BRIDGE_PORT ?? '8787', 10),
|
||||||
|
bridgeToken: requireEnv('BRIDGE_TOKEN'),
|
||||||
|
hermesHome: process.env.HERMES_HOME ?? '',
|
||||||
|
hermesBin: process.env.HERMES_BIN ?? 'hermes',
|
||||||
|
commandTimeoutSeconds: parseInt(process.env.COMMAND_TIMEOUT_SECONDS ?? '30', 10),
|
||||||
|
allowedBoards: (process.env.BRIDGE_ALLOWED_BOARDS ?? 'default')
|
||||||
|
.split(',')
|
||||||
|
.map((s) => s.trim())
|
||||||
|
.filter(Boolean),
|
||||||
|
logTailMaxLines: parseInt(process.env.LOG_TAIL_MAX_LINES ?? '500', 10),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function requireEnv(name: string): string {
|
||||||
|
const value = process.env[name];
|
||||||
|
if (!value) {
|
||||||
|
throw new Error(`Missing required environment variable: ${name}`);
|
||||||
|
}
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Health check ────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function getHealthData(version: string): HealthData {
|
||||||
|
return {
|
||||||
|
status: 'ok',
|
||||||
|
version,
|
||||||
|
bridge: {
|
||||||
|
configured: true,
|
||||||
|
reachable: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Server skeleton ─────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Placeholder for the actual HTTP server (Express / Fastify / raw http).
|
||||||
|
* M2 will implement real endpoints.
|
||||||
|
*
|
||||||
|
* Route map (ADR §10):
|
||||||
|
* GET /bridge/v1/health
|
||||||
|
* GET /bridge/v1/boards
|
||||||
|
* GET /bridge/v1/boards/:board/tasks
|
||||||
|
* GET /bridge/v1/boards/:board/tasks/:taskId
|
||||||
|
* GET /bridge/v1/boards/:board/tasks/:taskId/log
|
||||||
|
* POST /bridge/v1/boards/:board/tasks
|
||||||
|
* POST /bridge/v1/boards/:board/tasks/:taskId/comment
|
||||||
|
* POST /bridge/v1/boards/:board/tasks/:taskId/assign
|
||||||
|
* POST /bridge/v1/boards/:board/tasks/:taskId/promote
|
||||||
|
* POST /bridge/v1/boards/:board/tasks/:taskId/block
|
||||||
|
* POST /bridge/v1/boards/:board/tasks/:taskId/unblock
|
||||||
|
* POST /bridge/v1/boards/:board/tasks/:taskId/archive
|
||||||
|
* POST /bridge/v1/boards/:board/dispatch
|
||||||
|
*/
|
||||||
|
export async function startBridge(config: BridgeConfig): Promise<void> {
|
||||||
|
// M2: implement HTTP server with auth middleware
|
||||||
|
console.log(
|
||||||
|
`[bridge] Skeleton loaded. Would listen on ${config.host}:${config.port}`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Main ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const VERSION = '0.1.0';
|
||||||
|
|
||||||
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||||
|
const config = loadConfig();
|
||||||
|
console.log(`[bridge] v${VERSION} starting...`);
|
||||||
|
await startBridge(config);
|
||||||
|
}
|
||||||
10
packages/bridge/tsconfig.build.json
Normal file
10
packages/bridge/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||||
|
}
|
||||||
24
packages/bridge/tsconfig.json
Normal file
24
packages/bridge/tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
28
packages/contracts/package.json
Normal file
28
packages/contracts/package.json
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
{
|
||||||
|
"name": "@kanban/contracts",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": {
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"import": "./dist/index.js"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"main": "./dist/index.js",
|
||||||
|
"types": "./dist/index.d.ts",
|
||||||
|
"files": [
|
||||||
|
"dist"
|
||||||
|
],
|
||||||
|
"scripts": {
|
||||||
|
"build": "tsc -p tsconfig.build.json",
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"clean": "rm -rf dist"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"zod": "^3.23.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"typescript": "^5.5.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
97
packages/contracts/src/index.ts
Normal file
97
packages/contracts/src/index.ts
Normal file
|
|
@ -0,0 +1,97 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// @kanban/contracts — barrel export
|
||||||
|
// Shared types and Zod schemas for Kanban Mini App
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// TypeScript types
|
||||||
|
export type {
|
||||||
|
Board,
|
||||||
|
BoardSlug,
|
||||||
|
TaskStatus,
|
||||||
|
TaskSummary,
|
||||||
|
TaskDetail,
|
||||||
|
TaskComment,
|
||||||
|
TaskRun,
|
||||||
|
TaskEvent,
|
||||||
|
TaskAttachment,
|
||||||
|
AppRole,
|
||||||
|
AppUser,
|
||||||
|
ColumnDef,
|
||||||
|
} from './types/kanban.js';
|
||||||
|
export { DEFAULT_COLUMNS } from './types/kanban.js';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
ApiOk,
|
||||||
|
ApiError,
|
||||||
|
ApiErrorBody,
|
||||||
|
ApiResponse,
|
||||||
|
PaginatedData,
|
||||||
|
PaginatedResponse,
|
||||||
|
HealthData,
|
||||||
|
MeData,
|
||||||
|
BoardListData,
|
||||||
|
TaskListParams,
|
||||||
|
TaskListData,
|
||||||
|
TaskDetailParams,
|
||||||
|
TaskDetailData,
|
||||||
|
TaskLogParams,
|
||||||
|
TaskLogData,
|
||||||
|
CreateTaskPayload,
|
||||||
|
CommentPayload,
|
||||||
|
AssignPayload,
|
||||||
|
PromotePayload,
|
||||||
|
BlockPayload,
|
||||||
|
UnblockPayload,
|
||||||
|
ArchivePayload,
|
||||||
|
DispatchPayload,
|
||||||
|
ErrorCode,
|
||||||
|
} from './types/api.js';
|
||||||
|
|
||||||
|
export type {
|
||||||
|
TelegramUser,
|
||||||
|
TelegramChat,
|
||||||
|
TelegramInitData,
|
||||||
|
TelegramWebAppThemeParams,
|
||||||
|
TelegramWebAppInfo,
|
||||||
|
} from './types/telegram.js';
|
||||||
|
|
||||||
|
// Zod schemas
|
||||||
|
export {
|
||||||
|
BoardSlugSchema,
|
||||||
|
BoardSchema,
|
||||||
|
TaskStatusSchema,
|
||||||
|
TaskIdSchema,
|
||||||
|
TaskSummarySchema,
|
||||||
|
TaskCommentSchema,
|
||||||
|
TaskRunSchema,
|
||||||
|
TaskEventSchema,
|
||||||
|
TaskAttachmentSchema,
|
||||||
|
TaskDetailSchema,
|
||||||
|
} from './schemas/kanban.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
apiOkSchema,
|
||||||
|
ApiErrorBodySchema,
|
||||||
|
ApiErrorSchema,
|
||||||
|
apiResponseSchema,
|
||||||
|
paginatedDataSchema,
|
||||||
|
CreateTaskPayloadSchema,
|
||||||
|
CommentPayloadSchema,
|
||||||
|
AssignPayloadSchema,
|
||||||
|
PromotePayloadSchema,
|
||||||
|
BlockPayloadSchema,
|
||||||
|
UnblockPayloadSchema,
|
||||||
|
ArchivePayloadSchema,
|
||||||
|
DispatchPayloadSchema,
|
||||||
|
TaskListQuerySchema,
|
||||||
|
TaskLogQuerySchema,
|
||||||
|
BoardParamSchema,
|
||||||
|
TaskParamSchema,
|
||||||
|
} from './schemas/api.js';
|
||||||
|
|
||||||
|
export {
|
||||||
|
TelegramUserSchema,
|
||||||
|
TelegramChatSchema,
|
||||||
|
TelegramInitDataSchema,
|
||||||
|
TelegramWebAppThemeParamsSchema,
|
||||||
|
} from './schemas/telegram.js';
|
||||||
102
packages/contracts/src/schemas/api.ts
Normal file
102
packages/contracts/src/schemas/api.ts
Normal file
|
|
@ -0,0 +1,102 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Zod schemas for API request/response validation
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// ─── Envelope ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function apiOkSchema<T extends z.ZodTypeAny>(dataSchema: T) {
|
||||||
|
return z.object({
|
||||||
|
ok: z.literal(true),
|
||||||
|
data: dataSchema,
|
||||||
|
requestId: z.string(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ApiErrorBodySchema = z.object({
|
||||||
|
code: z.string(),
|
||||||
|
message: z.string(),
|
||||||
|
details: z.unknown().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const ApiErrorSchema = z.object({
|
||||||
|
ok: z.literal(false),
|
||||||
|
error: ApiErrorBodySchema,
|
||||||
|
requestId: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export function apiResponseSchema<T extends z.ZodTypeAny>(dataSchema: T) {
|
||||||
|
return z.union([apiOkSchema(dataSchema), ApiErrorSchema]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Pagination ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export function paginatedDataSchema<T extends z.ZodTypeAny>(itemSchema: T) {
|
||||||
|
return z.object({
|
||||||
|
items: z.array(itemSchema),
|
||||||
|
nextCursor: z.string().nullable().optional(),
|
||||||
|
totalEstimate: z.number().int().optional(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Write payloads ──────────────────────────────────────────
|
||||||
|
|
||||||
|
export const CreateTaskPayloadSchema = z.object({
|
||||||
|
title: z.string().min(1).max(500),
|
||||||
|
body: z.string().max(50_000).optional(),
|
||||||
|
assignee: z.string().max(128).optional(),
|
||||||
|
priority: z.number().int().min(0).max(10).optional(),
|
||||||
|
workspaceKind: z.string().max(64).optional(),
|
||||||
|
workspacePath: z.string().max(1024).optional(),
|
||||||
|
goalMode: z.boolean().optional(),
|
||||||
|
maxRuntimeSeconds: z.number().int().positive().optional(),
|
||||||
|
idempotencyKey: z.string().max(256).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const CommentPayloadSchema = z.object({
|
||||||
|
body: z.string().min(1).max(50_000),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const AssignPayloadSchema = z.object({
|
||||||
|
assignee: z.string().min(1).max(128),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const PromotePayloadSchema = z.object({
|
||||||
|
reason: z.string().max(1000).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const BlockPayloadSchema = z.object({
|
||||||
|
reason: z.string().min(1).max(1000),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const UnblockPayloadSchema = z.object({});
|
||||||
|
|
||||||
|
export const ArchivePayloadSchema = z.object({});
|
||||||
|
|
||||||
|
export const DispatchPayloadSchema = z.object({});
|
||||||
|
|
||||||
|
// ─── Query params ────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const TaskListQuerySchema = z.object({
|
||||||
|
status: z.string().optional(),
|
||||||
|
assignee: z.string().optional(),
|
||||||
|
q: z.string().max(200).optional(),
|
||||||
|
limit: z.coerce.number().int().min(1).max(200).optional(),
|
||||||
|
cursor: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TaskLogQuerySchema = z.object({
|
||||||
|
lines: z.coerce.number().int().min(1).max(1000).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Board slug & task ID (for URL params) ───────────────────
|
||||||
|
|
||||||
|
export const BoardParamSchema = z.object({
|
||||||
|
board: z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TaskParamSchema = z.object({
|
||||||
|
board: z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/),
|
||||||
|
taskId: z.string().min(1).max(128).regex(/^[a-zA-Z0-9_-]+$/),
|
||||||
|
});
|
||||||
3
packages/contracts/src/schemas/index.ts
Normal file
3
packages/contracts/src/schemas/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
export * from './kanban.js';
|
||||||
|
export * from './api.js';
|
||||||
|
export * from './telegram.js';
|
||||||
89
packages/contracts/src/schemas/kanban.ts
Normal file
89
packages/contracts/src/schemas/kanban.ts
Normal file
|
|
@ -0,0 +1,89 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Zod schemas for Kanban domain models
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
// ─── Board ───────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const BoardSlugSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(128)
|
||||||
|
.regex(/^[a-zA-Z0-9_-]+$/, 'Board slug must be alphanumeric with hyphens/underscores');
|
||||||
|
|
||||||
|
export const BoardSchema = z.object({
|
||||||
|
slug: BoardSlugSchema,
|
||||||
|
name: z.string().optional(),
|
||||||
|
description: z.string().optional(),
|
||||||
|
icon: z.string().optional(),
|
||||||
|
current: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
// ─── Task ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export const TaskStatusSchema = z.enum([
|
||||||
|
'triage',
|
||||||
|
'todo',
|
||||||
|
'ready',
|
||||||
|
'running',
|
||||||
|
'blocked',
|
||||||
|
'done',
|
||||||
|
'archived',
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const TaskIdSchema = z
|
||||||
|
.string()
|
||||||
|
.min(1)
|
||||||
|
.max(128)
|
||||||
|
.regex(/^[a-zA-Z0-9_-]+$/, 'Task ID must be alphanumeric with hyphens/underscores');
|
||||||
|
|
||||||
|
export const TaskSummarySchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
title: z.string(),
|
||||||
|
status: TaskStatusSchema,
|
||||||
|
assignee: z.string().nullable().optional(),
|
||||||
|
priority: z.number().int().nullable().optional(),
|
||||||
|
createdAt: z.string().optional(),
|
||||||
|
updatedAt: z.string().optional(),
|
||||||
|
blockedReason: z.string().nullable().optional(),
|
||||||
|
parentIds: z.array(z.string()).optional(),
|
||||||
|
childIds: z.array(z.string()).optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TaskCommentSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
author: z.string().optional(),
|
||||||
|
body: z.string().min(1),
|
||||||
|
createdAt: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TaskRunSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
status: z.string().optional(),
|
||||||
|
startedAt: z.string().optional(),
|
||||||
|
finishedAt: z.string().optional(),
|
||||||
|
logAvailable: z.boolean().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TaskEventSchema = z.object({
|
||||||
|
id: z.string().optional(),
|
||||||
|
type: z.string(),
|
||||||
|
message: z.string().optional(),
|
||||||
|
createdAt: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TaskAttachmentSchema = z.object({
|
||||||
|
id: z.string(),
|
||||||
|
filename: z.string(),
|
||||||
|
sizeBytes: z.number().int().optional(),
|
||||||
|
mimeType: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TaskDetailSchema = TaskSummarySchema.extend({
|
||||||
|
body: z.string().nullable().optional(),
|
||||||
|
comments: z.array(TaskCommentSchema).optional(),
|
||||||
|
runs: z.array(TaskRunSchema).optional(),
|
||||||
|
events: z.array(TaskEventSchema).optional(),
|
||||||
|
attachments: z.array(TaskAttachmentSchema).optional(),
|
||||||
|
});
|
||||||
46
packages/contracts/src/schemas/telegram.ts
Normal file
46
packages/contracts/src/schemas/telegram.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Zod schemas for Telegram types
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
export const TelegramUserSchema = z.object({
|
||||||
|
id: z.number().int().positive(),
|
||||||
|
is_bot: z.boolean().optional(),
|
||||||
|
first_name: z.string().min(1),
|
||||||
|
last_name: z.string().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
language_code: z.string().optional(),
|
||||||
|
is_premium: z.boolean().optional(),
|
||||||
|
photo_url: z.string().url().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TelegramChatSchema = z.object({
|
||||||
|
id: z.number().int(),
|
||||||
|
type: z.string(),
|
||||||
|
title: z.string().optional(),
|
||||||
|
username: z.string().optional(),
|
||||||
|
photo_url: z.string().url().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TelegramInitDataSchema = z.object({
|
||||||
|
hash: z.string().min(1),
|
||||||
|
auth_date: z.number().int().positive(),
|
||||||
|
query_id: z.string().optional(),
|
||||||
|
user: TelegramUserSchema.optional(),
|
||||||
|
chat: TelegramChatSchema.optional(),
|
||||||
|
chat_type: z.string().optional(),
|
||||||
|
chat_instance: z.string().optional(),
|
||||||
|
start_param: z.string().optional(),
|
||||||
|
can_send_after: z.number().int().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const TelegramWebAppThemeParamsSchema = z.object({
|
||||||
|
bg_color: z.string().optional(),
|
||||||
|
text_color: z.string().optional(),
|
||||||
|
hint_color: z.string().optional(),
|
||||||
|
link_color: z.string().optional(),
|
||||||
|
button_color: z.string().optional(),
|
||||||
|
button_text_color: z.string().optional(),
|
||||||
|
secondary_bg_color: z.string().optional(),
|
||||||
|
});
|
||||||
150
packages/contracts/src/types/api.ts
Normal file
150
packages/contracts/src/types/api.ts
Normal file
|
|
@ -0,0 +1,150 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// API request/response envelope types
|
||||||
|
// From ADR §10 "Common response envelope" and endpoint specs
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
import type { Board, TaskSummary, TaskDetail, AppUser } from './kanban.js';
|
||||||
|
|
||||||
|
// ─── Envelope ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface ApiOk<T> {
|
||||||
|
ok: true;
|
||||||
|
data: T;
|
||||||
|
requestId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiErrorBody {
|
||||||
|
code: string;
|
||||||
|
message: string;
|
||||||
|
details?: unknown;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ApiError {
|
||||||
|
ok: false;
|
||||||
|
error: ApiErrorBody;
|
||||||
|
requestId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ApiResponse<T> = ApiOk<T> | ApiError;
|
||||||
|
|
||||||
|
// ─── Pagination ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface PaginatedData<T> {
|
||||||
|
items: T[];
|
||||||
|
nextCursor?: string | null;
|
||||||
|
totalEstimate?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type PaginatedResponse<T> = ApiOk<PaginatedData<T>> | ApiError;
|
||||||
|
|
||||||
|
// ─── Health ──────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface HealthData {
|
||||||
|
status: 'ok' | 'degraded';
|
||||||
|
version: string;
|
||||||
|
bridge: {
|
||||||
|
configured: boolean;
|
||||||
|
reachable: boolean;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Me ──────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface MeData {
|
||||||
|
user: AppUser;
|
||||||
|
expiresAt: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Board list ──────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type BoardListData = Board[];
|
||||||
|
|
||||||
|
// ─── Task list ───────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TaskListParams {
|
||||||
|
board: string;
|
||||||
|
status?: string;
|
||||||
|
assignee?: string;
|
||||||
|
q?: string;
|
||||||
|
limit?: number;
|
||||||
|
cursor?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TaskListData = PaginatedData<TaskSummary>;
|
||||||
|
|
||||||
|
// ─── Task detail ─────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TaskDetailParams {
|
||||||
|
board: string;
|
||||||
|
taskId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TaskDetailData = TaskDetail;
|
||||||
|
|
||||||
|
// ─── Task log ────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TaskLogParams {
|
||||||
|
board: string;
|
||||||
|
taskId: string;
|
||||||
|
lines?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskLogData {
|
||||||
|
content: string;
|
||||||
|
truncated: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Write payloads ──────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface CreateTaskPayload {
|
||||||
|
title: string;
|
||||||
|
body?: string;
|
||||||
|
assignee?: string;
|
||||||
|
priority?: number;
|
||||||
|
workspaceKind?: string;
|
||||||
|
workspacePath?: string;
|
||||||
|
goalMode?: boolean;
|
||||||
|
maxRuntimeSeconds?: number;
|
||||||
|
idempotencyKey?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CommentPayload {
|
||||||
|
body: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AssignPayload {
|
||||||
|
assignee: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PromotePayload {
|
||||||
|
reason?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface BlockPayload {
|
||||||
|
reason: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface UnblockPayload {
|
||||||
|
// empty for now; may add reason later
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ArchivePayload {
|
||||||
|
// empty for now
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DispatchPayload {
|
||||||
|
// empty for now
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Error codes (ADR §10) ──────────────────────────────────
|
||||||
|
|
||||||
|
export type ErrorCode =
|
||||||
|
| 'UNAUTHORIZED'
|
||||||
|
| 'FORBIDDEN'
|
||||||
|
| 'NOT_FOUND'
|
||||||
|
| 'VALIDATION_ERROR'
|
||||||
|
| 'RATE_LIMITED'
|
||||||
|
| 'BRIDGE_UNREACHABLE'
|
||||||
|
| 'BRIDGE_ERROR'
|
||||||
|
| 'TIMEOUT'
|
||||||
|
| 'INTERNAL_ERROR';
|
||||||
108
packages/contracts/src/types/kanban.ts
Normal file
108
packages/contracts/src/types/kanban.ts
Normal file
|
|
@ -0,0 +1,108 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Core Kanban domain types
|
||||||
|
// Based on ADR §10 "Core models" and §10 "API contract for v1"
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/** Hermes Kanban board identifier */
|
||||||
|
export type BoardSlug = string;
|
||||||
|
|
||||||
|
export interface Board {
|
||||||
|
slug: BoardSlug;
|
||||||
|
name?: string;
|
||||||
|
description?: string;
|
||||||
|
icon?: string;
|
||||||
|
current?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Task ────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
export type TaskStatus =
|
||||||
|
| 'triage'
|
||||||
|
| 'todo'
|
||||||
|
| 'ready'
|
||||||
|
| 'running'
|
||||||
|
| 'blocked'
|
||||||
|
| 'done'
|
||||||
|
| 'archived';
|
||||||
|
|
||||||
|
export interface TaskSummary {
|
||||||
|
id: string;
|
||||||
|
title: string;
|
||||||
|
status: TaskStatus;
|
||||||
|
assignee?: string | null;
|
||||||
|
priority?: number | null;
|
||||||
|
createdAt?: string;
|
||||||
|
updatedAt?: string;
|
||||||
|
blockedReason?: string | null;
|
||||||
|
parentIds?: string[];
|
||||||
|
childIds?: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskDetail extends TaskSummary {
|
||||||
|
body?: string | null;
|
||||||
|
comments?: TaskComment[];
|
||||||
|
runs?: TaskRun[];
|
||||||
|
events?: TaskEvent[];
|
||||||
|
attachments?: TaskAttachment[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Sub-resources ───────────────────────────────────────────
|
||||||
|
|
||||||
|
export interface TaskComment {
|
||||||
|
id?: string;
|
||||||
|
author?: string;
|
||||||
|
body: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskRun {
|
||||||
|
id?: string;
|
||||||
|
status?: string;
|
||||||
|
startedAt?: string;
|
||||||
|
finishedAt?: string;
|
||||||
|
logAvailable?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskEvent {
|
||||||
|
id?: string;
|
||||||
|
type: string;
|
||||||
|
message?: string;
|
||||||
|
createdAt?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TaskAttachment {
|
||||||
|
id: string;
|
||||||
|
filename: string;
|
||||||
|
sizeBytes?: number;
|
||||||
|
mimeType?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── App roles (ADR §7) ─────────────────────────────────────
|
||||||
|
|
||||||
|
export type AppRole = 'viewer' | 'operator' | 'admin';
|
||||||
|
|
||||||
|
export interface AppUser {
|
||||||
|
telegramId: number;
|
||||||
|
firstName?: string;
|
||||||
|
lastName?: string;
|
||||||
|
username?: string;
|
||||||
|
role: AppRole;
|
||||||
|
allowedBoards?: BoardSlug[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Column grouping (used by frontend kanban view) ─────────
|
||||||
|
|
||||||
|
export interface ColumnDef {
|
||||||
|
status: TaskStatus;
|
||||||
|
title: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const DEFAULT_COLUMNS: ColumnDef[] = [
|
||||||
|
{ status: 'triage', title: 'Triage' },
|
||||||
|
{ status: 'todo', title: 'To Do' },
|
||||||
|
{ status: 'ready', title: 'Ready' },
|
||||||
|
{ status: 'running', title: 'Running' },
|
||||||
|
{ status: 'blocked', title: 'Blocked' },
|
||||||
|
{ status: 'done', title: 'Done' },
|
||||||
|
{ status: 'archived', title: 'Archived' },
|
||||||
|
];
|
||||||
88
packages/contracts/src/types/telegram.ts
Normal file
88
packages/contracts/src/types/telegram.ts
Normal file
|
|
@ -0,0 +1,88 @@
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
// Telegram Mini App types
|
||||||
|
// From ADR §2 "Telegram Mini Apps" and §7 auth model
|
||||||
|
// ─────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsed Telegram WebAppInitData.
|
||||||
|
* Fields are present after parsing the raw initData query string.
|
||||||
|
* @see https://core.telegram.org/bots/webapps#webappinitdata
|
||||||
|
*/
|
||||||
|
export interface TelegramUser {
|
||||||
|
id: number;
|
||||||
|
is_bot?: boolean;
|
||||||
|
first_name: string;
|
||||||
|
last_name?: string;
|
||||||
|
username?: string;
|
||||||
|
language_code?: string;
|
||||||
|
is_premium?: boolean;
|
||||||
|
photo_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TelegramChat {
|
||||||
|
id: number;
|
||||||
|
type: string;
|
||||||
|
title?: string;
|
||||||
|
username?: string;
|
||||||
|
photo_url?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parsed representation of Telegram initData.
|
||||||
|
* The raw string is query-string-like; the frontend sends it as-is.
|
||||||
|
* The BFF parses and validates the hash server-side.
|
||||||
|
*/
|
||||||
|
export interface TelegramInitData {
|
||||||
|
/** HMAC hash from Telegram — used for validation, never trusted alone */
|
||||||
|
hash: string;
|
||||||
|
/** Unix timestamp when the data was signed */
|
||||||
|
auth_date: number;
|
||||||
|
/** Bot-provided unique session identifier */
|
||||||
|
query_id?: string;
|
||||||
|
/** The user who opened the Mini App */
|
||||||
|
user?: TelegramUser;
|
||||||
|
/** The chat from which the Mini App was opened */
|
||||||
|
chat?: TelegramChat;
|
||||||
|
/** Chat type if opened from a chat attachment */
|
||||||
|
chat_type?: string;
|
||||||
|
/** Chat instance if opened from a chat attachment */
|
||||||
|
chat_instance?: string;
|
||||||
|
/** Start parameter from t.me link */
|
||||||
|
start_param?: string;
|
||||||
|
/** Arbitrary data from bot link */
|
||||||
|
can_send_after?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Telegram WebApp color scheme object as passed to the Mini App.
|
||||||
|
* Used for theme-aware UI rendering.
|
||||||
|
*/
|
||||||
|
export interface TelegramWebAppThemeParams {
|
||||||
|
bg_color?: string;
|
||||||
|
text_color?: string;
|
||||||
|
hint_color?: string;
|
||||||
|
link_color?: string;
|
||||||
|
button_color?: string;
|
||||||
|
button_text_color?: string;
|
||||||
|
secondary_bg_color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Telegram WebApp platform info.
|
||||||
|
*/
|
||||||
|
export interface TelegramWebAppInfo {
|
||||||
|
/** Color scheme: 'light' or 'dark' */
|
||||||
|
colorScheme: 'light' | 'dark';
|
||||||
|
/** Current theme parameters */
|
||||||
|
themeParams: TelegramWebAppThemeParams;
|
||||||
|
/** Whether the Mini App is expanded */
|
||||||
|
isExpanded: boolean;
|
||||||
|
/** Current viewport height */
|
||||||
|
viewportHeight: number;
|
||||||
|
/** Current viewport stable height (excluding keyboard) */
|
||||||
|
viewportStableHeight: number;
|
||||||
|
/** Platform identifier (e.g., 'android', 'ios', 'tdesktop', 'web') */
|
||||||
|
platform: string;
|
||||||
|
/** App version */
|
||||||
|
version: string;
|
||||||
|
}
|
||||||
10
packages/contracts/tsconfig.build.json
Normal file
10
packages/contracts/tsconfig.build.json
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"compilerOptions": {
|
||||||
|
"noEmit": false,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true
|
||||||
|
},
|
||||||
|
"exclude": ["node_modules", "dist", "**/*.test.ts", "**/*.spec.ts"]
|
||||||
|
}
|
||||||
24
packages/contracts/tsconfig.json
Normal file
24
packages/contracts/tsconfig.json
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
{
|
||||||
|
"compilerOptions": {
|
||||||
|
"target": "ES2022",
|
||||||
|
"module": "ESNext",
|
||||||
|
"moduleResolution": "bundler",
|
||||||
|
"lib": ["ES2022"],
|
||||||
|
"strict": true,
|
||||||
|
"esModuleInterop": true,
|
||||||
|
"skipLibCheck": true,
|
||||||
|
"forceConsistentCasingInFileNames": true,
|
||||||
|
"resolveJsonModule": true,
|
||||||
|
"isolatedModules": true,
|
||||||
|
"declaration": true,
|
||||||
|
"declarationMap": true,
|
||||||
|
"sourceMap": true,
|
||||||
|
"outDir": "./dist",
|
||||||
|
"rootDir": "./src",
|
||||||
|
"noUnusedLocals": true,
|
||||||
|
"noUnusedParameters": true,
|
||||||
|
"noFallthroughCasesInSwitch": true
|
||||||
|
},
|
||||||
|
"include": ["src/**/*.ts"],
|
||||||
|
"exclude": ["node_modules", "dist"]
|
||||||
|
}
|
||||||
353
pnpm-lock.yaml
generated
Normal file
353
pnpm-lock.yaml
generated
Normal file
|
|
@ -0,0 +1,353 @@
|
||||||
|
lockfileVersion: '9.0'
|
||||||
|
|
||||||
|
settings:
|
||||||
|
autoInstallPeers: true
|
||||||
|
excludeLinksFromLockfile: false
|
||||||
|
|
||||||
|
importers:
|
||||||
|
|
||||||
|
.: {}
|
||||||
|
|
||||||
|
packages/bridge:
|
||||||
|
dependencies:
|
||||||
|
'@kanban/contracts':
|
||||||
|
specifier: workspace:*
|
||||||
|
version: link:../contracts
|
||||||
|
zod:
|
||||||
|
specifier: ^3.23.0
|
||||||
|
version: 3.25.76
|
||||||
|
devDependencies:
|
||||||
|
'@types/node':
|
||||||
|
specifier: ^20.14.0
|
||||||
|
version: 20.19.43
|
||||||
|
tsx:
|
||||||
|
specifier: ^4.16.0
|
||||||
|
version: 4.23.0
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.5.0
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
|
packages/contracts:
|
||||||
|
dependencies:
|
||||||
|
zod:
|
||||||
|
specifier: ^3.23.0
|
||||||
|
version: 3.25.76
|
||||||
|
devDependencies:
|
||||||
|
typescript:
|
||||||
|
specifier: ^5.5.0
|
||||||
|
version: 5.9.3
|
||||||
|
|
||||||
|
packages:
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [aix]
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.28.1':
|
||||||
|
resolution: {integrity: sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [android]
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [freebsd]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.28.1':
|
||||||
|
resolution: {integrity: sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.28.1':
|
||||||
|
resolution: {integrity: sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [loong64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.28.1':
|
||||||
|
resolution: {integrity: sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [mips64el]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ppc64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [riscv64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.28.1':
|
||||||
|
resolution: {integrity: sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [s390x]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [linux]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [netbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [openbsd]
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [openharmony]
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [sunos]
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [arm64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.28.1':
|
||||||
|
resolution: {integrity: sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [ia32]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.28.1':
|
||||||
|
resolution: {integrity: sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
cpu: [x64]
|
||||||
|
os: [win32]
|
||||||
|
|
||||||
|
'@types/node@20.19.43':
|
||||||
|
resolution: {integrity: sha512-6oYBAi5ikg4Pl+kGsoYtawUMBT2zZMCvPNF7pVLnHZfd1zf38DRiWn/gT01RYCdUqkv7Fhr+C9ot4/tb+2sVvA==}
|
||||||
|
|
||||||
|
esbuild@0.28.1:
|
||||||
|
resolution: {integrity: sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==}
|
||||||
|
engines: {node: '>=18'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
|
||||||
|
engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0}
|
||||||
|
os: [darwin]
|
||||||
|
|
||||||
|
tsx@4.23.0:
|
||||||
|
resolution: {integrity: sha512-eUdUIaCr963q2h5u3+QwvYp0+eqPvn+egeqZUm0hwERCqqx1E3kK5ehbGCvqSE5MQAULr67ww0cA3jKc3YkM1w==}
|
||||||
|
engines: {node: '>=18.0.0'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
typescript@5.9.3:
|
||||||
|
resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==}
|
||||||
|
engines: {node: '>=14.17'}
|
||||||
|
hasBin: true
|
||||||
|
|
||||||
|
undici-types@6.21.0:
|
||||||
|
resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==}
|
||||||
|
|
||||||
|
zod@3.25.76:
|
||||||
|
resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==}
|
||||||
|
|
||||||
|
snapshots:
|
||||||
|
|
||||||
|
'@esbuild/aix-ppc64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-arm@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/android-x64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-arm64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/darwin-x64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-arm64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/freebsd-x64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-arm@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ia32@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-loong64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-mips64el@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-ppc64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-riscv64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-s390x@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/linux-x64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-arm64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/netbsd-x64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-arm64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openbsd-x64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/openharmony-arm64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/sunos-x64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-arm64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-ia32@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@esbuild/win32-x64@0.28.1':
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
'@types/node@20.19.43':
|
||||||
|
dependencies:
|
||||||
|
undici-types: 6.21.0
|
||||||
|
|
||||||
|
esbuild@0.28.1:
|
||||||
|
optionalDependencies:
|
||||||
|
'@esbuild/aix-ppc64': 0.28.1
|
||||||
|
'@esbuild/android-arm': 0.28.1
|
||||||
|
'@esbuild/android-arm64': 0.28.1
|
||||||
|
'@esbuild/android-x64': 0.28.1
|
||||||
|
'@esbuild/darwin-arm64': 0.28.1
|
||||||
|
'@esbuild/darwin-x64': 0.28.1
|
||||||
|
'@esbuild/freebsd-arm64': 0.28.1
|
||||||
|
'@esbuild/freebsd-x64': 0.28.1
|
||||||
|
'@esbuild/linux-arm': 0.28.1
|
||||||
|
'@esbuild/linux-arm64': 0.28.1
|
||||||
|
'@esbuild/linux-ia32': 0.28.1
|
||||||
|
'@esbuild/linux-loong64': 0.28.1
|
||||||
|
'@esbuild/linux-mips64el': 0.28.1
|
||||||
|
'@esbuild/linux-ppc64': 0.28.1
|
||||||
|
'@esbuild/linux-riscv64': 0.28.1
|
||||||
|
'@esbuild/linux-s390x': 0.28.1
|
||||||
|
'@esbuild/linux-x64': 0.28.1
|
||||||
|
'@esbuild/netbsd-arm64': 0.28.1
|
||||||
|
'@esbuild/netbsd-x64': 0.28.1
|
||||||
|
'@esbuild/openbsd-arm64': 0.28.1
|
||||||
|
'@esbuild/openbsd-x64': 0.28.1
|
||||||
|
'@esbuild/openharmony-arm64': 0.28.1
|
||||||
|
'@esbuild/sunos-x64': 0.28.1
|
||||||
|
'@esbuild/win32-arm64': 0.28.1
|
||||||
|
'@esbuild/win32-ia32': 0.28.1
|
||||||
|
'@esbuild/win32-x64': 0.28.1
|
||||||
|
|
||||||
|
fsevents@2.3.3:
|
||||||
|
optional: true
|
||||||
|
|
||||||
|
tsx@4.23.0:
|
||||||
|
dependencies:
|
||||||
|
esbuild: 0.28.1
|
||||||
|
optionalDependencies:
|
||||||
|
fsevents: 2.3.3
|
||||||
|
|
||||||
|
typescript@5.9.3: {}
|
||||||
|
|
||||||
|
undici-types@6.21.0: {}
|
||||||
|
|
||||||
|
zod@3.25.76: {}
|
||||||
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
packages:
|
||||||
|
- 'packages/*'
|
||||||
|
# Future: - 'apps/*' for BFF, frontend
|
||||||
Loading…
Add table
Reference in a new issue