@anteros/core 0.0.1-alpha.1

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.
Files changed (55) hide show
  1. package/README.md +143 -0
  2. package/database/collection.ts +160 -0
  3. package/database/decorator.ts +172 -0
  4. package/database/file.ts +93 -0
  5. package/database/mongodbadapter.ts +1128 -0
  6. package/database/rest.ts +14 -0
  7. package/database/schema.ts +160 -0
  8. package/database/tenant.ts +37 -0
  9. package/database/workflow.ts +384 -0
  10. package/index.ts +28 -0
  11. package/lib/asyncContextStorage.ts +68 -0
  12. package/lib/define.ts +114 -0
  13. package/lib/error.ts +21 -0
  14. package/lib/files.ts +459 -0
  15. package/lib/middleware.ts +66 -0
  16. package/lib/routes.ts +44 -0
  17. package/lib/scripts.ts +47 -0
  18. package/lib/services.ts +45 -0
  19. package/lib/sockets.ts +44 -0
  20. package/lib/workflow.ts +60 -0
  21. package/package.json +31 -0
  22. package/server/api.ts +789 -0
  23. package/server/boot.ts +101 -0
  24. package/server/config.ts +107 -0
  25. package/server/env.ts +16 -0
  26. package/server/hono.ts +176 -0
  27. package/server/io.ts +15 -0
  28. package/server/routes.ts +48 -0
  29. package/server/security.ts +138 -0
  30. package/tests/api.test.ts +281 -0
  31. package/tsconfig.json +36 -0
  32. package/types/activity.d.ts +45 -0
  33. package/types/api.d.ts +85 -0
  34. package/types/collection.d.ts +82 -0
  35. package/types/config.d.ts +55 -0
  36. package/types/field.d.ts +72 -0
  37. package/types/file.d.ts +120 -0
  38. package/types/hook.d.ts +30 -0
  39. package/types/middleware.d.ts +18 -0
  40. package/types/mongo.d.ts +61 -0
  41. package/types/options.d.ts +7 -0
  42. package/types/rest.d.ts +18 -0
  43. package/types/route.d.ts +19 -0
  44. package/types/schema.d.ts +0 -0
  45. package/types/scripts.d.ts +10 -0
  46. package/types/service.d.ts +37 -0
  47. package/types/task.d.ts +12 -0
  48. package/types/tenant.d.ts +16 -0
  49. package/types/token.d.ts +14 -0
  50. package/types/websocket.d.ts +15 -0
  51. package/types/workflow.d.ts +91 -0
  52. package/utils/cache.ts +96 -0
  53. package/utils/crypto.ts +226 -0
  54. package/utils/func.ts +1037 -0
  55. package/utils/index.ts +17 -0
package/server/boot.ts ADDED
@@ -0,0 +1,101 @@
1
+
2
+ import { createApp } from './hono'
3
+ import type { ServerConfig } from '../types/config'
4
+ import dayjs from 'dayjs';
5
+ import pkg from '../package.json';
6
+ import boxen from 'boxen';
7
+ import "@colors/colors";
8
+ import { cfg, formatConfig } from './config' // import the config
9
+ import { syncTenants } from '../database/tenant'
10
+ import { syncCollections } from '../database/collection'
11
+ import { syncFileCollections } from '../database/file'
12
+
13
+ import { loadRoutes } from '../lib/routes'
14
+ import { runScripts } from '../lib/scripts'
15
+ import { io, engineIo, websocket } from './io'
16
+ import { loadServices } from '../lib/services'
17
+ import { loadSockets } from '../lib/sockets'
18
+ import { syncWorkflows } from '../lib/workflow'
19
+ import { loadTenantsMiddlewares } from '../lib/middleware'
20
+ type BootAppOptions = ServerConfig & {
21
+
22
+ }
23
+ async function bootApp(options: BootAppOptions = {} as BootAppOptions) {
24
+
25
+ try {
26
+
27
+ // Load required resources
28
+ //******************************* */
29
+ options = formatConfig(options);
30
+ await syncTenants(); // sync tenants and connect to database
31
+ await syncCollections(); // sync collections and create collections on database
32
+ await syncFileCollections(); // sync file collections
33
+ await loadServices(); // load services
34
+ await loadSockets(); // load websocket handlers
35
+ await syncWorkflows();
36
+ await loadTenantsMiddlewares();
37
+ loadRoutes(); // load routes
38
+ //******************************* */
39
+
40
+
41
+ const PORT = (cfg.server.port || 4000);
42
+ const NAME = cfg.server.name || process.env.APP_NAME || 'SERVER';
43
+ const useClusterMode = cfg.clusterMode || false;
44
+ const env = process.env.NODE_ENV || Bun.env.NODE_ENV || 'dev';
45
+
46
+
47
+ const app = createApp();
48
+
49
+ const server = Bun.serve({
50
+ port: PORT,
51
+ reusePort: useClusterMode,
52
+ fetch: (req, server) => {
53
+ const url = new URL(req.url);
54
+
55
+ if (url.pathname === "/socket.io/") {
56
+ return engineIo.handleRequest(req, server);
57
+ } else {
58
+ return app.fetch(req, server);
59
+ }
60
+
61
+ },
62
+ websocket: websocket,
63
+ maxRequestBodySize: 1024 * 1024 * 100, // 100MB
64
+ })
65
+
66
+ let box = '';
67
+ box += `${NAME}`.gray.underline + ` (PID: ${process.pid})\n\n`
68
+ box += `Env: ${env || 'dev'}`.green.bold + '\n'
69
+ box += `Cluster mode: ${useClusterMode ? 'On'.green.bold : 'Off'.red.bold}\n`.gray.bold
70
+ box += `Url: http://localhost:${PORT}`.gray.bold;
71
+ box += '\n\n';
72
+ box += `Last boot: 🔄 ${dayjs().format('YYYY-MM-DD HH:mm:ss')}`.gray;
73
+
74
+ console.log(boxen(box, {
75
+ title: ` @anteros/core ${pkg.version}`,
76
+ padding: 1,
77
+ float: 'left',
78
+ borderColor: 'gray',
79
+ titleAlignment: 'center',
80
+ borderStyle: 'double',
81
+ textAlignment: 'left',
82
+ dimBorder: true
83
+ }))
84
+
85
+
86
+ // After boot and App ready.
87
+ setTimeout(() => {
88
+ runScripts().catch(); // run scripts
89
+
90
+ }, 150);
91
+
92
+ return server;
93
+ } catch (err: any) {
94
+ console.error('Failed to boot server', err?.message);
95
+ process.exit(1);
96
+ }
97
+ }
98
+
99
+ export {
100
+ bootApp
101
+ }
@@ -0,0 +1,107 @@
1
+ import type { ServerConfig, Config } from "../types/config"
2
+ import path from "path"
3
+ import pkg from '../package.json';
4
+ import { cleanDeep } from "../utils/func";
5
+ const cfg: Config = { // app Config
6
+
7
+ server: {
8
+ port: 4000,
9
+ },
10
+ tenants: []
11
+ }
12
+
13
+
14
+
15
+
16
+ function formatConfig(config: ServerConfig) { // format the config
17
+
18
+ cfg.clusterMode = config.clusterMode ?? true;
19
+ cfg.server = {
20
+ ...cfg.server,
21
+ ...config.server,
22
+ }
23
+ cfg.version = config.version;
24
+ cfg.tenants = config.tenants ?? [];
25
+ return cfg;
26
+ }
27
+
28
+ async function loadAppConfig(serverConfig: ServerConfig) {
29
+ try {
30
+ /* const PKG =
31
+ await import(path.resolve(process.cwd(), 'package.json')); */
32
+ formatConfig({
33
+ ...serverConfig,
34
+ version: pkg.version,
35
+ });
36
+ } catch (err: any) {
37
+ console.error('Error loading app config', err?.message);
38
+ }
39
+ }
40
+
41
+
42
+
43
+ function safePublicConfig() {
44
+ return cleanDeep({
45
+ tenants: (cfg.tenants ?? []).map(t => ({
46
+ id: t.id,
47
+ name: t.name ?? t.id,
48
+ })),
49
+ collections: (cfg.collections ?? []).map(c => ({
50
+ _tenant_: c._tenant_,
51
+ slug: c.slug,
52
+ type: c.type,
53
+ actions: [
54
+ 'insertOne', 'insertMany', 'updateOne', 'updateMany',
55
+ 'deleteOne', 'deleteMany', 'findOne', 'find',
56
+ 'findOneAndUpdate', 'aggregate',
57
+ ...Object.keys(c.actions ?? {}),
58
+ ],
59
+ fields: (c.fields ?? []).map(f => ({
60
+ name: f.name,
61
+ type: f.type,
62
+ description: f.description,
63
+ required: f.required,
64
+ nullable: f.nullable,
65
+ empty: f.empty,
66
+ relation: f.relation,
67
+ enumOptions: f.enumOptions,
68
+ randomOptions: f.randomOptions,
69
+ defaultValue: f.defaultValue,
70
+ studio: f.studio,
71
+ })),
72
+ readOnlyFields: c.api?.readOnlyFields,
73
+ studio: c.studio,
74
+ })),
75
+ services: (cfg.services ?? []).map(s => ({
76
+ _tenant_: s._tenant_,
77
+ name: s.name,
78
+ enabled: s.enabled,
79
+ actions: Object.keys(s.actions ?? {}),
80
+ })),
81
+ fileCollections: (cfg.fileCollections ?? []).map(fc => ({
82
+ _tenant_: fc._tenant_,
83
+ slug: fc.slug,
84
+ fields: (fc.fields ?? []).map(f => ({
85
+ name: f.name,
86
+ type: f.type,
87
+ description: f.description,
88
+ required: f.required,
89
+ nullable: f.nullable,
90
+ empty: f.empty,
91
+ relation: f.relation,
92
+ enumOptions: f.enumOptions,
93
+ randomOptions: f.randomOptions,
94
+ defaultValue: f.defaultValue,
95
+ studio: f.studio,
96
+ })),
97
+ readOnlyFields: fc.api?.readOnlyFields,
98
+ })),
99
+ })
100
+ }
101
+
102
+ export {
103
+ formatConfig,
104
+ cfg,
105
+ loadAppConfig,
106
+ safePublicConfig
107
+ }
package/server/env.ts ADDED
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Hono context variables shared by middleware and route handlers.
3
+ */
4
+ export type HonoVariables = {
5
+ /** Present when `Authorization: Bearer <jwt>` was sent and verification succeeded. */
6
+ token?: {
7
+ /** Raw JWT string from the `Authorization` header. */
8
+ value: string | null;
9
+ /** Verified payload (claims). */
10
+ decoded: Record<string, unknown> | null;
11
+ /** Whether a token was provided in the Authorization header */
12
+ provided: boolean;
13
+ /** Whether the token is expired */
14
+ expired: boolean;
15
+ };
16
+ };
package/server/hono.ts ADDED
@@ -0,0 +1,176 @@
1
+ import { Hono } from "hono";
2
+ import { cors } from "hono/cors"
3
+ import { compress } from 'hono/compress'
4
+ import { ipRestriction } from "hono/ip-restriction"
5
+ import { bodyLimit } from "hono/body-limit"
6
+ import { secureHeaders } from "hono/secure-headers"
7
+ import { getConnInfo } from "hono/bun"
8
+ import { cfg } from "./config"
9
+ import { rateLimit } from "./security"
10
+ import { initializeRoutes } from "./routes";
11
+ import { sessionCtxStorage, asyncContextStorage, requestCtxStorage } from "../lib/asyncContextStorage";
12
+ import { initializeApi } from "./api";
13
+ import { getGlobalMiddlewares } from "../lib/middleware";
14
+ import { jwt } from "../utils/func";
15
+ import { AppError } from "../lib/error";
16
+ import type { HonoVariables } from "./env";
17
+
18
+ const app = new Hono<{ Variables: HonoVariables }>();
19
+ function createApp(): Hono<{ Variables: HonoVariables }> {
20
+ const allowHeaders = [
21
+ "Tenant-Id",
22
+ "Content-Type",
23
+ "Authorization",
24
+ "Accept",
25
+ "Origin",
26
+ "X-Requested-With",
27
+ "Access-Control-Request-Method",
28
+ "Access-Control-Request-Headers",
29
+ "CF-Connecting-IP",
30
+ "True-Client-IP",
31
+ "X-Forwarded-For",
32
+ "Cookie",
33
+ "X-Forwarded-Host",
34
+ ...(cfg.server.cors?.allowHeaders as string[] || []),
35
+ ]
36
+ const allowMethods = [
37
+ "GET",
38
+ "POST",
39
+ "PUT",
40
+ "DELETE",
41
+ "OPTIONS",
42
+ "PATCH",
43
+ "HEAD",
44
+ ...(cfg.server.cors?.allowMethods as string[] || []),
45
+ ]
46
+ // cors
47
+ const corsOrigin = cfg.server.cors?.origin as string[] | undefined;
48
+ const corsCredentials = cfg.server.cors?.credentials as boolean | undefined ?? false;
49
+
50
+ if (corsCredentials && (!corsOrigin || corsOrigin.includes('*'))) {
51
+ throw new Error(
52
+ 'CORS misconfiguration: credentials: true is incompatible with origin: *. '
53
+ + 'Set explicit origins in cfg.server.cors.origin.'
54
+ );
55
+ }
56
+
57
+
58
+
59
+ app.use(cors({
60
+ origin: corsOrigin || ['*'],
61
+ credentials: corsCredentials,
62
+ allowMethods: allowMethods,
63
+ allowHeaders: allowHeaders,
64
+ }));
65
+
66
+ // compress
67
+ app.use(compress());
68
+
69
+ // ip restriction
70
+ app.use(ipRestriction(getConnInfo, {
71
+ denyList: cfg.server?.ipRestriction?.denyList || [],
72
+ allowList: cfg.server?.ipRestriction?.allowList || [],
73
+ }))
74
+
75
+ // security headers
76
+ app.use(secureHeaders({
77
+ strictTransportSecurity: true,
78
+ xFrameOptions: true,
79
+ xContentTypeOptions: true,
80
+ xXssProtection: true,
81
+ referrerPolicy: true,
82
+ removePoweredBy: true,
83
+ }))
84
+
85
+
86
+ // body limit
87
+ app.use(bodyLimit({
88
+ maxSize: cfg.server.body?.maxSize ?? 1024 * 1024 * 100, // 100MB
89
+ }));
90
+
91
+
92
+
93
+ // rate limiting (global)
94
+ if (cfg.server.rateLimit?.enabled !== false) {
95
+ app.use('*', rateLimit({
96
+ windowMs: cfg.server.rateLimit?.windowMs ?? 60_000,
97
+ max: cfg.server.rateLimit?.max ?? 100,
98
+ }));
99
+
100
+ // stricter limit for login endpoints
101
+ app.use('/api/*/login', rateLimit({
102
+ windowMs: cfg.server.rateLimit?.login?.windowMs ?? 60_000,
103
+ max: cfg.server.rateLimit?.login?.max ?? 10,
104
+ }));
105
+ }
106
+
107
+ // async Context Storage
108
+ app.use(async (c, next) => {
109
+ return asyncContextStorage.run(new Map(), async () => {
110
+ const traceId = crypto.randomUUID();
111
+ const connInfo = getConnInfo(c);
112
+ requestCtxStorage.set('trace', { id: traceId });
113
+ requestCtxStorage.set('internal', false);
114
+
115
+
116
+
117
+ requestCtxStorage.set('meta', {
118
+ request: {
119
+ ip: connInfo.remote.address ?? c.req.header('CF-Connecting-IP') ?? c.req.header('True-Client-IP') ?? c.req.header('X-Forwarded-For') ?? 'unknown',
120
+ user_agent: c.req.header('User-Agent') ?? '',
121
+ headers: c.req.header(),
122
+ method: c.req.method,
123
+ path: c.req.path,
124
+ query: c.req.query(),
125
+ },
126
+ environment: Bun.env.NODE_ENV || process.env.NODE_ENV || 'dev',
127
+ hostname: Bun.env.HOSTNAME || process.env.HOSTNAME || 'localhost',
128
+ platform: Bun.env.PLATFORM || process.env.PLATFORM || 'unknown',
129
+ })
130
+ const bearer = c.req.header('Authorization')?.replace('Bearer ', '');
131
+ if (bearer) {
132
+ const { value, error } = await jwt.verify(bearer);
133
+ const isValid = !error && value != null;
134
+ const isExpired = !!error && error.toLowerCase().includes('exp');
135
+
136
+ const tokenData = {
137
+ value: bearer,
138
+ decoded: isValid ? (value as Record<string, unknown>) : null,
139
+ provided: true,
140
+ expired: isExpired || (value != null && typeof (value as any)?.exp === 'number' && (value as any).exp * 1000 < Date.now()),
141
+ };
142
+
143
+ requestCtxStorage.set('token', tokenData);
144
+ c.set('token', tokenData);
145
+ } else {
146
+ const emptyToken = { value: null, decoded: null, provided: false, expired: false };
147
+ requestCtxStorage.set('token', emptyToken);
148
+ c.set('token', emptyToken);
149
+ }
150
+
151
+ return await next();
152
+ })
153
+ })
154
+
155
+ // Global user-defined middlewares — has access to requestCtxStorage, asyncContextStorage, etc.
156
+ for (const mw of getGlobalMiddlewares()) {
157
+ app.use(mw.handler);
158
+ }
159
+
160
+ // initialize routes
161
+ initializeRoutes(app);
162
+
163
+ // initialize api
164
+ initializeApi(app);
165
+
166
+
167
+ return app;
168
+ }
169
+
170
+
171
+
172
+
173
+ export {
174
+ createApp,
175
+ app
176
+ }
package/server/io.ts ADDED
@@ -0,0 +1,15 @@
1
+ import { Server as Engine } from "@socket.io/bun-engine";
2
+ import { Server } from "socket.io";
3
+
4
+ const io = new Server();
5
+ const engineIo = new Engine();
6
+ io.bind(engineIo);
7
+ const { websocket } = engineIo.handler()
8
+
9
+
10
+
11
+ export {
12
+ io,
13
+ engineIo,
14
+ websocket
15
+ }
@@ -0,0 +1,48 @@
1
+ import type { Hono } from "hono";
2
+ import { cfg } from "./config";
3
+ import path from "path";
4
+ import { jwt } from "../utils/func";
5
+ import { useRest } from "../database/rest";
6
+ import { io } from "./io";
7
+ import type { HonoVariables } from "./env";
8
+
9
+ function initializeRoutes(app: Hono<{ Variables: HonoVariables }>) {
10
+ for (let route of cfg.routes ?? []) {
11
+ if (route._prefix_) {
12
+ let method = route.method.toLocaleLowerCase();
13
+ let routePath = path.posix.join(route._prefix_, route.path);
14
+
15
+ if (route.method == "GET") {
16
+ app.get(routePath, async (c) => { //
17
+ return route.handler({
18
+ c,
19
+ rest: new useRest({
20
+ tenant_id: route._tenant_,
21
+ }),
22
+ jwt: jwt,
23
+ io: io,
24
+ });
25
+ });
26
+ }
27
+
28
+ if (route.method == 'POST') {
29
+ app.post(routePath, async (c) => {
30
+ return route.handler({
31
+ c,
32
+ rest: new useRest({
33
+ tenant_id: route._tenant_,
34
+ }),
35
+ jwt: jwt,
36
+ io: io,
37
+ });
38
+ });
39
+ }
40
+ }
41
+ }
42
+
43
+ }
44
+
45
+
46
+ export {
47
+ initializeRoutes
48
+ }
@@ -0,0 +1,138 @@
1
+ import type { Context } from "hono";
2
+ import { getConnInfo } from "hono/bun";
3
+
4
+ // ─── Types ───────────────────────────────────────────────────────────────────
5
+
6
+ export type RateLimitStore = {
7
+ increment(key: string, windowMs: number): Promise<{ count: number; resetAt: number }>;
8
+ };
9
+
10
+ export type RateLimitOptions = {
11
+ windowMs?: number;
12
+ max?: number;
13
+ message?: string;
14
+ code?: string;
15
+ statusCode?: number;
16
+ keyGenerator?: (c: Context) => string | Promise<string>;
17
+ store?: RateLimitStore;
18
+ };
19
+
20
+ export type RedisClient = {
21
+ incr(key: string): Promise<number>;
22
+ pexpire(key: string, ms: number): Promise<number>;
23
+ pttl(key: string): Promise<number>;
24
+ };
25
+
26
+ // ─── In-Memory Store ─────────────────────────────────────────────────────────
27
+
28
+ interface MemoryEntry {
29
+ count: number;
30
+ resetAt: number;
31
+ }
32
+
33
+ export function createMemoryStore(cleanupIntervalMs = 60000): RateLimitStore & { dispose(): void } {
34
+ const store = new Map<string, MemoryEntry>();
35
+ let cleanupTimer: ReturnType<typeof setInterval> | null = null;
36
+
37
+ const cleanup = () => {
38
+ const now = Date.now();
39
+ for (const [key, entry] of store) {
40
+ if (entry.resetAt <= now) {
41
+ store.delete(key);
42
+ }
43
+ }
44
+ };
45
+
46
+ cleanupTimer = setInterval(cleanup, cleanupIntervalMs);
47
+ if (cleanupTimer && typeof cleanupTimer === "object" && "unref" in cleanupTimer) {
48
+ (cleanupTimer as any).unref();
49
+ }
50
+
51
+ return {
52
+ async increment(key: string, windowMs: number) {
53
+ const now = Date.now();
54
+ const entry = store.get(key);
55
+
56
+ if (!entry || entry.resetAt <= now) {
57
+ const resetAt = now + windowMs;
58
+ store.set(key, { count: 1, resetAt });
59
+ return { count: 1, resetAt };
60
+ }
61
+
62
+ entry.count += 1;
63
+ return { count: entry.count, resetAt: entry.resetAt };
64
+ },
65
+
66
+ dispose() {
67
+ if (cleanupTimer) {
68
+ clearInterval(cleanupTimer);
69
+ cleanupTimer = null;
70
+ }
71
+ store.clear();
72
+ },
73
+ };
74
+ }
75
+
76
+ // ─── Redis Store ─────────────────────────────────────────────────────────────
77
+
78
+ export function createRedisStore(redis: RedisClient): RateLimitStore {
79
+ return {
80
+ async increment(key: string, windowMs: number) {
81
+ const count = await redis.incr(key);
82
+
83
+ // First increment in this window – set the expiry
84
+ if (count === 1) {
85
+ await redis.pexpire(key, windowMs);
86
+ }
87
+
88
+ const ttl = await redis.pttl(key);
89
+ const resetAt = Date.now() + Math.max(0, ttl);
90
+
91
+ return { count, resetAt };
92
+ },
93
+ };
94
+ }
95
+
96
+ // ─── Default key generator ───────────────────────────────────────────────────
97
+
98
+ function defaultKeyGenerator(c: Context): string {
99
+ const connInfo = getConnInfo(c);
100
+ const ip =
101
+ connInfo.remote.address ??
102
+ c.req.header("CF-Connecting-IP") ??
103
+ c.req.header("True-Client-IP") ??
104
+ c.req.header("X-Forwarded-For") ??
105
+ "unknown";
106
+
107
+ return `rn:rl:${ip}`;
108
+ }
109
+
110
+ // ─── Middleware ──────────────────────────────────────────────────────────────
111
+
112
+ export function rateLimit(opts: RateLimitOptions = {}) {
113
+ const windowMs = opts.windowMs ?? 60_000;
114
+ const max = opts.max ?? 60;
115
+ const message = opts.message ?? "Too many requests, please try again later";
116
+ const code = opts.code ?? "RATE_LIMIT_EXCEEDED";
117
+ const statusCode = opts.statusCode ?? 429;
118
+ const keyGenerator = opts.keyGenerator ?? defaultKeyGenerator;
119
+ const store = opts.store ?? createMemoryStore();
120
+
121
+ return async (c: Context, next: () => Promise<void>): Promise<Response | void> => {
122
+ const key = await keyGenerator(c);
123
+ const { count, resetAt } = await store.increment(key, windowMs);
124
+ const remaining = Math.max(0, max - count);
125
+
126
+ c.header("X-RateLimit-Limit", String(max));
127
+ c.header("X-RateLimit-Remaining", String(remaining));
128
+ c.header("X-RateLimit-Reset", String(Math.ceil(resetAt / 1000)));
129
+
130
+ if (count > max) {
131
+ const retryAfter = Math.ceil((resetAt - Date.now()) / 1000);
132
+ c.header("Retry-After", String(retryAfter));
133
+ return c.json({ message, code }, statusCode as any);
134
+ }
135
+
136
+ await next();
137
+ };
138
+ }