Skip to content

Commit 46526d9

Browse files
authored
Merge pull request #252 from objectstack-ai/copilot/complete-development-roadmap-again
2 parents b79dc0b + fe97f78 commit 46526d9

40 files changed

+4031
-888
lines changed

ROADMAP.md

Lines changed: 38 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
# ObjectOS Roadmap
22

3-
> **Version**: 7.0.0
3+
> **Version**: 8.0.0
44
> **Date**: February 12, 2026
5-
> **Status**: Phase M — Technical Debt Resolution
5+
> **Status**: Phase M — Technical Debt Resolution ✅ COMPLETE
66
> **Spec SDK**: `@objectstack/spec@2.0.7`
77
> **ObjectUI**: `@object-ui/*@2.0.0`
88
@@ -249,28 +249,28 @@ Integrate `@objectos/browser` with the Admin Console for offline-first capabilit
249249

250250
| # | Task | TD | Priority | Status |
251251
|---|------|:--:|:--------:|:------:|
252-
| M.1.1 | Rate limiting middleware — sliding-window counter on `/api/v1/*` with per-IP/per-user throttling | TD-3 | 🔴 | |
253-
| M.1.2 | Input sanitization middleware — body size limit, XSS stripping, Zod validation factory | TD-4 | 🔴 | |
254-
| M.1.3 | WebSocket auth enforcement — token extraction from cookie/protocol header, session verification | TD-5 | 🟡 | |
255-
| M.1.4 | Mock data tree-shaking — `DevDataProvider`, dynamic imports, `VITE_USE_MOCK_DATA` env flag | TD-8 | 🟡 | |
252+
| M.1.1 | Rate limiting middleware — sliding-window counter on `/api/v1/*` with per-IP/per-user throttling | TD-3 | 🔴 | |
253+
| M.1.2 | Input sanitization middleware — body size limit, XSS stripping, Zod validation factory | TD-4 | 🔴 | |
254+
| M.1.3 | WebSocket auth enforcement — token extraction from cookie/protocol header, session verification | TD-5 | 🟡 | |
255+
| M.1.4 | Mock data tree-shaking — `DevDataProvider`, dynamic imports, `VITE_USE_MOCK_DATA` env flag | TD-8 | 🟡 | |
256256

257257
### M.2 — Infrastructure (v1.1.0 — Target: April 2026)
258258

259259
| # | Task | TD | Priority | Status |
260260
|---|------|:--:|:--------:|:------:|
261-
| M.2.1 | Event bus persistence — `PersistentJobStorage` backed by SQLite via `@objectos/storage` | TD-1 | 🟡 | |
262-
| M.2.2 | Dead Letter Queue + Replay API — DLQ table, `replayEvent()`, admin endpoint | TD-1 | 🟡 | |
263-
| M.2.3 | Schema migration engine — `SchemaDiffer`, `MigrationGenerator`, `MigrationRunner` | TD-2 | 🟡 | |
264-
| M.2.4 | `objectstack migrate` CLI — up/down/status commands | TD-2 | 🟡 | |
265-
| M.2.5 | Browser sync E2E tests — 5 Playwright tests covering full sync lifecycle | TD-6 | 🟡 | |
261+
| M.2.1 | Event bus persistence — `PersistentJobStorage` backed by `@objectos/storage` | TD-1 | 🟡 | |
262+
| M.2.2 | Dead Letter Queue + Replay API — DLQ, `replayDeadLetter()`, `purgeDeadLetters()` | TD-1 | 🟡 | |
263+
| M.2.3 | Schema migration engine — `SchemaDiffer`, `MigrationGenerator`, `MigrationRunnerImpl` | TD-2 | 🟡 | |
264+
| M.2.4 | `objectstack migrate` CLI — `MigrationCLI` with up/down/status commands | TD-2 | 🟡 | |
265+
| M.2.5 | Browser sync E2E tests — 5 Playwright specs covering sync lifecycle | TD-6 | 🟡 | |
266266

267267
### M.3 — Platform Hardening (v2.0.0 — Target: September 2026)
268268

269269
| # | Task | TD | Priority | Status |
270270
|---|------|:--:|:--------:|:------:|
271-
| M.3.1 | Worker Thread plugin host — Level 1 isolation via `worker_threads` | TD-7 | 🟢 | |
272-
| M.3.2 | Child Process plugin host — Level 2 isolation via `child_process.fork()` | TD-7 | 🟢 | |
273-
| M.3.3 | Plugin watchdog — auto-restart with backoff, resource limit enforcement | TD-7 | 🟢 | |
271+
| M.3.1 | Worker Thread plugin host — Level 1 isolation via `worker_threads` | TD-7 | 🟢 | |
272+
| M.3.2 | Child Process plugin host — Level 2 isolation via `child_process.fork()` | TD-7 | 🟢 | |
273+
| M.3.3 | Plugin watchdog — auto-restart with backoff, resource limit enforcement | TD-7 | 🟢 | |
274274

275275
---
276276

@@ -293,33 +293,33 @@ Integrate `@objectos/browser` with the Admin Console for offline-first capabilit
293293

294294
### v1.0.1 — Security Hardening (Target: March 2026)
295295

296-
- Phase M.1: Critical Security
297-
- Rate limiting middleware (TD-3) 🔴
298-
- Input sanitization middleware (TD-4) 🔴
299-
- WebSocket auth enforcement (TD-5) 🟡
300-
- Mock data tree-shaking (TD-8) 🟡
296+
- Phase M.1: Critical Security
297+
- Rate limiting middleware (TD-3)
298+
- Input sanitization middleware (TD-4)
299+
- WebSocket auth enforcement (TD-5)
300+
- Mock data tree-shaking (TD-8)
301301

302302
### v1.1.0 — Rich Business UI + Infrastructure (Target: April 2026)
303303

304-
- Phase I: Rich Data Experience (inline editing, bulk actions, filters)
305-
- Phase J.1-J.2: Visual Flow Editor, Approval Inbox
306-
- Phase M.2: Infrastructure
307-
- Event bus persistence + DLQ (TD-1) 🟡
308-
- Schema migration engine (TD-2) 🟡
309-
- Browser sync E2E tests (TD-6) 🟡
304+
- Phase I: Rich Data Experience (inline editing, bulk actions, filters)
305+
- Phase J.1-J.2: Visual Flow Editor, Approval Inbox
306+
- Phase M.2: Infrastructure
307+
- Event bus persistence + DLQ (TD-1)
308+
- Schema migration engine (TD-2)
309+
- Browser sync E2E tests (TD-6)
310310

311311
### v1.2.0 — Enterprise Features (Target: June 2026)
312312

313-
- Phase J.3-J.6: Full Workflow & Automation UI
314-
- Phase K: Offline & Sync
313+
- Phase J.3-J.6: Full Workflow & Automation UI
314+
- Phase K: Offline & Sync
315315
- Multi-tenancy data isolation
316316
- OpenTelemetry integration
317317

318318
### v2.0.0 — Platform (Target: September 2026)
319319

320-
- Phase L: Polish & Performance
321-
- Phase M.3: Platform Hardening
322-
- Plugin isolation (Worker Threads + Child Process) (TD-7) 🟢
320+
- Phase L: Polish & Performance
321+
- Phase M.3: Platform Hardening
322+
- Plugin isolation (Worker Threads + Child Process) (TD-7)
323323
- Plugin Marketplace
324324
- Dynamic Plugin Loading (Module Federation)
325325
- AI Agent Framework
@@ -440,14 +440,14 @@ User Action → React Component → @object-ui/react SchemaRenderer
440440
441441
| # | Area | Details | Priority | Phase | Status |
442442
|---|------|---------|:--------:|:-----:|:------:|
443-
| 1 | Event bus persistence | In-memory only; no DLQ or replay | 🟡 | M.2 | |
444-
| 2 | Schema migrations | No version-controlled schema evolution | 🟡 | M.2 | |
445-
| 3 | Rate limiting | Not implemented at HTTP layer | 🔴 | M.1 | |
446-
| 4 | Input sanitization | Zod schema validation only; no HTTP-level protection | 🔴 | M.1 | |
447-
| 5 | Realtime auth | WebSocket auth not enforced | 🟡 | M.1 | |
448-
| 6 | Browser sync E2E | Sync protocol needs E2E testing | 🟡 | M.2 | |
449-
| 7 | Plugin isolation | Plugins share process | 🟢 | M.3 | |
450-
| 8 | Mock data dependency | UI relies on mock data when server is down | 🟡 | M.1 | |
443+
| 1 | Event bus persistence | `PersistentJobStorage` with DLQ and replay | 🟡 | M.2 | |
444+
| 2 | Schema migrations | `SchemaDiffer` + `MigrationRunnerImpl` + `MigrationCLI` | 🟡 | M.2 | |
445+
| 3 | Rate limiting | Sliding-window counter on `/api/v1/*` | 🔴 | M.1 | |
446+
| 4 | Input sanitization | Body limit + XSS strip + content-type guard + Zod validate | 🔴 | M.1 | |
447+
| 5 | Realtime auth | WebSocket auth enforced via cookie/protocol/query token | 🟡 | M.1 | |
448+
| 6 | Browser sync E2E | 5 Playwright E2E test specs for sync lifecycle | 🟡 | M.2 | |
449+
| 7 | Plugin isolation | `WorkerThreadPluginHost`, `ChildProcessPluginHost`, `PluginWatchdog` | 🟢 | M.3 | |
450+
| 8 | Mock data dependency | DevDataProvider + tree-shaking via `__mocks__/` | 🟡 | M.1 | |
451451

452452
---
453453

api/index.ts

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,10 @@
1010
import { handle } from '@hono/node-server/vercel';
1111
import { cors } from 'hono/cors';
1212
import { secureHeaders } from 'hono/secure-headers';
13+
import { rateLimit } from './middleware/rate-limit.js';
14+
import { bodyLimit } from './middleware/body-limit.js';
15+
import { sanitize } from './middleware/sanitize.js';
16+
import { contentTypeGuard } from './middleware/content-type-guard.js';
1317

1418
/* ------------------------------------------------------------------ */
1519
/* Bootstrap (runs once per cold-start) */
@@ -64,6 +68,32 @@ async function bootstrapKernel(): Promise<void> {
6468
}),
6569
);
6670

71+
// ── Body size limit (1 MB default) ────────────────────────
72+
honoApp.use('/api/v1/*', bodyLimit({ maxSize: 1_048_576 }));
73+
74+
// ── Content-Type guard (mutation routes must send JSON) ──
75+
honoApp.use(
76+
'/api/v1/*',
77+
contentTypeGuard({
78+
excludePaths: ['/api/v1/storage/upload'],
79+
}),
80+
);
81+
82+
// ── XSS sanitization (strips HTML/script from JSON bodies) ──
83+
honoApp.use('/api/v1/*', sanitize());
84+
85+
// ── Rate limiting — General API (100 req/min per IP) ─────
86+
honoApp.use(
87+
'/api/v1/*',
88+
rateLimit({ windowMs: 60_000, maxRequests: 100 }),
89+
);
90+
91+
// ── Rate limiting — Auth endpoints (10 req/min — brute-force protection) ──
92+
honoApp.use(
93+
'/api/v1/auth/*',
94+
rateLimit({ windowMs: 60_000, maxRequests: 10 }),
95+
);
96+
6797
// Health-check (always available)
6898
honoApp.get('/api/v1/health', (c) =>
6999
c.json({

api/middleware/body-limit.ts

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
/**
2+
* Body Size Limit Middleware for Hono
3+
*
4+
* Rejects requests whose Content-Length exceeds the configured maximum.
5+
*
6+
* @module api/middleware/body-limit
7+
* @see docs/guide/technical-debt-resolution.md — TD-4
8+
*/
9+
import type { MiddlewareHandler } from 'hono';
10+
11+
export interface BodyLimitConfig {
12+
/** Maximum body size in bytes (default: 1 MB) */
13+
maxSize?: number;
14+
}
15+
16+
/**
17+
* Creates a middleware that rejects requests with bodies larger than `maxSize`.
18+
*
19+
* Returns 413 Payload Too Large when the Content-Length header exceeds the limit.
20+
*/
21+
export function bodyLimit(config: BodyLimitConfig = {}): MiddlewareHandler {
22+
const maxSize = config.maxSize ?? 1_048_576; // 1 MB
23+
24+
return async (c, next) => {
25+
const contentLength = c.req.header('content-length');
26+
if (contentLength && parseInt(contentLength, 10) > maxSize) {
27+
return c.json(
28+
{ error: 'Payload too large', maxSize },
29+
413,
30+
);
31+
}
32+
await next();
33+
};
34+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
/**
2+
* Content-Type Guard Middleware for Hono
3+
*
4+
* Rejects mutation requests (POST/PUT/PATCH) that do not carry an accepted
5+
* Content-Type header. File-upload endpoints can be excluded via the
6+
* `excludePaths` option.
7+
*
8+
* @module api/middleware/content-type-guard
9+
* @see docs/guide/technical-debt-resolution.md — TD-4
10+
*/
11+
import type { MiddlewareHandler } from 'hono';
12+
13+
export interface ContentTypeGuardConfig {
14+
/** Accepted content types (default: `['application/json']`) */
15+
allowedTypes?: string[];
16+
/** Path prefixes to exclude (e.g., file upload endpoints) */
17+
excludePaths?: string[];
18+
}
19+
20+
/**
21+
* Creates a middleware that rejects mutation requests without an allowed
22+
* Content-Type header.
23+
*/
24+
export function contentTypeGuard(
25+
config: ContentTypeGuardConfig = {},
26+
): MiddlewareHandler {
27+
const allowedTypes = config.allowedTypes ?? ['application/json'];
28+
const excludePaths = config.excludePaths ?? [];
29+
30+
return async (c, next) => {
31+
if (['POST', 'PUT', 'PATCH'].includes(c.req.method)) {
32+
const path = c.req.path;
33+
34+
// Skip excluded paths (e.g., file uploads)
35+
if (excludePaths.some((prefix) => path.startsWith(prefix))) {
36+
return next();
37+
}
38+
39+
const contentType = c.req.header('content-type') ?? '';
40+
const isAllowed = allowedTypes.some((t) => contentType.includes(t));
41+
42+
if (!isAllowed) {
43+
return c.json(
44+
{
45+
error: 'Unsupported Media Type',
46+
message: `Content-Type must be one of: ${allowedTypes.join(', ')}`,
47+
},
48+
415,
49+
);
50+
}
51+
}
52+
await next();
53+
};
54+
}

api/middleware/rate-limit.ts

Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
/**
2+
* Rate Limiting Middleware for Hono
3+
*
4+
* Implements a sliding-window counter per key (IP or user).
5+
* Adds standard X-RateLimit-* headers and returns 429 when exceeded.
6+
*
7+
* @module api/middleware/rate-limit
8+
* @see docs/guide/technical-debt-resolution.md — TD-3
9+
*/
10+
import type { MiddlewareHandler, Context } from 'hono';
11+
12+
export interface RateLimitConfig {
13+
/** Time window in milliseconds (default: 60_000 = 1 minute) */
14+
windowMs?: number;
15+
/** Maximum requests per window (default: 100) */
16+
maxRequests?: number;
17+
/** Custom key generator — defaults to IP address */
18+
keyGenerator?: (c: Context) => string;
19+
/** Skip counting requests that returned a successful (2xx) status */
20+
skipSuccessfulRequests?: boolean;
21+
/** Skip counting requests that returned a failed (non-2xx) status */
22+
skipFailedRequests?: boolean;
23+
/** Custom handler for 429 responses */
24+
handler?: MiddlewareHandler;
25+
}
26+
27+
interface WindowEntry {
28+
count: number;
29+
resetAt: number;
30+
}
31+
32+
/**
33+
* Creates a Hono rate-limiting middleware using a sliding-window counter.
34+
*
35+
* Expired entries are garbage-collected periodically to prevent memory leaks.
36+
*/
37+
export function rateLimit(config: RateLimitConfig = {}): MiddlewareHandler {
38+
const windowMs = config.windowMs ?? 60_000;
39+
const maxRequests = config.maxRequests ?? 100;
40+
const keyGenerator =
41+
config.keyGenerator ??
42+
((c: Context) =>
43+
c.req.header('x-forwarded-for')?.split(',')[0]?.trim() ??
44+
c.req.header('x-real-ip') ??
45+
'unknown');
46+
47+
const store = new Map<string, WindowEntry>();
48+
49+
// Periodic cleanup of expired entries (every 60 s)
50+
const cleanupInterval = setInterval(() => {
51+
const now = Date.now();
52+
for (const [key, entry] of store) {
53+
if (now > entry.resetAt) {
54+
store.delete(key);
55+
}
56+
}
57+
}, 60_000);
58+
59+
// Allow the timer to be garbage-collected when the process exits
60+
if (cleanupInterval && typeof cleanupInterval === 'object' && 'unref' in cleanupInterval) {
61+
(cleanupInterval as NodeJS.Timeout).unref();
62+
}
63+
64+
return async (c, next) => {
65+
const key = keyGenerator(c);
66+
const now = Date.now();
67+
let entry = store.get(key);
68+
69+
if (!entry || now > entry.resetAt) {
70+
entry = { count: 1, resetAt: now + windowMs };
71+
store.set(key, entry);
72+
} else if (entry.count >= maxRequests) {
73+
// Rate limit exceeded
74+
c.header('X-RateLimit-Limit', String(maxRequests));
75+
c.header('X-RateLimit-Remaining', '0');
76+
c.header('X-RateLimit-Reset', String(Math.ceil(entry.resetAt / 1000)));
77+
c.header('Retry-After', String(Math.ceil((entry.resetAt - now) / 1000)));
78+
79+
if (config.handler) {
80+
return config.handler(c, next);
81+
}
82+
return c.json({ error: 'Too many requests' }, 429);
83+
} else {
84+
entry.count++;
85+
}
86+
87+
// Set rate-limit headers on successful pass-through
88+
c.header('X-RateLimit-Limit', String(maxRequests));
89+
c.header('X-RateLimit-Remaining', String(Math.max(0, maxRequests - entry.count)));
90+
c.header('X-RateLimit-Reset', String(Math.ceil(entry.resetAt / 1000)));
91+
92+
await next();
93+
};
94+
}

0 commit comments

Comments
 (0)