@arcote.tech/arc-host 0.3.3 → 0.4.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.
- package/dist/index.d.ts +1 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1024 -364
- package/dist/index.js.map +12 -8
- package/dist/src/connection-manager.d.ts +16 -1
- package/dist/src/connection-manager.d.ts.map +1 -1
- package/dist/src/context-handler.d.ts +3 -5
- package/dist/src/context-handler.d.ts.map +1 -1
- package/dist/src/create-server.d.ts +36 -0
- package/dist/src/create-server.d.ts.map +1 -0
- package/dist/src/cron-scheduler.d.ts +30 -0
- package/dist/src/cron-scheduler.d.ts.map +1 -0
- package/dist/src/event-auth.d.ts +6 -1
- package/dist/src/event-auth.d.ts.map +1 -1
- package/dist/src/index.d.ts +6 -2
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/middleware/http.d.ts +15 -0
- package/dist/src/middleware/http.d.ts.map +1 -0
- package/dist/src/middleware/index.d.ts +4 -0
- package/dist/src/middleware/index.d.ts.map +1 -0
- package/dist/src/middleware/types.d.ts +31 -0
- package/dist/src/middleware/types.d.ts.map +1 -0
- package/dist/src/middleware/ws.d.ts +9 -0
- package/dist/src/middleware/ws.d.ts.map +1 -0
- package/dist/src/types.d.ts +25 -4
- package/dist/src/types.d.ts.map +1 -1
- package/index.ts +2 -4
- package/package.json +2 -1
- package/src/connection-manager.ts +37 -7
- package/src/context-handler.ts +22 -23
- package/src/create-server.ts +213 -0
- package/src/cron-scheduler.ts +124 -0
- package/src/event-auth.ts +26 -1
- package/src/index.ts +39 -9
- package/src/middleware/http.ts +414 -0
- package/src/middleware/index.ts +27 -0
- package/src/middleware/types.ts +42 -0
- package/src/middleware/ws.ts +266 -0
- package/src/types.ts +22 -4
- package/dist/src/arc-host.d.ts +0 -75
- package/dist/src/arc-host.d.ts.map +0 -1
- package/src/arc-host.ts +0 -818
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
import type { ArcContextAny, DatabaseAdapter } from "@arcote.tech/arc";
|
|
2
|
+
import type { Server } from "bun";
|
|
3
|
+
import jwt from "jsonwebtoken";
|
|
4
|
+
import { ConnectionManager } from "./connection-manager";
|
|
5
|
+
import { ContextHandler } from "./context-handler";
|
|
6
|
+
import { CronScheduler } from "./cron-scheduler";
|
|
7
|
+
import {
|
|
8
|
+
arcHttpHandlers,
|
|
9
|
+
arcWsHandlers,
|
|
10
|
+
cleanupClientSubs,
|
|
11
|
+
cleanupStreams,
|
|
12
|
+
} from "./middleware";
|
|
13
|
+
import type {
|
|
14
|
+
ArcHttpHandler,
|
|
15
|
+
ArcRequestContext,
|
|
16
|
+
ArcWsContext,
|
|
17
|
+
ArcWsHandler,
|
|
18
|
+
} from "./middleware/types";
|
|
19
|
+
import type { TokenPayload } from "./types";
|
|
20
|
+
|
|
21
|
+
type WebSocketData = { clientId: string };
|
|
22
|
+
|
|
23
|
+
export interface ArcServerConfig {
|
|
24
|
+
context: ArcContextAny;
|
|
25
|
+
dbAdapterFactory: (ctx: any) => Promise<DatabaseAdapter>;
|
|
26
|
+
httpHandlers?: ArcHttpHandler[];
|
|
27
|
+
wsHandlers?: ArcWsHandler[];
|
|
28
|
+
port?: number;
|
|
29
|
+
jwtSecret?: string;
|
|
30
|
+
/** Extra callback when a WS client disconnects (e.g. to clean up platform state) */
|
|
31
|
+
onWsClose?: (clientId: string) => void;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ArcServer {
|
|
35
|
+
server: Server<WebSocketData>;
|
|
36
|
+
contextHandler: ContextHandler;
|
|
37
|
+
connectionManager: ConnectionManager;
|
|
38
|
+
cronScheduler: CronScheduler;
|
|
39
|
+
stop: () => void;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Create an Arc server with composable HTTP + WS middleware.
|
|
44
|
+
*
|
|
45
|
+
* By default includes all Arc handlers (command, query, stream, route, eventSync, health,
|
|
46
|
+
* scope:auth, sync-events, request-sync, execute-command, view subscriptions).
|
|
47
|
+
* Platform servers add their own handlers on top.
|
|
48
|
+
*/
|
|
49
|
+
export async function createArcServer(
|
|
50
|
+
config: ArcServerConfig,
|
|
51
|
+
): Promise<ArcServer> {
|
|
52
|
+
const jwtSecret =
|
|
53
|
+
config.jwtSecret ||
|
|
54
|
+
process.env.JWT_SECRET ||
|
|
55
|
+
"arc-host-secret-change-in-production";
|
|
56
|
+
const port = config.port || 5005;
|
|
57
|
+
|
|
58
|
+
// Init context handler
|
|
59
|
+
const dbAdapter = config.dbAdapterFactory(config.context);
|
|
60
|
+
const contextHandler = new ContextHandler(config.context, dbAdapter);
|
|
61
|
+
await contextHandler.init();
|
|
62
|
+
|
|
63
|
+
// Start cron scheduler
|
|
64
|
+
const cronScheduler = new CronScheduler(contextHandler);
|
|
65
|
+
cronScheduler.start();
|
|
66
|
+
|
|
67
|
+
const connectionManager = new ConnectionManager();
|
|
68
|
+
|
|
69
|
+
const corsHeaders = {
|
|
70
|
+
"Access-Control-Allow-Origin": "*",
|
|
71
|
+
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
|
|
72
|
+
"Access-Control-Allow-Headers":
|
|
73
|
+
"Content-Type, Authorization, X-Arc-Scope",
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
function verifyToken(token: string): TokenPayload | null {
|
|
77
|
+
try {
|
|
78
|
+
const decoded = jwt.verify(token, jwtSecret) as any;
|
|
79
|
+
if (decoded.tokenName && !decoded.tokenType) {
|
|
80
|
+
return {
|
|
81
|
+
tokenType: decoded.tokenName,
|
|
82
|
+
params: decoded.params || {},
|
|
83
|
+
iat: decoded.iat,
|
|
84
|
+
exp: decoded.exp,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
return decoded as TokenPayload;
|
|
88
|
+
} catch {
|
|
89
|
+
try {
|
|
90
|
+
const parts = token.split(".");
|
|
91
|
+
if (parts.length !== 3) return null;
|
|
92
|
+
const payload = JSON.parse(atob(parts[1]));
|
|
93
|
+
return {
|
|
94
|
+
tokenType: payload.tokenName,
|
|
95
|
+
params: payload.params || {},
|
|
96
|
+
iat: payload.iat,
|
|
97
|
+
exp: payload.exp,
|
|
98
|
+
};
|
|
99
|
+
} catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Build handler chains — Arc defaults + user-provided extras
|
|
106
|
+
const defaultHttp = arcHttpHandlers(contextHandler, connectionManager);
|
|
107
|
+
const defaultWs = arcWsHandlers();
|
|
108
|
+
|
|
109
|
+
const httpHandlers = config.httpHandlers
|
|
110
|
+
? [...defaultHttp, ...config.httpHandlers]
|
|
111
|
+
: defaultHttp;
|
|
112
|
+
|
|
113
|
+
const wsHandlers = config.wsHandlers
|
|
114
|
+
? [...defaultWs, ...config.wsHandlers]
|
|
115
|
+
: defaultWs;
|
|
116
|
+
|
|
117
|
+
const wsCtx: ArcWsContext = {
|
|
118
|
+
contextHandler,
|
|
119
|
+
connectionManager,
|
|
120
|
+
verifyToken,
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const server = Bun.serve<WebSocketData>({
|
|
124
|
+
port,
|
|
125
|
+
idleTimeout: 255,
|
|
126
|
+
|
|
127
|
+
async fetch(req, server) {
|
|
128
|
+
const url = new URL(req.url);
|
|
129
|
+
|
|
130
|
+
// CORS preflight
|
|
131
|
+
if (req.method === "OPTIONS") {
|
|
132
|
+
return new Response(null, { headers: corsHeaders });
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Token extraction
|
|
136
|
+
const authHeader = req.headers.get("Authorization");
|
|
137
|
+
const rawToken =
|
|
138
|
+
authHeader?.replace("Bearer ", "") ||
|
|
139
|
+
url.searchParams.get("token");
|
|
140
|
+
const tokenPayload = rawToken ? verifyToken(rawToken) : null;
|
|
141
|
+
|
|
142
|
+
// WebSocket upgrade
|
|
143
|
+
if (
|
|
144
|
+
url.pathname === "/ws" &&
|
|
145
|
+
req.headers.get("Upgrade") === "websocket"
|
|
146
|
+
) {
|
|
147
|
+
if (server.upgrade(req, { data: { clientId: "" } })) return undefined;
|
|
148
|
+
return new Response("WebSocket upgrade failed", {
|
|
149
|
+
status: 500,
|
|
150
|
+
headers: corsHeaders,
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Run HTTP handler chain
|
|
155
|
+
const reqCtx: ArcRequestContext = {
|
|
156
|
+
rawToken,
|
|
157
|
+
tokenPayload,
|
|
158
|
+
corsHeaders,
|
|
159
|
+
};
|
|
160
|
+
for (const handler of httpHandlers) {
|
|
161
|
+
const response = await handler(req, url, reqCtx);
|
|
162
|
+
if (response) return response;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return new Response("Not Found", {
|
|
166
|
+
status: 404,
|
|
167
|
+
headers: corsHeaders,
|
|
168
|
+
});
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
websocket: {
|
|
172
|
+
open(ws) {
|
|
173
|
+
connectionManager.addClient(ws as any);
|
|
174
|
+
},
|
|
175
|
+
async message(ws, messageStr) {
|
|
176
|
+
const client = connectionManager.getClientByWs(ws as any);
|
|
177
|
+
if (!client) return;
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
const message = JSON.parse(messageStr as string);
|
|
181
|
+
for (const handler of wsHandlers) {
|
|
182
|
+
const handled = await handler(client, message, wsCtx);
|
|
183
|
+
if (handled) break;
|
|
184
|
+
}
|
|
185
|
+
} catch (error) {
|
|
186
|
+
console.error("Failed to parse WS message:", error);
|
|
187
|
+
}
|
|
188
|
+
},
|
|
189
|
+
close(ws) {
|
|
190
|
+
const client = connectionManager.getClientByWs(ws as any);
|
|
191
|
+
if (client) {
|
|
192
|
+
cleanupClientSubs(client.id);
|
|
193
|
+
config.onWsClose?.(client.id);
|
|
194
|
+
connectionManager.removeClient(client.id);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
console.log(`Arc Server running on http://localhost:${port}`);
|
|
201
|
+
|
|
202
|
+
return {
|
|
203
|
+
server,
|
|
204
|
+
contextHandler,
|
|
205
|
+
connectionManager,
|
|
206
|
+
cronScheduler,
|
|
207
|
+
stop: () => {
|
|
208
|
+
cronScheduler.stop();
|
|
209
|
+
cleanupStreams();
|
|
210
|
+
server.stop();
|
|
211
|
+
},
|
|
212
|
+
};
|
|
213
|
+
}
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CronScheduler
|
|
3
|
+
*
|
|
4
|
+
* Discovers aggregate cron methods from context elements and schedules them
|
|
5
|
+
* using node-cron. For each cron trigger, queries all instances of the aggregate
|
|
6
|
+
* and executes the command for each instance.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { ArcContextAny, AggregateCronMethodEntry } from "@arcote.tech/arc";
|
|
10
|
+
import { Cron } from "croner";
|
|
11
|
+
import type { ContextHandler } from "./context-handler";
|
|
12
|
+
|
|
13
|
+
interface CronJob {
|
|
14
|
+
entry: AggregateCronMethodEntry;
|
|
15
|
+
task: Cron;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export class CronScheduler {
|
|
19
|
+
private jobs: CronJob[] = [];
|
|
20
|
+
private contextHandler: ContextHandler;
|
|
21
|
+
|
|
22
|
+
constructor(contextHandler: ContextHandler) {
|
|
23
|
+
this.contextHandler = contextHandler;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Discover cron methods from all aggregates in the context and start scheduling.
|
|
28
|
+
*/
|
|
29
|
+
start(): void {
|
|
30
|
+
const context = this.contextHandler.context;
|
|
31
|
+
const cronEntries = this.discoverCronMethods(context);
|
|
32
|
+
|
|
33
|
+
if (cronEntries.length === 0) return;
|
|
34
|
+
|
|
35
|
+
console.log(
|
|
36
|
+
`[ARC:Cron] Discovered ${cronEntries.length} cron method(s):`,
|
|
37
|
+
);
|
|
38
|
+
|
|
39
|
+
for (const entry of cronEntries) {
|
|
40
|
+
console.log(
|
|
41
|
+
`[ARC:Cron] ${entry.aggregateName}.${entry.methodName} → "${entry.cronExpression}"`,
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
const task = new Cron(entry.cronExpression, async () => {
|
|
45
|
+
await this.executeCronMethod(entry);
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
this.jobs.push({ entry, task });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Stop all cron jobs.
|
|
54
|
+
*/
|
|
55
|
+
stop(): void {
|
|
56
|
+
for (const job of this.jobs) {
|
|
57
|
+
job.task.stop();
|
|
58
|
+
}
|
|
59
|
+
this.jobs = [];
|
|
60
|
+
console.log("[ARC:Cron] All cron jobs stopped.");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Extract cron method entries from all aggregate context elements.
|
|
65
|
+
*/
|
|
66
|
+
private discoverCronMethods(
|
|
67
|
+
context: ArcContextAny,
|
|
68
|
+
): AggregateCronMethodEntry[] {
|
|
69
|
+
const entries: AggregateCronMethodEntry[] = [];
|
|
70
|
+
|
|
71
|
+
for (const element of context.elements) {
|
|
72
|
+
const ctor = (element as any).ctor;
|
|
73
|
+
if (ctor?.__aggregateCronMethods) {
|
|
74
|
+
entries.push(...ctor.__aggregateCronMethods);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return entries;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Execute a cron method for all instances of the aggregate.
|
|
83
|
+
*/
|
|
84
|
+
private async executeCronMethod(
|
|
85
|
+
entry: AggregateCronMethodEntry,
|
|
86
|
+
): Promise<void> {
|
|
87
|
+
const commandName = `${entry.aggregateName}.${entry.methodName}`;
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
// Query all instances of this aggregate
|
|
91
|
+
const dataStorage = this.contextHandler.getDataStorage();
|
|
92
|
+
const store = dataStorage.getStore<any>(entry.aggregateName);
|
|
93
|
+
const instances = await store.find({});
|
|
94
|
+
|
|
95
|
+
if (instances.length === 0) {
|
|
96
|
+
console.log(
|
|
97
|
+
`[ARC:Cron] ${commandName}: no instances found, skipping.`,
|
|
98
|
+
);
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
console.log(
|
|
103
|
+
`[ARC:Cron] ${commandName}: executing for ${instances.length} instance(s)...`,
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
for (const instance of instances) {
|
|
107
|
+
try {
|
|
108
|
+
await this.contextHandler.executeCommand(
|
|
109
|
+
commandName,
|
|
110
|
+
{ _id: instance._id },
|
|
111
|
+
null,
|
|
112
|
+
);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
console.error(
|
|
115
|
+
`[ARC:Cron] ${commandName} failed for instance ${instance._id}:`,
|
|
116
|
+
error,
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
} catch (error) {
|
|
121
|
+
console.error(`[ARC:Cron] ${commandName} failed:`, error);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
}
|
package/src/event-auth.ts
CHANGED
|
@@ -109,7 +109,7 @@ function checkConditionsMatch(
|
|
|
109
109
|
}
|
|
110
110
|
|
|
111
111
|
/**
|
|
112
|
-
* Filter events that a token can receive
|
|
112
|
+
* Filter events that a single token can receive
|
|
113
113
|
*/
|
|
114
114
|
export function filterEventsForToken(
|
|
115
115
|
token: TokenPayload | null,
|
|
@@ -125,3 +125,28 @@ export function filterEventsForToken(
|
|
|
125
125
|
return canTokenReceiveEvent(token, eventDef, eventInstance);
|
|
126
126
|
});
|
|
127
127
|
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Filter events visible to ANY of the provided tokens.
|
|
131
|
+
* Used for multi-scope clients where an event visible to any scope should be sent.
|
|
132
|
+
*/
|
|
133
|
+
export function filterEventsForTokens(
|
|
134
|
+
tokens: TokenPayload[],
|
|
135
|
+
events: SyncableEvent[],
|
|
136
|
+
eventDefinitions: Map<string, ArcEventAny>,
|
|
137
|
+
): SyncableEvent[] {
|
|
138
|
+
if (tokens.length === 0) {
|
|
139
|
+
// No tokens — only public events
|
|
140
|
+
return filterEventsForToken(null, events, eventDefinitions);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return events.filter((eventInstance) => {
|
|
144
|
+
const eventDef = eventDefinitions.get(eventInstance.type);
|
|
145
|
+
if (!eventDef) return false;
|
|
146
|
+
|
|
147
|
+
// Event is visible if ANY token can receive it
|
|
148
|
+
return tokens.some((token) =>
|
|
149
|
+
canTokenReceiveEvent(token, eventDef, eventInstance),
|
|
150
|
+
);
|
|
151
|
+
});
|
|
152
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,44 @@
|
|
|
1
|
-
//
|
|
2
|
-
export {
|
|
1
|
+
// Server
|
|
2
|
+
export { createArcServer } from "./create-server";
|
|
3
|
+
export type { ArcServer, ArcServerConfig } from "./create-server";
|
|
4
|
+
|
|
5
|
+
// Middleware (composable handlers)
|
|
6
|
+
export {
|
|
7
|
+
arcHttpHandlers,
|
|
8
|
+
arcWsHandlers,
|
|
9
|
+
cleanupClientSubs,
|
|
10
|
+
cleanupStreams,
|
|
11
|
+
commandHandler,
|
|
12
|
+
eventSyncHandler,
|
|
13
|
+
executeCommandHandler,
|
|
14
|
+
healthHandler,
|
|
15
|
+
queryHandler,
|
|
16
|
+
requestSyncHandler,
|
|
17
|
+
routeHandler,
|
|
18
|
+
scopeAuthHandler,
|
|
19
|
+
streamHandler,
|
|
20
|
+
syncEventsHandler,
|
|
21
|
+
querySubscriptionHandler,
|
|
22
|
+
} from "./middleware";
|
|
23
|
+
export type {
|
|
24
|
+
ArcHttpHandler,
|
|
25
|
+
ArcRequestContext,
|
|
26
|
+
ArcWsContext,
|
|
27
|
+
ArcWsHandler,
|
|
28
|
+
} from "./middleware";
|
|
29
|
+
|
|
30
|
+
// Core (unchanged)
|
|
3
31
|
export { ConnectionManager } from "./connection-manager";
|
|
4
32
|
export { ContextHandler } from "./context-handler";
|
|
33
|
+
export { CronScheduler } from "./cron-scheduler";
|
|
34
|
+
|
|
35
|
+
// Auth utilities
|
|
36
|
+
export {
|
|
37
|
+
canTokenEmitEvent,
|
|
38
|
+
canTokenReceiveEvent,
|
|
39
|
+
filterEventsForToken,
|
|
40
|
+
filterEventsForTokens,
|
|
41
|
+
} from "./event-auth";
|
|
5
42
|
|
|
6
43
|
// Types
|
|
7
44
|
export type {
|
|
@@ -12,10 +49,3 @@ export type {
|
|
|
12
49
|
SyncableEvent,
|
|
13
50
|
TokenPayload,
|
|
14
51
|
} from "./types";
|
|
15
|
-
|
|
16
|
-
// Auth utilities
|
|
17
|
-
export {
|
|
18
|
-
canTokenEmitEvent,
|
|
19
|
-
canTokenReceiveEvent,
|
|
20
|
-
filterEventsForToken,
|
|
21
|
-
} from "./event-auth";
|