zylaris 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +558 -0
- package/Zylaris.js.png +0 -0
- package/examples/default/index.html +13 -0
- package/examples/default/package.json +23 -0
- package/examples/default/src/app/about/page.tsx +18 -0
- package/examples/default/src/app/counter/page.tsx +22 -0
- package/examples/default/src/app/global.css +225 -0
- package/examples/default/src/app/layout.tsx +33 -0
- package/examples/default/src/app/page.tsx +14 -0
- package/examples/default/src/entry-client.tsx +87 -0
- package/examples/default/src/entry-server.tsx +52 -0
- package/examples/default/src/router.ts +60 -0
- package/examples/default/tsconfig.json +28 -0
- package/examples/default/zylaris.config.ts +24 -0
- package/package.json +34 -0
- package/packages/adapter/package.json +59 -0
- package/packages/adapter/src/adapters/bun.ts +215 -0
- package/packages/adapter/src/adapters/cloudflare.ts +278 -0
- package/packages/adapter/src/adapters/deno.ts +219 -0
- package/packages/adapter/src/adapters/netlify.ts +274 -0
- package/packages/adapter/src/adapters/node.ts +155 -0
- package/packages/adapter/src/adapters/static.ts +134 -0
- package/packages/adapter/src/adapters/vercel.ts +239 -0
- package/packages/adapter/src/index.ts +115 -0
- package/packages/adapter/src/lib/builder.ts +361 -0
- package/packages/adapter/src/types.ts +191 -0
- package/packages/adapter/tsconfig.json +8 -0
- package/packages/cli/package.json +43 -0
- package/packages/cli/src/bin.ts +107 -0
- package/packages/cli/src/commands/build.ts +197 -0
- package/packages/cli/src/commands/create.ts +222 -0
- package/packages/cli/src/commands/deploy.ts +90 -0
- package/packages/cli/src/commands/dev.ts +108 -0
- package/packages/cli/src/index.ts +6 -0
- package/packages/cli/tsconfig.json +9 -0
- package/packages/compiler/package.json +39 -0
- package/packages/compiler/src/index.ts +210 -0
- package/packages/compiler/src/jit.ts +187 -0
- package/packages/compiler/tsconfig.json +9 -0
- package/packages/core/package.json +55 -0
- package/packages/core/src/components.test.ts +125 -0
- package/packages/core/src/components.ts +181 -0
- package/packages/core/src/config.ts +204 -0
- package/packages/core/src/hooks.ts +142 -0
- package/packages/core/src/index.ts +59 -0
- package/packages/core/src/jsx-runtime.ts +46 -0
- package/packages/core/tsconfig.json +16 -0
- package/packages/dev-server/package.json +51 -0
- package/packages/dev-server/src/index.ts +306 -0
- package/packages/dev-server/src/jit-middleware.ts +78 -0
- package/packages/dev-server/tsconfig.json +9 -0
- package/packages/plugins/package.json +44 -0
- package/packages/plugins/src/cdn/loader.ts +275 -0
- package/packages/plugins/src/index.ts +238 -0
- package/packages/plugins/src/loaders/auto-import.ts +219 -0
- package/packages/plugins/src/loaders/external.ts +332 -0
- package/packages/plugins/src/transforms/index.ts +407 -0
- package/packages/plugins/src/types.ts +296 -0
- package/packages/plugins/tsconfig.json +8 -0
- package/packages/reactivity/package.json +36 -0
- package/packages/reactivity/src/computed.d.ts +3 -0
- package/packages/reactivity/src/computed.d.ts.map +1 -0
- package/packages/reactivity/src/computed.js +64 -0
- package/packages/reactivity/src/computed.js.map +1 -0
- package/packages/reactivity/src/computed.test.ts +83 -0
- package/packages/reactivity/src/computed.ts +69 -0
- package/packages/reactivity/src/index.d.ts +6 -0
- package/packages/reactivity/src/index.d.ts.map +1 -0
- package/packages/reactivity/src/index.js +7 -0
- package/packages/reactivity/src/index.js.map +1 -0
- package/packages/reactivity/src/index.ts +18 -0
- package/packages/reactivity/src/resource.d.ts +6 -0
- package/packages/reactivity/src/resource.d.ts.map +1 -0
- package/packages/reactivity/src/resource.js +43 -0
- package/packages/reactivity/src/resource.js.map +1 -0
- package/packages/reactivity/src/resource.test.ts +70 -0
- package/packages/reactivity/src/resource.ts +59 -0
- package/packages/reactivity/src/signal.d.ts +7 -0
- package/packages/reactivity/src/signal.d.ts.map +1 -0
- package/packages/reactivity/src/signal.js +145 -0
- package/packages/reactivity/src/signal.js.map +1 -0
- package/packages/reactivity/src/signal.test.ts +130 -0
- package/packages/reactivity/src/signal.ts +207 -0
- package/packages/reactivity/src/store.d.ts +4 -0
- package/packages/reactivity/src/store.d.ts.map +1 -0
- package/packages/reactivity/src/store.js +62 -0
- package/packages/reactivity/src/store.js.map +1 -0
- package/packages/reactivity/src/store.test.ts +38 -0
- package/packages/reactivity/src/store.ts +111 -0
- package/packages/reactivity/src/types.d.ts +43 -0
- package/packages/reactivity/src/types.d.ts.map +1 -0
- package/packages/reactivity/src/types.js +3 -0
- package/packages/reactivity/src/types.js.map +1 -0
- package/packages/reactivity/src/types.ts +43 -0
- package/packages/reactivity/tsconfig.json +9 -0
- package/packages/router/package.json +44 -0
- package/packages/router/src/components.tsx +150 -0
- package/packages/router/src/fs-router.ts +163 -0
- package/packages/router/src/index.ts +22 -0
- package/packages/router/src/router.test.ts +111 -0
- package/packages/router/src/router.ts +112 -0
- package/packages/router/src/types.ts +69 -0
- package/packages/router/tsconfig.json +10 -0
- package/packages/server/package.json +41 -0
- package/packages/server/src/action.test.ts +102 -0
- package/packages/server/src/action.ts +201 -0
- package/packages/server/src/api.ts +143 -0
- package/packages/server/src/index.ts +18 -0
- package/packages/server/src/types.ts +72 -0
- package/packages/server/tsconfig.json +9 -0
- package/pnpm-workspace.yaml +4 -0
- package/scripts/publish.ps1 +138 -0
- package/scripts/publish.sh +142 -0
- package/tsconfig.json +28 -0
- package/turbo.json +24 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@zylaris/server",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"description": "Server-side utilities for Zylaris",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"import": "./dist/index.js"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc",
|
|
20
|
+
"dev": "tsc --watch",
|
|
21
|
+
"test": "vitest run --passWithNoTests",
|
|
22
|
+
"typecheck": "tsc --noEmit",
|
|
23
|
+
"clean": "rm -rf dist"
|
|
24
|
+
},
|
|
25
|
+
"dependencies": {
|
|
26
|
+
"@zylaris/reactivity": "workspace:*",
|
|
27
|
+
"zod": "^3.22.4"
|
|
28
|
+
},
|
|
29
|
+
"devDependencies": {
|
|
30
|
+
"@types/node": "^20.0.0",
|
|
31
|
+
"typescript": "^5.3.3",
|
|
32
|
+
"vitest": "^1.2.0"
|
|
33
|
+
},
|
|
34
|
+
"keywords": [
|
|
35
|
+
"server",
|
|
36
|
+
"actions",
|
|
37
|
+
"api",
|
|
38
|
+
"zylaris"
|
|
39
|
+
],
|
|
40
|
+
"license": "MIT"
|
|
41
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { action, ActionBuilder, Action, requireAuth, requireRole } from './action.js';
|
|
3
|
+
import type { ActionContext } from './types.js';
|
|
4
|
+
|
|
5
|
+
describe('ActionBuilder', () => {
|
|
6
|
+
it('should create action builder', () => {
|
|
7
|
+
const builder = action();
|
|
8
|
+
expect(builder).toBeInstanceOf(ActionBuilder);
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
it('should build action with run', async () => {
|
|
12
|
+
const myAction = action().run(async (input) => {
|
|
13
|
+
const { name } = input as { name: string };
|
|
14
|
+
return { message: `Hello ${name}` };
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
expect(myAction).toBeInstanceOf(Action);
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
describe('Action', () => {
|
|
22
|
+
it('should execute action successfully', async () => {
|
|
23
|
+
const myAction = action().run(async (input) => {
|
|
24
|
+
const { value } = input as { value: number };
|
|
25
|
+
return { result: value * 2 };
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const result = await myAction.execute({ value: 5 }, {
|
|
29
|
+
request: new Request('http://localhost'),
|
|
30
|
+
headers: new Headers(),
|
|
31
|
+
cookies: new Map(),
|
|
32
|
+
ip: '127.0.0.1',
|
|
33
|
+
userAgent: 'test'
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
expect(result.success).toBe(true);
|
|
37
|
+
expect(result.data).toEqual({ result: 10 });
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it('should execute with null input', async () => {
|
|
41
|
+
const myAction = action().run(async () => {
|
|
42
|
+
return { success: true };
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
const result = await myAction.execute(null, {
|
|
46
|
+
request: new Request('http://localhost'),
|
|
47
|
+
headers: new Headers(),
|
|
48
|
+
cookies: new Map(),
|
|
49
|
+
ip: '127.0.0.1',
|
|
50
|
+
userAgent: 'test'
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
// Action without input validation should succeed
|
|
54
|
+
expect(result.success).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe('requireAuth', () => {
|
|
59
|
+
it('should return error when no user', async () => {
|
|
60
|
+
const middleware = requireAuth();
|
|
61
|
+
const result = await middleware({
|
|
62
|
+
request: new Request('http://localhost'),
|
|
63
|
+
headers: new Headers(),
|
|
64
|
+
cookies: new Map(),
|
|
65
|
+
ip: '127.0.0.1',
|
|
66
|
+
userAgent: 'test'
|
|
67
|
+
} as ActionContext);
|
|
68
|
+
|
|
69
|
+
expect(result).not.toBeNull();
|
|
70
|
+
expect(result && result.code).toBe('UNAUTHORIZED');
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
it('should return null when user exists', async () => {
|
|
74
|
+
const middleware = requireAuth();
|
|
75
|
+
const result = await middleware({
|
|
76
|
+
request: new Request('http://localhost'),
|
|
77
|
+
headers: new Headers(),
|
|
78
|
+
cookies: new Map(),
|
|
79
|
+
ip: '127.0.0.1',
|
|
80
|
+
userAgent: 'test',
|
|
81
|
+
user: { id: '1', email: 'test@test.com' }
|
|
82
|
+
} as ActionContext);
|
|
83
|
+
|
|
84
|
+
expect(result).toBeNull();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe('requireRole', () => {
|
|
89
|
+
it('should return error when no user', async () => {
|
|
90
|
+
const middleware = requireRole(['admin']);
|
|
91
|
+
const result = await middleware({
|
|
92
|
+
request: new Request('http://localhost'),
|
|
93
|
+
headers: new Headers(),
|
|
94
|
+
cookies: new Map(),
|
|
95
|
+
ip: '127.0.0.1',
|
|
96
|
+
userAgent: 'test'
|
|
97
|
+
} as ActionContext);
|
|
98
|
+
|
|
99
|
+
expect(result).not.toBeNull();
|
|
100
|
+
expect(result && result.code).toBe('UNAUTHORIZED');
|
|
101
|
+
});
|
|
102
|
+
});
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
import type {
|
|
3
|
+
ActionConfig,
|
|
4
|
+
ActionContext,
|
|
5
|
+
ActionResult,
|
|
6
|
+
ActionError,
|
|
7
|
+
Middleware,
|
|
8
|
+
RateLimitConfig,
|
|
9
|
+
ActionHandler,
|
|
10
|
+
} from './types.js';
|
|
11
|
+
|
|
12
|
+
// Rate limiting store (in production, use Redis)
|
|
13
|
+
const rateLimitStore = new Map<string, { count: number; resetAt: number }>();
|
|
14
|
+
|
|
15
|
+
export class ActionBuilder<I, O> {
|
|
16
|
+
private config: ActionConfig<I, O>;
|
|
17
|
+
|
|
18
|
+
constructor(config: ActionConfig<I, O> = {}) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
input<T extends ZodType>(schema: T): ActionBuilder<ReturnType<T['parse']>, O> {
|
|
23
|
+
return new ActionBuilder({ ...this.config, input: schema });
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
output<T extends ZodType>(schema: T): ActionBuilder<I, ReturnType<T['parse']>> {
|
|
27
|
+
return new ActionBuilder({ ...this.config, output: schema });
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
use(middleware: Middleware): this {
|
|
31
|
+
this.config.middleware = [...(this.config.middleware || []), middleware];
|
|
32
|
+
return this;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
rateLimit(config: RateLimitConfig): this {
|
|
36
|
+
this.config.rateLimit = config;
|
|
37
|
+
return this;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
run(handler: ActionHandler<I, O>): Action<I, O> {
|
|
41
|
+
return new Action(this.config, handler);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export class Action<I, O> {
|
|
46
|
+
id: string;
|
|
47
|
+
|
|
48
|
+
constructor(
|
|
49
|
+
private config: ActionConfig<I, O>,
|
|
50
|
+
private handler: ActionHandler<I, O>
|
|
51
|
+
) {
|
|
52
|
+
this.id = generateActionId();
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async execute(input: unknown, ctx: ActionContext): Promise<ActionResult<O>> {
|
|
56
|
+
try {
|
|
57
|
+
// Check rate limit
|
|
58
|
+
if (this.config.rateLimit) {
|
|
59
|
+
const rateLimitError = await checkRateLimit(this.config.rateLimit, ctx);
|
|
60
|
+
if (rateLimitError) {
|
|
61
|
+
return { success: false, error: rateLimitError };
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Run middleware
|
|
66
|
+
if (this.config.middleware) {
|
|
67
|
+
for (const middleware of this.config.middleware) {
|
|
68
|
+
const result = await middleware(ctx);
|
|
69
|
+
if (result) {
|
|
70
|
+
return { success: false, error: result };
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Validate input
|
|
76
|
+
let validatedInput: I;
|
|
77
|
+
if (this.config.input) {
|
|
78
|
+
const result = this.config.input.safeParse(input);
|
|
79
|
+
if (!result.success) {
|
|
80
|
+
return {
|
|
81
|
+
success: false,
|
|
82
|
+
error: {
|
|
83
|
+
code: 'VALIDATION_ERROR',
|
|
84
|
+
message: 'Input validation failed',
|
|
85
|
+
fieldErrors: result.error.flatten().fieldErrors as Record<string, string[]>,
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
validatedInput = result.data as I;
|
|
90
|
+
} else {
|
|
91
|
+
validatedInput = input as I;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Execute handler
|
|
95
|
+
const output = await this.handler(validatedInput, ctx);
|
|
96
|
+
|
|
97
|
+
// Validate output
|
|
98
|
+
if (this.config.output) {
|
|
99
|
+
const result = this.config.output.safeParse(output);
|
|
100
|
+
if (!result.success) {
|
|
101
|
+
return {
|
|
102
|
+
success: false,
|
|
103
|
+
error: {
|
|
104
|
+
code: 'OUTPUT_VALIDATION_ERROR',
|
|
105
|
+
message: 'Output validation failed',
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return { success: true, data: output };
|
|
112
|
+
} catch (error) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
error: {
|
|
116
|
+
code: 'INTERNAL_ERROR',
|
|
117
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
118
|
+
},
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function generateActionId(): string {
|
|
125
|
+
return `action_${Math.random().toString(36).substring(2, 11)}`;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async function checkRateLimit(
|
|
129
|
+
config: RateLimitConfig,
|
|
130
|
+
ctx: ActionContext
|
|
131
|
+
): Promise<ActionError | null> {
|
|
132
|
+
const key = config.key ? config.key(ctx) : ctx.ip;
|
|
133
|
+
const windowMs = parseTimeWindow(config.window);
|
|
134
|
+
const now = Date.now();
|
|
135
|
+
|
|
136
|
+
const record = rateLimitStore.get(key);
|
|
137
|
+
|
|
138
|
+
if (!record || now > record.resetAt) {
|
|
139
|
+
rateLimitStore.set(key, {
|
|
140
|
+
count: 1,
|
|
141
|
+
resetAt: now + windowMs,
|
|
142
|
+
});
|
|
143
|
+
return null;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
if (record.count >= config.max) {
|
|
147
|
+
return {
|
|
148
|
+
code: 'RATE_LIMIT_EXCEEDED',
|
|
149
|
+
message: `Rate limit exceeded. Try again in ${Math.ceil((record.resetAt - now) / 1000)}s`,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
record.count++;
|
|
154
|
+
return null;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function parseTimeWindow(window: string): number {
|
|
158
|
+
const match = window.match(/^(\d+)([smhd])$/);
|
|
159
|
+
if (!match) return 60000; // Default 1 minute
|
|
160
|
+
|
|
161
|
+
const [, num, unit] = match;
|
|
162
|
+
const multiplier = {
|
|
163
|
+
s: 1000,
|
|
164
|
+
m: 60000,
|
|
165
|
+
h: 3600000,
|
|
166
|
+
d: 86400000,
|
|
167
|
+
}[unit] || 60000;
|
|
168
|
+
|
|
169
|
+
return parseInt(num) * multiplier;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Factory function
|
|
173
|
+
export function action(): ActionBuilder<unknown, unknown> {
|
|
174
|
+
return new ActionBuilder();
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// Built-in middleware
|
|
178
|
+
export function requireAuth(): Middleware {
|
|
179
|
+
return (ctx) => {
|
|
180
|
+
if (!ctx.user) {
|
|
181
|
+
return {
|
|
182
|
+
code: 'UNAUTHORIZED',
|
|
183
|
+
message: 'Authentication required',
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
return null;
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export function requireRole(_roles: string[]): Middleware {
|
|
191
|
+
return (ctx) => {
|
|
192
|
+
if (!ctx.user) {
|
|
193
|
+
return {
|
|
194
|
+
code: 'UNAUTHORIZED',
|
|
195
|
+
message: 'Authentication required',
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
// In real implementation, check user roles
|
|
199
|
+
return null;
|
|
200
|
+
};
|
|
201
|
+
}
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
import type {
|
|
3
|
+
APIConfig,
|
|
4
|
+
APIHandler,
|
|
5
|
+
ActionContext,
|
|
6
|
+
Middleware,
|
|
7
|
+
} from './types.js';
|
|
8
|
+
|
|
9
|
+
export class APIBuilder<I, O> {
|
|
10
|
+
private config: APIConfig<I, O>;
|
|
11
|
+
|
|
12
|
+
constructor(config: APIConfig<I, O> = {}) {
|
|
13
|
+
this.config = config;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
query<T extends ZodType>(schema: T): APIBuilder<ReturnType<T['parse']>, O> {
|
|
17
|
+
return new APIBuilder({ ...this.config, query: schema });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
body<T extends ZodType>(schema: T): APIBuilder<ReturnType<T['parse']>, O> {
|
|
21
|
+
return new APIBuilder({ ...this.config, body: schema });
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
output<T extends ZodType>(schema: T): APIBuilder<I, ReturnType<T['parse']>> {
|
|
25
|
+
return new APIBuilder({ ...this.config, output: schema });
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
use(middleware: Middleware): this {
|
|
29
|
+
this.config.middleware = [...(this.config.middleware || []), middleware];
|
|
30
|
+
return this;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
handler(fn: APIHandler<I, O>): (request: Request) => Promise<Response> {
|
|
34
|
+
return async (request: Request) => {
|
|
35
|
+
try {
|
|
36
|
+
const ctx = await createContext(request);
|
|
37
|
+
|
|
38
|
+
// Run middleware
|
|
39
|
+
if (this.config.middleware) {
|
|
40
|
+
for (const middleware of this.config.middleware) {
|
|
41
|
+
const result = await middleware(ctx);
|
|
42
|
+
if (result) {
|
|
43
|
+
return Response.json(
|
|
44
|
+
{ success: false, error: result },
|
|
45
|
+
{ status: 400 }
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Parse input
|
|
52
|
+
let input: I = {} as I;
|
|
53
|
+
|
|
54
|
+
if (this.config.query) {
|
|
55
|
+
const url = new URL(request.url);
|
|
56
|
+
const query = Object.fromEntries(url.searchParams);
|
|
57
|
+
const result = this.config.query.safeParse(query);
|
|
58
|
+
if (!result.success) {
|
|
59
|
+
return Response.json(
|
|
60
|
+
{
|
|
61
|
+
success: false,
|
|
62
|
+
error: {
|
|
63
|
+
code: 'VALIDATION_ERROR',
|
|
64
|
+
message: 'Query validation failed',
|
|
65
|
+
fieldErrors: result.error.flatten().fieldErrors,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
{ status: 400 }
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
input = { ...input, ...result.data };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if (this.config.body && request.body) {
|
|
75
|
+
const body = await request.json();
|
|
76
|
+
const result = this.config.body.safeParse(body);
|
|
77
|
+
if (!result.success) {
|
|
78
|
+
return Response.json(
|
|
79
|
+
{
|
|
80
|
+
success: false,
|
|
81
|
+
error: {
|
|
82
|
+
code: 'VALIDATION_ERROR',
|
|
83
|
+
message: 'Body validation failed',
|
|
84
|
+
fieldErrors: result.error.flatten().fieldErrors,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
{ status: 400 }
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
input = { ...input, ...result.data };
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Execute handler
|
|
94
|
+
const response = await fn(input, ctx);
|
|
95
|
+
return response;
|
|
96
|
+
} catch (error) {
|
|
97
|
+
console.error('API Error:', error);
|
|
98
|
+
return Response.json(
|
|
99
|
+
{
|
|
100
|
+
success: false,
|
|
101
|
+
error: {
|
|
102
|
+
code: 'INTERNAL_ERROR',
|
|
103
|
+
message: error instanceof Error ? error.message : 'Unknown error',
|
|
104
|
+
},
|
|
105
|
+
},
|
|
106
|
+
{ status: 500 }
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
async function createContext(request: Request): Promise<ActionContext> {
|
|
114
|
+
const headers = request.headers;
|
|
115
|
+
const cookies = parseCookies(headers.get('cookie') || '');
|
|
116
|
+
|
|
117
|
+
return {
|
|
118
|
+
request,
|
|
119
|
+
headers,
|
|
120
|
+
cookies,
|
|
121
|
+
ip: headers.get('x-forwarded-for') || 'unknown',
|
|
122
|
+
userAgent: headers.get('user-agent') || 'unknown',
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function parseCookies(cookieHeader: string): Map<string, string> {
|
|
127
|
+
const cookies = new Map<string, string>();
|
|
128
|
+
if (!cookieHeader) return cookies;
|
|
129
|
+
|
|
130
|
+
cookieHeader.split(';').forEach((cookie) => {
|
|
131
|
+
const [name, value] = cookie.trim().split('=');
|
|
132
|
+
if (name && value) {
|
|
133
|
+
cookies.set(name, decodeURIComponent(value));
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
return cookies;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Factory function
|
|
141
|
+
export function api(): APIBuilder<unknown, unknown> {
|
|
142
|
+
return new APIBuilder();
|
|
143
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Zylaris Server - Server-side utilities
|
|
2
|
+
|
|
3
|
+
export { Action, ActionBuilder, action, requireAuth, requireRole } from './action.js';
|
|
4
|
+
export { APIBuilder, api } from './api.js';
|
|
5
|
+
|
|
6
|
+
export type {
|
|
7
|
+
ActionContext,
|
|
8
|
+
User,
|
|
9
|
+
Session,
|
|
10
|
+
ActionConfig,
|
|
11
|
+
ActionResult,
|
|
12
|
+
ActionError,
|
|
13
|
+
Middleware,
|
|
14
|
+
RateLimitConfig,
|
|
15
|
+
ActionHandler,
|
|
16
|
+
APIConfig,
|
|
17
|
+
APIHandler,
|
|
18
|
+
} from './types.js';
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import type { ZodType } from 'zod';
|
|
2
|
+
|
|
3
|
+
export interface ActionContext {
|
|
4
|
+
request: Request;
|
|
5
|
+
headers: Headers;
|
|
6
|
+
cookies: Map<string, string>;
|
|
7
|
+
user?: User;
|
|
8
|
+
session?: Session;
|
|
9
|
+
ip: string;
|
|
10
|
+
userAgent: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface User {
|
|
14
|
+
id: string;
|
|
15
|
+
email: string;
|
|
16
|
+
name?: string;
|
|
17
|
+
[key: string]: unknown;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface Session {
|
|
21
|
+
id: string;
|
|
22
|
+
userId: string;
|
|
23
|
+
expiresAt: Date;
|
|
24
|
+
[key: string]: unknown;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ActionConfig<I, O> {
|
|
28
|
+
input?: ZodType<I>;
|
|
29
|
+
output?: ZodType<O>;
|
|
30
|
+
middleware?: Middleware[];
|
|
31
|
+
rateLimit?: RateLimitConfig;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ActionResult<O> {
|
|
35
|
+
success: boolean;
|
|
36
|
+
data?: O;
|
|
37
|
+
error?: ActionError;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ActionError {
|
|
41
|
+
code: string;
|
|
42
|
+
message: string;
|
|
43
|
+
fieldErrors?: Record<string, string[]>;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type Middleware = (
|
|
47
|
+
ctx: ActionContext
|
|
48
|
+
) => Promise<ActionError | null | undefined> | ActionError | null | undefined;
|
|
49
|
+
|
|
50
|
+
export interface RateLimitConfig {
|
|
51
|
+
max: number;
|
|
52
|
+
window: string; // e.g., '1m', '1h'
|
|
53
|
+
key?: (ctx: ActionContext) => string;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export type ActionHandler<I, O> = (
|
|
57
|
+
input: I,
|
|
58
|
+
ctx: ActionContext
|
|
59
|
+
) => Promise<O> | O;
|
|
60
|
+
|
|
61
|
+
// API Route types
|
|
62
|
+
export interface APIConfig<I, O> {
|
|
63
|
+
query?: ZodType<I>;
|
|
64
|
+
body?: ZodType<I>;
|
|
65
|
+
output?: ZodType<O>;
|
|
66
|
+
middleware?: Middleware[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export type APIHandler<_I, _O> = (
|
|
70
|
+
input: _I,
|
|
71
|
+
ctx: ActionContext
|
|
72
|
+
) => Promise<Response> | Response;
|