@arcote.tech/arc-host 0.1.4 → 0.1.6

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/host.d.ts CHANGED
@@ -15,6 +15,10 @@ declare class RTCHost implements RealTimeCommunicationAdapter {
15
15
  * Convert JWT payload to AuthContext
16
16
  */
17
17
  private tokenToAuthContext;
18
+ /**
19
+ * Extract client IP address from request headers
20
+ */
21
+ private getClientIpAddress;
18
22
  /**
19
23
  * Get default auth context for anonymous users
20
24
  */
@@ -29,6 +33,7 @@ declare class RTCHost implements RealTimeCommunicationAdapter {
29
33
  private setNestedProperty;
30
34
  private handleCommand;
31
35
  private handleQuery;
36
+ private handleRoute;
32
37
  private setupServer;
33
38
  private isPublicEndpoint;
34
39
  private handleSync;
@@ -1 +1 @@
1
- {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../host.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAElB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EAGvB,KAAK,4BAA4B,EAClC,MAAM,kBAAkB,CAAC;AAI1B,cAAM,OAAQ,YAAW,4BAA4B;IAMjD,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,KAAK,CAAuB;gBAG1B,OAAO,EAAE,aAAa,EAC9B,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC;IASrC,aAAa,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,IAAI;IAI5C,IAAI,CACR,gBAAgB,EAAE,CAAC,EACjB,KAAK,EACL,IAAI,GACL,EAAE;QACD,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,IAAI,GACT,OAAO,CAAC,IAAI,CAAC;YAIF,eAAe;IAU7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAO1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAO7B;;OAEG;YACW,qBAAqB;IAWnC;;OAEG;IACH,OAAO,CAAC,iBAAiB;YAwCX,aAAa;YAmDb,WAAW;IAgEzB,OAAO,CAAC,WAAW;IA6DnB,OAAO,CAAC,gBAAgB;YAOV,UAAU;YA+CV,SAAS;IAevB,OAAO,CAAC,cAAc;CAGvB;AAED,eAAO,MAAM,cAAc,YACf,aAAa,aAAa,OAAO,CAAC,eAAe,CAAC,kBAE3D,CAAC"}
1
+ {"version":3,"file":"host.d.ts","sourceRoot":"","sources":["../host.ts"],"names":[],"mappings":"AAAA,OAAO,EAOL,KAAK,aAAa,EAElB,KAAK,eAAe,EACpB,KAAK,kBAAkB,EAGvB,KAAK,4BAA4B,EAClC,MAAM,kBAAkB,CAAC;AAI1B,cAAM,OAAQ,YAAW,4BAA4B;IAMjD,OAAO,CAAC,OAAO;IALjB,OAAO,CAAC,MAAM,CAAU;IACxB,OAAO,CAAC,SAAS,CAAoB;IACrC,OAAO,CAAC,KAAK,CAAuB;gBAG1B,OAAO,EAAE,aAAa,EAC9B,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC;IASrC,aAAa,CAAC,OAAO,EAAE,kBAAkB,EAAE,GAAG,IAAI;IAI5C,IAAI,CACR,gBAAgB,EAAE,CAAC,EACjB,KAAK,EACL,IAAI,GACL,EAAE;QACD,KAAK,EAAE,MAAM,CAAC;QACd,IAAI,EAAE,MAAM,CAAC;KACd,KAAK,IAAI,GACT,OAAO,CAAC,IAAI,CAAC;YAIF,eAAe;IAS7B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAW1B;;OAEG;IACH,OAAO,CAAC,kBAAkB;IAuB1B;;OAEG;IACH,OAAO,CAAC,qBAAqB;IAQ7B;;OAEG;YACW,qBAAqB;IAWnC;;OAEG;IACH,OAAO,CAAC,iBAAiB;YAwCX,aAAa;YAoDb,WAAW;YAiEX,WAAW;IAgEzB,OAAO,CAAC,WAAW;IAmEnB,OAAO,CAAC,gBAAgB;YA4BV,UAAU;YA+CV,SAAS;IAevB,OAAO,CAAC,cAAc;CAGvB;AAED,eAAO,MAAM,cAAc,YACf,aAAa,aAAa,OAAO,CAAC,eAAe,CAAC,kBAE3D,CAAC"}
package/dist/index.js CHANGED
@@ -4904,6 +4904,7 @@ var constructorString = Object.prototype.constructor.toString();
4904
4904
  class ArcContextElement {
4905
4905
  $event;
4906
4906
  name;
4907
+ queryBuilder;
4907
4908
  commandContext;
4908
4909
  commandClient;
4909
4910
  observer;
@@ -5632,7 +5633,7 @@ class ArcFindQuery extends ArcCollectionQuery {
5632
5633
  }
5633
5634
  onChange(change) {
5634
5635
  const lastResult = this.lastResult;
5635
- const lastResultAsArray = lastResult?.toArray() || [];
5636
+ const lastResultAsArray = lastResult || [];
5636
5637
  const index = lastResultAsArray.findIndex((e) => e._id === (change.type === "delete" ? change.id : change.id));
5637
5638
  const isInLastResult = index !== -1;
5638
5639
  const shouldBeInTheResult = change.type !== "delete" && this.checkItem(change.item);
@@ -6654,6 +6655,40 @@ class Model extends ModelBase {
6654
6655
  }
6655
6656
  });
6656
6657
  }
6658
+ routes(authContext) {
6659
+ return new Proxy({}, {
6660
+ get: (_, name) => {
6661
+ const element3 = this.context.elements.find((element4) => element4.name === name);
6662
+ if (!element3) {
6663
+ throw new Error(`Route element "${String(name)}" not found`);
6664
+ }
6665
+ if (typeof element3.getHandler !== "function") {
6666
+ throw new Error(`Element "${String(name)}" does not have route handlers`);
6667
+ }
6668
+ return async (method, req, routeParams, url) => {
6669
+ const handler = element3.getHandler(method);
6670
+ if (!handler) {
6671
+ throw new Error(`Method ${method} not allowed for route ${String(name)}`);
6672
+ }
6673
+ const forkedDataStorage = this.dataStorage.fork();
6674
+ const eventPublisher = new EventPublisher(this.context, this.dataStorage, authContext);
6675
+ const publishEvent = async (event3) => {
6676
+ await eventPublisher.publishEvent(event3, forkedDataStorage);
6677
+ };
6678
+ const commandContext = this.context.commandContext(forkedDataStorage, publishEvent, authContext);
6679
+ try {
6680
+ const result = await handler(commandContext, req, routeParams, url);
6681
+ await forkedDataStorage.merge();
6682
+ eventPublisher.runAsyncListeners();
6683
+ return result;
6684
+ } catch (error) {
6685
+ this.catchErrorCallback(error);
6686
+ throw error;
6687
+ }
6688
+ };
6689
+ }
6690
+ });
6691
+ }
6657
6692
  get $debug() {
6658
6693
  return {};
6659
6694
  }
@@ -6719,20 +6754,36 @@ class RTCHost {
6719
6754
  const payload = verifyToken(token);
6720
6755
  return payload;
6721
6756
  } catch (error) {
6722
- console.error("Token verification failed:", error);
6723
6757
  return null;
6724
6758
  }
6725
6759
  }
6726
- tokenToAuthContext(payload) {
6760
+ tokenToAuthContext(payload, ipAddress) {
6727
6761
  return {
6728
6762
  userId: payload.userId,
6729
- roles: []
6763
+ roles: [],
6764
+ ipAddress
6730
6765
  };
6731
6766
  }
6732
- getDefaultAuthContext() {
6767
+ getClientIpAddress(req) {
6768
+ const xForwardedFor = req.headers.get("x-forwarded-for");
6769
+ const xRealIp = req.headers.get("x-real-ip");
6770
+ const cfConnectingIp = req.headers.get("cf-connecting-ip");
6771
+ if (xForwardedFor) {
6772
+ return xForwardedFor.split(",")[0].trim();
6773
+ }
6774
+ if (xRealIp) {
6775
+ return xRealIp;
6776
+ }
6777
+ if (cfConnectingIp) {
6778
+ return cfConnectingIp;
6779
+ }
6780
+ return;
6781
+ }
6782
+ getDefaultAuthContext(ipAddress) {
6733
6783
  return {
6734
- userId: "anonymous",
6735
- roles: []
6784
+ userId: undefined,
6785
+ roles: [],
6786
+ ipAddress
6736
6787
  };
6737
6788
  }
6738
6789
  async parseFormDataToObject(formData) {
@@ -6784,15 +6835,16 @@ class RTCHost {
6784
6835
  try {
6785
6836
  const authHeader = req.headers.get("Authorization");
6786
6837
  const token = authHeader?.replace("Bearer ", "");
6838
+ const clientIp = this.getClientIpAddress(req);
6787
6839
  let authContext;
6788
6840
  if (token && !this.isPublicEndpoint(url.pathname)) {
6789
6841
  const payload = await this.verifyAuthToken(token);
6790
6842
  if (!payload) {
6791
6843
  return new Response("Invalid or expired token", { status: 401 });
6792
6844
  }
6793
- authContext = this.tokenToAuthContext(payload);
6845
+ authContext = this.tokenToAuthContext(payload, clientIp);
6794
6846
  } else {
6795
- authContext = this.getDefaultAuthContext();
6847
+ authContext = this.getDefaultAuthContext(clientIp);
6796
6848
  }
6797
6849
  let argument;
6798
6850
  const contentType = req.headers.get("Content-Type");
@@ -6817,13 +6869,14 @@ class RTCHost {
6817
6869
  try {
6818
6870
  const authHeader = req.headers.get("Authorization");
6819
6871
  const token = authHeader?.replace("Bearer ", "");
6872
+ const clientIp = this.getClientIpAddress(req);
6820
6873
  let authContext;
6821
6874
  if (token) {
6822
6875
  const payload = await this.verifyAuthToken(token);
6823
6876
  if (!payload) {
6824
6877
  return new Response("Invalid or expired token", { status: 401 });
6825
6878
  }
6826
- authContext = this.tokenToAuthContext(payload);
6879
+ authContext = this.tokenToAuthContext(payload, clientIp);
6827
6880
  } else {
6828
6881
  return new Response("Authorization token required", { status: 401 });
6829
6882
  }
@@ -6856,6 +6909,50 @@ class RTCHost {
6856
6909
  });
6857
6910
  }
6858
6911
  }
6912
+ async handleRoute(req) {
6913
+ const url = new URL(req.url);
6914
+ const method = req.method;
6915
+ let matchedRoute = null;
6916
+ let routeParams = {};
6917
+ for (const element of this.context.elements) {
6918
+ if (typeof element.matchesRoutePath === "function") {
6919
+ const { matches, params } = element.matchesRoutePath(url.pathname);
6920
+ if (matches) {
6921
+ matchedRoute = element;
6922
+ routeParams = params || {};
6923
+ break;
6924
+ }
6925
+ }
6926
+ }
6927
+ if (!matchedRoute) {
6928
+ return new Response("Route not found", { status: 404 });
6929
+ }
6930
+ const handler = matchedRoute.getHandler(method);
6931
+ if (!handler) {
6932
+ return new Response(`Method ${method} not allowed`, { status: 405 });
6933
+ }
6934
+ try {
6935
+ const authHeader = req.headers.get("Authorization");
6936
+ const token = authHeader?.replace("Bearer ", "");
6937
+ const clientIp = this.getClientIpAddress(req);
6938
+ let authContext;
6939
+ if (token && !matchedRoute.isPublic) {
6940
+ const payload = await this.verifyAuthToken(token);
6941
+ if (!payload) {
6942
+ return new Response("Invalid or expired token", { status: 401 });
6943
+ }
6944
+ authContext = this.tokenToAuthContext(payload, clientIp);
6945
+ } else {
6946
+ authContext = this.getDefaultAuthContext(clientIp);
6947
+ }
6948
+ const routes = this.model.routes(authContext);
6949
+ const response = await routes[matchedRoute.name](method, req, routeParams, url);
6950
+ return response;
6951
+ } catch (error) {
6952
+ console.error(`Error executing route ${matchedRoute.name}:`, error);
6953
+ return new Response("Internal Server Error", { status: 500 });
6954
+ }
6955
+ }
6859
6956
  setupServer() {
6860
6957
  this.server = Bun.serve({
6861
6958
  fetch: async (req, server) => {
@@ -6886,6 +6983,10 @@ class RTCHost {
6886
6983
  if (url.pathname === "/query" && req.method === "POST") {
6887
6984
  return await this.handleQuery(req);
6888
6985
  }
6986
+ const routeResponse = await this.handleRoute(req);
6987
+ if (routeResponse.status !== 404) {
6988
+ return routeResponse;
6989
+ }
6889
6990
  return new Response("Not Found", { status: 404 });
6890
6991
  },
6891
6992
  websocket: {
@@ -6901,8 +7002,21 @@ class RTCHost {
6901
7002
  });
6902
7003
  }
6903
7004
  isPublicEndpoint(pathname) {
6904
- const publicEndpoints = ["/command/signin", "/command/register"];
6905
- return publicEndpoints.some((endpoint) => pathname === endpoint);
7005
+ for (const element of this.context.elements) {
7006
+ if (typeof element.matchesCommandPath === "function") {
7007
+ const { matches, isPublic } = element.matchesCommandPath(pathname);
7008
+ if (matches) {
7009
+ return isPublic;
7010
+ }
7011
+ }
7012
+ if (typeof element.matchesRoutePath === "function") {
7013
+ const { matches, isPublic } = element.matchesRoutePath(pathname);
7014
+ if (matches) {
7015
+ return isPublic;
7016
+ }
7017
+ }
7018
+ }
7019
+ return false;
6906
7020
  }
6907
7021
  async handleSync(lastDate) {
6908
7022
  const syncDate = new Date;
package/host.ts CHANGED
@@ -53,7 +53,6 @@ class RTCHost implements RealTimeCommunicationAdapter {
53
53
  const payload = verifyToken(token);
54
54
  return payload;
55
55
  } catch (error) {
56
- console.error("Token verification failed:", error);
57
56
  return null;
58
57
  }
59
58
  }
@@ -61,20 +60,51 @@ class RTCHost implements RealTimeCommunicationAdapter {
61
60
  /**
62
61
  * Convert JWT payload to AuthContext
63
62
  */
64
- private tokenToAuthContext(payload: TokenPayload): AuthContext {
63
+ private tokenToAuthContext(
64
+ payload: TokenPayload,
65
+ ipAddress?: string,
66
+ ): AuthContext {
65
67
  return {
66
68
  userId: payload.userId,
67
69
  roles: [], // Default to no roles, you may want to extend TokenPayload to include roles
70
+ ipAddress,
68
71
  };
69
72
  }
70
73
 
74
+ /**
75
+ * Extract client IP address from request headers
76
+ */
77
+ private getClientIpAddress(req: Request): string | undefined {
78
+ // Check common headers for client IP
79
+ const xForwardedFor = req.headers.get("x-forwarded-for");
80
+ const xRealIp = req.headers.get("x-real-ip");
81
+ const cfConnectingIp = req.headers.get("cf-connecting-ip");
82
+
83
+ if (xForwardedFor) {
84
+ // x-forwarded-for can contain multiple IPs, take the first one
85
+ return xForwardedFor.split(",")[0].trim();
86
+ }
87
+
88
+ if (xRealIp) {
89
+ return xRealIp;
90
+ }
91
+
92
+ if (cfConnectingIp) {
93
+ return cfConnectingIp;
94
+ }
95
+
96
+ // Fallback - this might not work in all environments
97
+ return undefined;
98
+ }
99
+
71
100
  /**
72
101
  * Get default auth context for anonymous users
73
102
  */
74
- private getDefaultAuthContext(): AuthContext {
103
+ private getDefaultAuthContext(ipAddress?: string): AuthContext {
75
104
  return {
76
- userId: "anonymous",
105
+ userId: undefined as any,
77
106
  roles: [],
107
+ ipAddress,
78
108
  };
79
109
  }
80
110
 
@@ -148,15 +178,16 @@ class RTCHost implements RealTimeCommunicationAdapter {
148
178
  const authHeader = req.headers.get("Authorization");
149
179
  const token = authHeader?.replace("Bearer ", "");
150
180
 
181
+ const clientIp = this.getClientIpAddress(req);
151
182
  let authContext: AuthContext;
152
183
  if (token && !this.isPublicEndpoint(url.pathname)) {
153
184
  const payload = await this.verifyAuthToken(token);
154
185
  if (!payload) {
155
186
  return new Response("Invalid or expired token", { status: 401 });
156
187
  }
157
- authContext = this.tokenToAuthContext(payload);
188
+ authContext = this.tokenToAuthContext(payload, clientIp);
158
189
  } else {
159
- authContext = this.getDefaultAuthContext();
190
+ authContext = this.getDefaultAuthContext(clientIp);
160
191
  }
161
192
 
162
193
  let argument: any;
@@ -192,13 +223,14 @@ class RTCHost implements RealTimeCommunicationAdapter {
192
223
  const authHeader = req.headers.get("Authorization");
193
224
  const token = authHeader?.replace("Bearer ", "");
194
225
 
226
+ const clientIp = this.getClientIpAddress(req);
195
227
  let authContext: AuthContext;
196
228
  if (token) {
197
229
  const payload = await this.verifyAuthToken(token);
198
230
  if (!payload) {
199
231
  return new Response("Invalid or expired token", { status: 401 });
200
232
  }
201
- authContext = this.tokenToAuthContext(payload);
233
+ authContext = this.tokenToAuthContext(payload, clientIp);
202
234
  } else {
203
235
  return new Response("Authorization token required", { status: 401 });
204
236
  }
@@ -250,6 +282,70 @@ class RTCHost implements RealTimeCommunicationAdapter {
250
282
  }
251
283
  }
252
284
 
285
+ private async handleRoute(req: Request) {
286
+ const url = new URL(req.url);
287
+ const method = req.method;
288
+
289
+ // Find matching route
290
+ let matchedRoute: any = null;
291
+ let routeParams: Record<string, string> = {};
292
+
293
+ for (const element of this.context.elements) {
294
+ // Check if element has matchesRoutePath method (ArcRoute)
295
+ if (typeof (element as any).matchesRoutePath === "function") {
296
+ const { matches, params } = (element as any).matchesRoutePath(
297
+ url.pathname,
298
+ );
299
+ if (matches) {
300
+ matchedRoute = element;
301
+ routeParams = params || {};
302
+ break;
303
+ }
304
+ }
305
+ }
306
+
307
+ if (!matchedRoute) {
308
+ return new Response("Route not found", { status: 404 });
309
+ }
310
+
311
+ const handler = matchedRoute.getHandler(method);
312
+ if (!handler) {
313
+ return new Response(`Method ${method} not allowed`, { status: 405 });
314
+ }
315
+
316
+ try {
317
+ // Extract token from Authorization header
318
+ const authHeader = req.headers.get("Authorization");
319
+ const token = authHeader?.replace("Bearer ", "");
320
+
321
+ const clientIp = this.getClientIpAddress(req);
322
+ let authContext: AuthContext;
323
+
324
+ if (token && !matchedRoute.isPublic) {
325
+ const payload = await this.verifyAuthToken(token);
326
+ if (!payload) {
327
+ return new Response("Invalid or expired token", { status: 401 });
328
+ }
329
+ authContext = this.tokenToAuthContext(payload, clientIp);
330
+ } else {
331
+ authContext = this.getDefaultAuthContext(clientIp);
332
+ }
333
+
334
+ // Use the model's routes method to properly handle event publishing
335
+ const routes = this.model.routes(authContext);
336
+ const response = await routes[matchedRoute.name](
337
+ method,
338
+ req,
339
+ routeParams,
340
+ url,
341
+ );
342
+ return response;
343
+ } catch (error) {
344
+ console.error(`Error executing route ${matchedRoute.name}:`, error);
345
+ return new Response("Internal Server Error", { status: 500 });
346
+ }
347
+ }
348
+
253
349
  private setupServer() {
254
350
  this.server = Bun.serve({
255
351
  fetch: async (req, server) => {
@@ -296,6 +392,12 @@ class RTCHost implements RealTimeCommunicationAdapter {
296
392
  return await this.handleQuery(req);
297
393
  }
298
394
 
395
+ // Try to handle as a route
396
+ const routeResponse = await this.handleRoute(req);
397
+ if (routeResponse.status !== 404) {
398
+ return routeResponse;
399
+ }
400
+
299
401
  return new Response("Not Found", { status: 404 });
300
402
  },
301
403
  websocket: {
@@ -312,10 +414,31 @@ class RTCHost implements RealTimeCommunicationAdapter {
312
414
  }
313
415
 
314
416
  private isPublicEndpoint(pathname: string): boolean {
315
- // Define which endpoints don't require authentication
316
- const publicEndpoints = ["/command/signin", "/command/register"];
417
+ // Iterate through all context elements and check if any match and are public
418
+ for (const element of this.context.elements) {
419
+ // Check if element has matchesCommandPath method (ArcCommand)
420
+ if (typeof (element as any).matchesCommandPath === "function") {
421
+ const { matches, isPublic } = (element as any).matchesCommandPath(
422
+ pathname,
423
+ );
424
+ if (matches) {
425
+ return isPublic;
426
+ }
427
+ }
428
+
429
+ // Check if element has matchesRoutePath method (ArcRoute)
430
+ if (typeof (element as any).matchesRoutePath === "function") {
431
+ const { matches, isPublic } = (element as any).matchesRoutePath(
432
+ pathname,
433
+ );
434
+ if (matches) {
435
+ return isPublic;
436
+ }
437
+ }
438
+ }
317
439
 
318
- return publicEndpoints.some((endpoint) => pathname === endpoint);
440
+ // Default to non-public if no matching element found
441
+ return false;
319
442
  }
320
443
 
321
444
  private async handleSync(lastDate: string | null) {
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
- "version": "0.1.4",
7
+ "version": "0.1.6",
8
8
  "private": false,
9
9
  "author": "Przemysław Krasiński [arcote.tech]",
10
10
  "dependencies": {