@arcote.tech/arc-host 0.7.25 → 0.7.27

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.
@@ -1,6 +1,13 @@
1
1
  import { type ArcContextAny, type ArcEventAny, type DatabaseAdapter, LocalEventPublisher, MasterDataStorage, Model } from "@arcote.tech/arc";
2
2
  import type { ArcTelemetry } from "@arcote.tech/arc-otel";
3
3
  import type { EventAuthContext, SyncableEvent, TokenPayload } from "./types";
4
+ /** Options for an in-process route invocation. */
5
+ export interface ExecuteRouteOptions {
6
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
7
+ body?: unknown;
8
+ headers?: Record<string, string>;
9
+ query?: Record<string, string>;
10
+ }
4
11
  /**
5
12
  * Handles a single context on the host
6
13
  */
@@ -11,9 +18,23 @@ export declare class ContextHandler {
11
18
  private dataStorage;
12
19
  private eventPublisher;
13
20
  private eventDefinitions;
21
+ /**
22
+ * Per-token verification registry: tokenName → ArcToken (with its secret).
23
+ * Collected from every protectBy() in the context. Fail-closed: a token
24
+ * without a secret cannot verify any JWT (→ requests rejected).
25
+ */
26
+ private tokenRegistry;
14
27
  private hostEventIdCounter;
15
28
  private initialized;
16
29
  constructor(context: ArcContextAny, dbAdapter: Promise<DatabaseAdapter>, telemetry?: ArcTelemetry | undefined);
30
+ /**
31
+ * Verify a raw JWT cryptographically (HMAC-SHA256) against the secret of the
32
+ * token definition named in its payload, and check expiry. Returns the
33
+ * decoded payload or null. No unsigned fallback — forged/invalid/expired
34
+ * tokens are rejected. This is the single verification entry point for both
35
+ * the HTTP boundary and in-process dispatch.
36
+ */
37
+ verifyToken(token: string): Promise<TokenPayload | null>;
17
38
  /**
18
39
  * Initialize the context handler and run seed data if needed.
19
40
  */
@@ -36,6 +57,16 @@ export declare class ContextHandler {
36
57
  executeCommand(commandName: string, params: any, rawToken: string | null, options?: {
37
58
  internal?: boolean;
38
59
  }): Promise<any>;
60
+ /**
61
+ * Execute a route handler in-process by NAME (no TCP). Mirrors the HTTP
62
+ * routeHandler boundary: real JWT verification, the same public/protected
63
+ * gate, scoped context, then the handler with a synthetic Request. Returns
64
+ * the handler's Response (or a 401/403/404/405 when the gate rejects).
65
+ *
66
+ * Used by webhooks/route tests and any in-process caller that needs the full
67
+ * route boundary without binding a port.
68
+ */
69
+ executeRoute(routeName: string, options: ExecuteRouteOptions, rawToken: string | null): Promise<Response>;
39
70
  /**
40
71
  * Persist events from a client and return with host IDs
41
72
  */
@@ -1 +1 @@
1
- {"version":3,"file":"context-handler.d.ts","sourceRoot":"","sources":["../../src/context-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EACL,KAAK,aAAa,EAClB,KAAK,WAAW,EAChB,KAAK,eAAe,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,EAGN,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE7E;;GAEG;AACH,qBAAa,cAAc;aASP,OAAO,EAAE,aAAa;IAEtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAV7B,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,cAAc,CAAsB;IAC5C,OAAO,CAAC,gBAAgB,CAAkC;IAC1D,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAS;gBAGV,OAAO,EAAE,aAAa,EACtC,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC,EAClB,SAAS,CAAC,EAAE,YAAY,YAAA;IAqB3C;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B;;;OAGG;YACW,QAAQ;IA4BtB;;;;;;;;;OASG;IACG,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,GAAG,EACX,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAC/B,OAAO,CAAC,GAAG,CAAC;IAoEf;;OAEG;IACG,aAAa,CACjB,MAAM,EAAE,KAAK,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,GAAG,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;KACvC,CAAC,EACF,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,GAAG,IAAI,GACzB,OAAO,CAAC,aAAa,EAAE,CAAC;IA8C3B;;OAEG;IACG,cAAc,CAClB,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,KAAK,EAAE,YAAY,GAAG,IAAI,GACzB,OAAO,CAAC,aAAa,EAAE,CAAC;IAkD3B;;OAEG;IACH,QAAQ,IAAI,KAAK,CAAC,aAAa,CAAC;IAIhC;;OAEG;IACH,cAAc,IAAI,iBAAiB;IAInC;;OAEG;IACH,mBAAmB,IAAI,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC;IAI/C;;OAEG;IACH,iBAAiB,IAAI,mBAAmB;CAGzC"}
1
+ {"version":3,"file":"context-handler.d.ts","sourceRoot":"","sources":["../../src/context-handler.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,WAAW,EAGhB,KAAK,eAAe,EACpB,mBAAmB,EACnB,iBAAiB,EACjB,KAAK,EAIN,MAAM,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,uBAAuB,CAAC;AAE1D,OAAO,KAAK,EAAE,gBAAgB,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE7E,kDAAkD;AAClD,MAAM,WAAW,mBAAmB;IAClC,MAAM,CAAC,EAAE,KAAK,GAAG,MAAM,GAAG,KAAK,GAAG,QAAQ,GAAG,OAAO,CAAC;IACrD,IAAI,CAAC,EAAE,OAAO,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,qBAAa,cAAc;aAeP,OAAO,EAAE,aAAa;IAEtC,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAC;IAhB7B,OAAO,CAAC,KAAK,CAAuB;IACpC,OAAO,CAAC,WAAW,CAAoB;IACvC,OAAO,CAAC,cAAc,CAAsB;IAC5C,OAAO,CAAC,gBAAgB,CAAkC;IAC1D;;;;OAIG;IACH,OAAO,CAAC,aAAa,CAAkC;IACvD,OAAO,CAAC,kBAAkB,CAAK;IAC/B,OAAO,CAAC,WAAW,CAAS;gBAGV,OAAO,EAAE,aAAa,EACtC,SAAS,EAAE,OAAO,CAAC,eAAe,CAAC,EAClB,SAAS,CAAC,EAAE,YAAY,YAAA;IAiC3C;;;;;;OAMG;IACG,WAAW,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC;IA4B9D;;OAEG;IACG,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IAO3B;;;OAGG;YACW,QAAQ;IA4BtB;;;;;;;;;OASG;IACG,cAAc,CAClB,WAAW,EAAE,MAAM,EACnB,MAAM,EAAE,GAAG,EACX,QAAQ,EAAE,MAAM,GAAG,IAAI,EACvB,OAAO,CAAC,EAAE;QAAE,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,GAC/B,OAAO,CAAC,GAAG,CAAC;IAoEf;;;;;;;;OAQG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,mBAAmB,EAC5B,QAAQ,EAAE,MAAM,GAAG,IAAI,GACtB,OAAO,CAAC,QAAQ,CAAC;IA4EpB;;OAEG;IACG,aAAa,CACjB,MAAM,EAAE,KAAK,CAAC;QACZ,OAAO,EAAE,MAAM,CAAC;QAChB,IAAI,EAAE,MAAM,CAAC;QACb,OAAO,EAAE,GAAG,CAAC;QACb,SAAS,EAAE,MAAM,CAAC;QAClB,WAAW,CAAC,EAAE,gBAAgB,GAAG,IAAI,CAAC;KACvC,CAAC,EACF,QAAQ,EAAE,MAAM,EAChB,KAAK,EAAE,YAAY,GAAG,IAAI,GACzB,OAAO,CAAC,aAAa,EAAE,CAAC;IA8C3B;;OAEG;IACG,cAAc,CAClB,eAAe,EAAE,MAAM,GAAG,IAAI,EAC9B,KAAK,EAAE,YAAY,GAAG,IAAI,GACzB,OAAO,CAAC,aAAa,EAAE,CAAC;IAkD3B;;OAEG;IACH,QAAQ,IAAI,KAAK,CAAC,aAAa,CAAC;IAIhC;;OAEG;IACH,cAAc,IAAI,iBAAiB;IAInC;;OAEG;IACH,mBAAmB,IAAI,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC;IAI/C;;OAEG;IACH,iBAAiB,IAAI,mBAAmB;CAGzC"}
@@ -32,6 +32,23 @@ export interface ArcServerConfig {
32
32
  * scripty bez CORP, jeśli request leci bez credentials.
33
33
  */
34
34
  coep?: "unsafe-none" | "credentialless" | "require-corp";
35
+ /**
36
+ * Allowlist of origins permitted for credentialed CORS. When set, only these
37
+ * origins are reflected into `Access-Control-Allow-Origin` (with
38
+ * `Allow-Credentials: true`). When unset, the server reflects the request
39
+ * Origin but WITHOUT `Allow-Credentials` (no credentialed cross-origin reads).
40
+ */
41
+ corsOrigins?: string[];
42
+ /** Max request body size in bytes (default 10 MB). Larger → 413. */
43
+ maxRequestBodySize?: number;
44
+ /** Extra security headers merged onto every response (override defaults). */
45
+ securityHeaders?: Record<string, string>;
46
+ /**
47
+ * Accept the auth token from the `?token=` query string. Default false —
48
+ * tokens in URLs leak to logs/Referer. Clients send tokens via the
49
+ * `Authorization` header or (for WS) the `scope:auth` message.
50
+ */
51
+ allowTokenInQuery?: boolean;
35
52
  /** Optional OpenTelemetry instance. When provided, the HTTP fetch handler
36
53
  * and WS message dispatch are wrapped in spans, with parent context
37
54
  * extracted from `traceparent` (header / message field). All nested
@@ -1 +1 @@
1
- {"version":3,"file":"create-server.d.ts","sourceRoot":"","sources":["../../src/create-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAQ,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAElC,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAMjD,OAAO,KAAK,EACV,cAAc,EAGd,YAAY,EACb,MAAM,oBAAoB,CAAC;AAG5B,KAAK,aAAa,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,aAAa,CAAC;IACvB,gBAAgB,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACzD,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,aAAa,GAAG,gBAAgB,GAAG,cAAc,CAAC;IACzD;;;kEAG8D;IAC9D,SAAS,CAAC,EAAE,YAAY,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;IAC9B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,SAAS,CAAC,CAmQpB"}
1
+ {"version":3,"file":"create-server.d.ts","sourceRoot":"","sources":["../../src/create-server.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AACvE,OAAO,KAAK,EAAE,YAAY,EAAQ,MAAM,uBAAuB,CAAC;AAChE,OAAO,KAAK,EAAE,MAAM,EAAE,MAAM,KAAK,CAAC;AAClC,OAAO,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AACzD,OAAO,EAAE,cAAc,EAAE,MAAM,mBAAmB,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,MAAM,kBAAkB,CAAC;AAMjD,OAAO,KAAK,EACV,cAAc,EAGd,YAAY,EACb,MAAM,oBAAoB,CAAC;AAG5B,KAAK,aAAa,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAA;CAAE,CAAC;AAE1C,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,aAAa,CAAC;IACvB,gBAAgB,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC,eAAe,CAAC,CAAC;IACzD,YAAY,CAAC,EAAE,cAAc,EAAE,CAAC;IAChC,UAAU,CAAC,EAAE,YAAY,EAAE,CAAC;IAC5B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,oFAAoF;IACpF,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC;;;;;;;;;;;;;OAaG;IACH,IAAI,CAAC,EAAE,aAAa,GAAG,gBAAgB,GAAG,cAAc,CAAC;IACzD;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,EAAE,CAAC;IACvB,oEAAoE;IACpE,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,6EAA6E;IAC7E,eAAe,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACzC;;;;OAIG;IACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B;;;kEAG8D;IAC9D,SAAS,CAAC,EAAE,YAAY,CAAC;CAC1B;AAED,MAAM,WAAW,SAAS;IACxB,MAAM,EAAE,MAAM,CAAC,aAAa,CAAC,CAAC;IAC9B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,aAAa,EAAE,aAAa,CAAC;IAC7B,IAAI,EAAE,MAAM,IAAI,CAAC;CAClB;AAED;;;;;;GAMG;AACH,wBAAsB,eAAe,CACnC,MAAM,EAAE,eAAe,GACtB,OAAO,CAAC,SAAS,CAAC,CAoRpB"}
@@ -1 +1 @@
1
- {"version":3,"file":"event-auth.d.ts","sourceRoot":"","sources":["../../src/event-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,GAAG,IAAI,EAC1B,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAqCT;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,YAAY,GAAG,IAAI,EAC1B,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,GAAG,GACX,OAAO,CA+BT;AAmBD;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,GAAG,IAAI,EAC1B,MAAM,EAAE,aAAa,EAAE,EACvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GACzC,aAAa,EAAE,CASjB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,YAAY,EAAE,EACtB,MAAM,EAAE,aAAa,EAAE,EACvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GACzC,aAAa,EAAE,CAejB"}
1
+ {"version":3,"file":"event-auth.d.ts","sourceRoot":"","sources":["../../src/event-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,KAAK,EAAE,aAAa,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAE3D;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,GAAG,IAAI,EAC1B,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,aAAa,GAC3B,OAAO,CAqCT;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,YAAY,GAAG,IAAI,EAC1B,KAAK,EAAE,WAAW,EAClB,OAAO,EAAE,GAAG,GACX,OAAO,CA+BT;AA0BD;;GAEG;AACH,wBAAgB,oBAAoB,CAClC,KAAK,EAAE,YAAY,GAAG,IAAI,EAC1B,MAAM,EAAE,aAAa,EAAE,EACvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GACzC,aAAa,EAAE,CASjB;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,YAAY,EAAE,EACtB,MAAM,EAAE,aAAa,EAAE,EACvB,gBAAgB,EAAE,GAAG,CAAC,MAAM,EAAE,WAAW,CAAC,GACzC,aAAa,EAAE,CAejB"}
@@ -1 +1 @@
1
- {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/middleware/http.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,SAAS,CAAC;AAoCjE,wBAAgB,aAAa,CAAC,EAAE,EAAE,iBAAiB,GAAG,cAAc,CAQnE;AAMD,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,GAAG,cAAc,CA+BjE;AAMD,wBAAgB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,cAAc,CAoC/D;AAMD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,cAAc,GAAG,cAAc,CA4BnE;AAMD,wBAAgB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,cAAc,CA8G/D;AAMD,wBAAgB,eAAe,CAC7B,EAAE,EAAE,cAAc,EAClB,EAAE,EAAE,iBAAiB,GACpB,cAAc,EAAE,CAQlB"}
1
+ {"version":3,"file":"http.d.ts","sourceRoot":"","sources":["../../../src/middleware/http.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,cAAc,EAAqB,MAAM,SAAS,CAAC;AAsDjE,wBAAgB,aAAa,CAAC,EAAE,EAAE,iBAAiB,GAAG,cAAc,CAQnE;AAMD,wBAAgB,cAAc,CAAC,EAAE,EAAE,cAAc,GAAG,cAAc,CA+BjE;AAMD,wBAAgB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,cAAc,CAmC/D;AAMD,wBAAgB,gBAAgB,CAAC,EAAE,EAAE,cAAc,GAAG,cAAc,CA8BnE;AAMD,wBAAgB,YAAY,CAAC,EAAE,EAAE,cAAc,GAAG,cAAc,CA6G/D;AAMD,wBAAgB,eAAe,CAC7B,EAAE,EAAE,cAAc,EAClB,EAAE,EAAE,iBAAiB,GACpB,cAAc,EAAE,CAQlB"}
@@ -21,7 +21,7 @@ export type ArcHttpHandler = (req: Request, url: URL, ctx: ArcRequestContext) =>
21
21
  export interface ArcWsContext {
22
22
  contextHandler: ContextHandler;
23
23
  connectionManager: ConnectionManager;
24
- verifyToken: (token: string) => TokenPayload | null;
24
+ verifyToken: (token: string) => Promise<TokenPayload | null>;
25
25
  }
26
26
  /**
27
27
  * WebSocket message handler.
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/middleware/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,iBAAiB,KACnB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,YAAY,GAAG,IAAI,CAAC;CACrD;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,GAAG,EACZ,GAAG,EAAE,YAAY,KACd,OAAO,CAAC,OAAO,CAAC,CAAC"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../src/middleware/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAC/D,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,oBAAoB,CAAC;AACzD,OAAO,KAAK,EAAE,eAAe,EAAE,YAAY,EAAE,MAAM,UAAU,CAAC;AAE9D;;;GAGG;AACH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,YAAY,EAAE,YAAY,GAAG,IAAI,CAAC;IAClC,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;CACrC;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,CAC3B,GAAG,EAAE,OAAO,EACZ,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,iBAAiB,KACnB,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,GAAG,QAAQ,GAAG,IAAI,CAAC;AAEhD;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B,cAAc,EAAE,cAAc,CAAC;IAC/B,iBAAiB,EAAE,iBAAiB,CAAC;IACrC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,YAAY,GAAG,IAAI,CAAC,CAAC;CAC9D;AAED;;;GAGG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,MAAM,EAAE,eAAe,EACvB,OAAO,EAAE,GAAG,EACZ,GAAG,EAAE,YAAY,KACd,OAAO,CAAC,OAAO,CAAC,CAAC"}
package/package.json CHANGED
@@ -4,11 +4,11 @@
4
4
  "main": "dist/index.js",
5
5
  "types": "dist/index.d.ts",
6
6
  "type": "module",
7
- "version": "0.7.25",
7
+ "version": "0.7.27",
8
8
  "private": false,
9
9
  "author": "Przemysław Krasiński [arcote.tech]",
10
10
  "dependencies": {
11
- "@arcote.tech/arc-adapter-db-sqlite": "^0.7.25",
11
+ "@arcote.tech/arc-adapter-db-sqlite": "^0.7.27",
12
12
  "croner": "^9.0.0",
13
13
  "jsonwebtoken": "^9.0.2"
14
14
  },
@@ -24,8 +24,8 @@
24
24
  "@types/bun": "^1.2.0"
25
25
  },
26
26
  "peerDependencies": {
27
- "@arcote.tech/arc": "^0.7.25",
28
- "@arcote.tech/arc-otel": "^0.7.25"
27
+ "@arcote.tech/arc": "^0.7.27",
28
+ "@arcote.tech/arc-otel": "^0.7.27"
29
29
  },
30
30
  "peerDependenciesMeta": {
31
31
  "@arcote.tech/arc-otel": {
@@ -1,17 +1,29 @@
1
1
  import {
2
+ ArcAuthorizationError,
2
3
  type ArcContextAny,
3
4
  type ArcEventAny,
5
+ type ArcRouteAny,
6
+ type ArcTokenAny,
4
7
  type DatabaseAdapter,
5
8
  LocalEventPublisher,
6
9
  MasterDataStorage,
7
10
  Model,
8
11
  ScopedModel,
12
+ collectTokens,
9
13
  mutationExecutor,
10
14
  } from "@arcote.tech/arc";
11
15
  import type { ArcTelemetry } from "@arcote.tech/arc-otel";
12
16
  import { canTokenEmitEvent, filterEventsForToken } from "./event-auth";
13
17
  import type { EventAuthContext, SyncableEvent, TokenPayload } from "./types";
14
18
 
19
+ /** Options for an in-process route invocation. */
20
+ export interface ExecuteRouteOptions {
21
+ method?: "GET" | "POST" | "PUT" | "DELETE" | "PATCH";
22
+ body?: unknown;
23
+ headers?: Record<string, string>;
24
+ query?: Record<string, string>;
25
+ }
26
+
15
27
  /**
16
28
  * Handles a single context on the host
17
29
  */
@@ -20,6 +32,12 @@ export class ContextHandler {
20
32
  private dataStorage: MasterDataStorage;
21
33
  private eventPublisher: LocalEventPublisher;
22
34
  private eventDefinitions = new Map<string, ArcEventAny>();
35
+ /**
36
+ * Per-token verification registry: tokenName → ArcToken (with its secret).
37
+ * Collected from every protectBy() in the context. Fail-closed: a token
38
+ * without a secret cannot verify any JWT (→ requests rejected).
39
+ */
40
+ private tokenRegistry = new Map<string, ArcTokenAny>();
23
41
  private hostEventIdCounter = 0;
24
42
  private initialized = false;
25
43
 
@@ -45,6 +63,53 @@ export class ContextHandler {
45
63
  this.eventDefinitions.set(element.name, element as ArcEventAny);
46
64
  }
47
65
  }
66
+
67
+ // Build the token registry once — the canonical, transport-agnostic source
68
+ // of JWT verification, shared by the HTTP server (createArcServer) and the
69
+ // in-process route dispatcher (executeRoute).
70
+ for (const tok of collectTokens(context)) {
71
+ this.tokenRegistry.set(tok.name, tok);
72
+ if (!tok.hasSecret) {
73
+ console.error(
74
+ `[ARC] SECURITY: token "${tok.name}" has no secret configured — its JWTs cannot be verified and will be rejected. Set .secret(process.env.…).`,
75
+ );
76
+ }
77
+ }
78
+ }
79
+
80
+ /**
81
+ * Verify a raw JWT cryptographically (HMAC-SHA256) against the secret of the
82
+ * token definition named in its payload, and check expiry. Returns the
83
+ * decoded payload or null. No unsigned fallback — forged/invalid/expired
84
+ * tokens are rejected. This is the single verification entry point for both
85
+ * the HTTP boundary and in-process dispatch.
86
+ */
87
+ async verifyToken(token: string): Promise<TokenPayload | null> {
88
+ let tokenName: string | undefined;
89
+ try {
90
+ const parts = token.split(".");
91
+ if (parts.length !== 3) return null;
92
+ const payload = JSON.parse(
93
+ atob(parts[1].replace(/-/g, "+").replace(/_/g, "/")),
94
+ );
95
+ tokenName = payload?.tokenName;
96
+ } catch {
97
+ return null;
98
+ }
99
+ if (!tokenName) return null;
100
+
101
+ const def = this.tokenRegistry.get(tokenName);
102
+ if (!def) return null;
103
+
104
+ const verified = await def.verifyJWT(token);
105
+ if (!verified) return null;
106
+
107
+ return {
108
+ tokenType: verified.tokenName,
109
+ params: verified.params || {},
110
+ iat: verified.iat,
111
+ exp: verified.exp,
112
+ };
48
113
  }
49
114
 
50
115
  /**
@@ -138,7 +203,7 @@ export class ContextHandler {
138
203
  // directly through `ctx.mutate.*` (bypassing this dispatcher) or pass
139
204
  // `{ internal: true }` (e.g. cron-scheduler).
140
205
  if (!options?.internal && (command as any).__isPrivate) {
141
- throw new Error(
206
+ throw new ArcAuthorizationError(
142
207
  `Command '${commandName}' is private and not callable from a client.`,
143
208
  );
144
209
  }
@@ -172,6 +237,95 @@ export class ContextHandler {
172
237
  }
173
238
  }
174
239
 
240
+ /**
241
+ * Execute a route handler in-process by NAME (no TCP). Mirrors the HTTP
242
+ * routeHandler boundary: real JWT verification, the same public/protected
243
+ * gate, scoped context, then the handler with a synthetic Request. Returns
244
+ * the handler's Response (or a 401/403/404/405 when the gate rejects).
245
+ *
246
+ * Used by webhooks/route tests and any in-process caller that needs the full
247
+ * route boundary without binding a port.
248
+ */
249
+ async executeRoute(
250
+ routeName: string,
251
+ options: ExecuteRouteOptions,
252
+ rawToken: string | null,
253
+ ): Promise<Response> {
254
+ const element = this.context.get(routeName) as ArcRouteAny | undefined;
255
+ if (!element || typeof (element as any).getHandler !== "function") {
256
+ return Response.json({ error: "Route not found" }, { status: 404 });
257
+ }
258
+
259
+ const method = options.method ?? "POST";
260
+ const handler = element.getHandler(method as any);
261
+ if (!handler) {
262
+ return Response.json(
263
+ { error: `Method ${method} not allowed` },
264
+ { status: 405 },
265
+ );
266
+ }
267
+
268
+ const tokenPayload = rawToken ? await this.verifyToken(rawToken) : null;
269
+ // A presented-but-invalid token is rejected (never downgraded to anonymous).
270
+ if (rawToken && !tokenPayload) {
271
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
272
+ }
273
+
274
+ // Protection gate — identical semantics to middleware/http.ts routeHandler.
275
+ if (!element.isPublic && element.hasProtections) {
276
+ if (!tokenPayload) {
277
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
278
+ }
279
+ let isAuthorized = false;
280
+ for (const protection of element.protections) {
281
+ if (protection.token.name === tokenPayload.tokenType) {
282
+ const mockTokenInstance = {
283
+ params: tokenPayload.params,
284
+ getTokenDefinition: () => protection.token,
285
+ };
286
+ if (await protection.check(mockTokenInstance as any)) {
287
+ isAuthorized = true;
288
+ break;
289
+ }
290
+ }
291
+ }
292
+ if (!isAuthorized) {
293
+ return Response.json({ error: "Forbidden" }, { status: 403 });
294
+ }
295
+ } else if (!element.isPublic && !element.hasProtections) {
296
+ if (!tokenPayload) {
297
+ return Response.json({ error: "Unauthorized" }, { status: 401 });
298
+ }
299
+ }
300
+
301
+ const scoped = new ScopedModel(this.model, "request");
302
+ if (rawToken) scoped.setToken(rawToken);
303
+
304
+ const authParams = tokenPayload
305
+ ? { params: tokenPayload.params, tokenName: tokenPayload.tokenType }
306
+ : undefined;
307
+ const routeContext = element.buildContext(scoped.getAdapters(), authParams);
308
+
309
+ const url = new URL(`http://harness.local${element.fullPath}`);
310
+ if (options.query) {
311
+ for (const [k, v] of Object.entries(options.query)) {
312
+ url.searchParams.set(k, v);
313
+ }
314
+ }
315
+ const headers = new Headers(options.headers ?? {});
316
+ if (!headers.has("Content-Type")) headers.set("Content-Type", "application/json");
317
+ const req = new Request(url, {
318
+ method,
319
+ headers,
320
+ body:
321
+ method === "GET" || options.body == null
322
+ ? undefined
323
+ : JSON.stringify(options.body),
324
+ });
325
+
326
+ return handler(routeContext, req, {}, url);
327
+ }
328
+
175
329
  /**
176
330
  * Persist events from a client and return with host IDs
177
331
  */
@@ -1,7 +1,6 @@
1
1
  import type { ArcContextAny, DatabaseAdapter } from "@arcote.tech/arc";
2
2
  import type { ArcTelemetry, Span } from "@arcote.tech/arc-otel";
3
3
  import type { Server } from "bun";
4
- import jwt from "jsonwebtoken";
5
4
  import { ConnectionManager } from "./connection-manager";
6
5
  import { ContextHandler } from "./context-handler";
7
6
  import { CronScheduler } from "./cron-scheduler";
@@ -44,6 +43,23 @@ export interface ArcServerConfig {
44
43
  * scripty bez CORP, jeśli request leci bez credentials.
45
44
  */
46
45
  coep?: "unsafe-none" | "credentialless" | "require-corp";
46
+ /**
47
+ * Allowlist of origins permitted for credentialed CORS. When set, only these
48
+ * origins are reflected into `Access-Control-Allow-Origin` (with
49
+ * `Allow-Credentials: true`). When unset, the server reflects the request
50
+ * Origin but WITHOUT `Allow-Credentials` (no credentialed cross-origin reads).
51
+ */
52
+ corsOrigins?: string[];
53
+ /** Max request body size in bytes (default 10 MB). Larger → 413. */
54
+ maxRequestBodySize?: number;
55
+ /** Extra security headers merged onto every response (override defaults). */
56
+ securityHeaders?: Record<string, string>;
57
+ /**
58
+ * Accept the auth token from the `?token=` query string. Default false —
59
+ * tokens in URLs leak to logs/Referer. Clients send tokens via the
60
+ * `Authorization` header or (for WS) the `scope:auth` message.
61
+ */
62
+ allowTokenInQuery?: boolean;
47
63
  /** Optional OpenTelemetry instance. When provided, the HTTP fetch handler
48
64
  * and WS message dispatch are wrapped in spans, with parent context
49
65
  * extracted from `traceparent` (header / message field). All nested
@@ -69,10 +85,6 @@ export interface ArcServer {
69
85
  export async function createArcServer(
70
86
  config: ArcServerConfig,
71
87
  ): Promise<ArcServer> {
72
- const jwtSecret =
73
- config.jwtSecret ||
74
- process.env.JWT_SECRET ||
75
- "arc-host-secret-change-in-production";
76
88
  const port = config.port || 5005;
77
89
 
78
90
  // Init context handler — telemetry (if any) flows through so executeCommand
@@ -103,50 +115,50 @@ export async function createArcServer(
103
115
  // Apps z SQLite WASM/OPFS świadomie przełączają na require-corp przez
104
116
  // `coep` w ArcServerConfig.
105
117
  const coep = config.coep ?? "unsafe-none";
106
- function buildCorsHeaders(req?: Request) {
107
- const origin = req?.headers.get("Origin") || "*";
108
- return {
109
- "Access-Control-Allow-Origin": origin,
118
+ const corsOrigins = config.corsOrigins;
119
+ const maxRequestBodySize = config.maxRequestBodySize ?? 10 * 1024 * 1024;
120
+ const securityHeaders: Record<string, string> = {
121
+ "X-Content-Type-Options": "nosniff",
122
+ "X-Frame-Options": "DENY",
123
+ "Referrer-Policy": "no-referrer",
124
+ ...(config.securityHeaders ?? {}),
125
+ };
126
+
127
+ function buildCorsHeaders(req?: Request): Record<string, string> {
128
+ const origin = req?.headers.get("Origin") || null;
129
+ const headers: Record<string, string> = {
110
130
  "Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, PATCH, OPTIONS",
111
131
  "Access-Control-Allow-Headers":
112
132
  "Content-Type, Authorization, X-Arc-Scope, X-Arc-Tokens",
113
- "Access-Control-Allow-Credentials": "true",
114
133
  // Cross-origin isolation — patrz `coep` w ArcServerConfig.
115
134
  "Cross-Origin-Opener-Policy": "same-origin",
116
135
  "Cross-Origin-Embedder-Policy": coep,
117
136
  "Cross-Origin-Resource-Policy": "cross-origin",
137
+ ...securityHeaders,
118
138
  };
119
- }
120
- const corsHeaders = buildCorsHeaders();
121
139
 
122
- function verifyToken(token: string): TokenPayload | null {
123
- try {
124
- const decoded = jwt.verify(token, jwtSecret) as any;
125
- if (decoded.tokenName && !decoded.tokenType) {
126
- return {
127
- tokenType: decoded.tokenName,
128
- params: decoded.params || {},
129
- iat: decoded.iat,
130
- exp: decoded.exp,
131
- };
132
- }
133
- return decoded as TokenPayload;
134
- } catch {
135
- try {
136
- const parts = token.split(".");
137
- if (parts.length !== 3) return null;
138
- const payload = JSON.parse(atob(parts[1]));
139
- return {
140
- tokenType: payload.tokenName,
141
- params: payload.params || {},
142
- iat: payload.iat,
143
- exp: payload.exp,
144
- };
145
- } catch {
146
- return null;
140
+ if (corsOrigins && corsOrigins.length > 0) {
141
+ // Allowlist mode: reflect only listed origins, with credentials.
142
+ if (origin && corsOrigins.includes(origin)) {
143
+ headers["Access-Control-Allow-Origin"] = origin;
144
+ headers["Access-Control-Allow-Credentials"] = "true";
145
+ headers["Vary"] = "Origin";
147
146
  }
147
+ // Origin not allowed → no ACAO header (browser blocks the read).
148
+ } else {
149
+ // No allowlist: reflect origin for convenience but WITHOUT credentials
150
+ // (never combine an arbitrary reflected origin with Allow-Credentials).
151
+ headers["Access-Control-Allow-Origin"] = origin ?? "*";
152
+ if (origin) headers["Vary"] = "Origin";
148
153
  }
154
+ return headers;
149
155
  }
156
+ const corsHeaders = buildCorsHeaders();
157
+
158
+ // JWT verification is owned by the ContextHandler (single, transport-agnostic
159
+ // source — also used by in-process executeRoute). The HTTP boundary delegates.
160
+ const verifyToken = (token: string): Promise<TokenPayload | null> =>
161
+ contextHandler.verifyToken(token);
150
162
 
151
163
  // Build handler chains — Arc defaults + user-provided extras
152
164
  const defaultHttp = arcHttpHandlers(contextHandler, connectionManager);
@@ -183,10 +195,22 @@ export async function createArcServer(
183
195
  // (via runWithExtractedContext + startSpan) so nested handlers'
184
196
  // spans attach to this one as parent.
185
197
  const handleRequest = async (span: Span | undefined): Promise<Response> => {
198
+ // Reject oversized requests before reading the body (DoS guard).
199
+ const contentLength = Number(req.headers.get("Content-Length") ?? 0);
200
+ if (contentLength > maxRequestBodySize) {
201
+ return Response.json(
202
+ { error: "Payload too large" },
203
+ { status: 413, headers: corsHeaders },
204
+ );
205
+ }
206
+
186
207
  const authHeader = req.headers.get("Authorization");
187
- let rawToken =
188
- authHeader?.replace("Bearer ", "") ||
189
- url.searchParams.get("token");
208
+ let rawToken: string | null = authHeader?.replace("Bearer ", "") || null;
209
+
210
+ // Token in query string is off by default (leaks to logs/Referer).
211
+ if (!rawToken && config.allowTokenInQuery) {
212
+ rawToken = url.searchParams.get("token");
213
+ }
190
214
 
191
215
  if (!rawToken) {
192
216
  const cookieHeader = req.headers.get("Cookie");
@@ -195,7 +219,16 @@ export async function createArcServer(
195
219
  if (match) rawToken = decodeURIComponent(match[1]);
196
220
  }
197
221
  }
198
- const tokenPayload = rawToken ? verifyToken(rawToken) : null;
222
+ const tokenPayload = rawToken ? await verifyToken(rawToken) : null;
223
+
224
+ // A presented-but-invalid token (forged / wrong secret / expired) is
225
+ // rejected globally — never silently downgraded to anonymous.
226
+ if (rawToken && !tokenPayload) {
227
+ return Response.json(
228
+ { error: "Unauthorized" },
229
+ { status: 401, headers: corsHeaders },
230
+ );
231
+ }
199
232
 
200
233
  if (
201
234
  url.pathname === "/ws" &&
package/src/event-auth.ts CHANGED
@@ -99,8 +99,15 @@ function checkConditionsMatch(
99
99
  data: Record<string, any>,
100
100
  ): boolean {
101
101
  for (const [key, expectedValue] of Object.entries(conditions)) {
102
- const actualValue = data[key];
102
+ // An undefined expected value is NOT a real constraint — it would match any
103
+ // missing field (undefined === undefined), acting as a wildcard. A token
104
+ // whose scoping field (e.g. projectId) is absent must NOT gain access, so
105
+ // deny instead of treating undefined as "anything".
106
+ if (expectedValue === undefined) {
107
+ return false;
108
+ }
103
109
 
110
+ const actualValue = data[key];
104
111
  if (actualValue !== expectedValue) {
105
112
  return false;
106
113
  }
@@ -1,9 +1,27 @@
1
1
  import type { ArcRouteAny } from "@arcote.tech/arc";
2
- import { ScopedModel } from "@arcote.tech/arc";
2
+ import { ArcValidationError, arcErrorStatus, ScopedModel } from "@arcote.tech/arc";
3
3
  import type { ConnectionManager } from "../connection-manager";
4
4
  import type { ContextHandler } from "../context-handler";
5
5
  import type { ArcHttpHandler, ArcRequestContext } from "./types";
6
6
 
7
+ // ---------------------------------------------------------------------------
8
+ // Error → HTTP mapping
9
+ //
10
+ // Typed Arc errors map to 400/401/403 with safe bodies. Anything else is an
11
+ // unexpected failure → 500 with a GENERIC message (never leak error.message
12
+ // or stack to the client).
13
+ // ---------------------------------------------------------------------------
14
+
15
+ function errorBody(error: unknown): { status: number; body: Record<string, any> } {
16
+ const status = arcErrorStatus(error) ?? 500;
17
+ if (error instanceof ArcValidationError) {
18
+ return { status, body: { error: "Invalid parameters", fields: error.fields } };
19
+ }
20
+ if (status === 401) return { status, body: { error: "Unauthorized" } };
21
+ if (status === 403) return { status, body: { error: "Forbidden" } };
22
+ return { status: 500, body: { error: "Internal server error" } };
23
+ }
24
+
7
25
  // ---------------------------------------------------------------------------
8
26
  // Shared
9
27
  // ---------------------------------------------------------------------------
@@ -76,11 +94,11 @@ export function commandHandler(ch: ContextHandler): ArcHttpHandler {
76
94
  headers: ctx.corsHeaders,
77
95
  });
78
96
  } catch (error) {
79
- console.error(`[ARC] Command '${commandName}' error:`, error);
80
- return Response.json(
81
- { error: (error as Error).message },
82
- { status: 500, headers: ctx.corsHeaders },
83
- );
97
+ const { status, body } = errorBody(error);
98
+ if (status === 500) {
99
+ console.error(`[ARC] Command '${commandName}' error:`, error);
100
+ }
101
+ return Response.json(body, { status, headers: ctx.corsHeaders });
84
102
  }
85
103
  };
86
104
  }
@@ -119,10 +137,9 @@ export function queryHandler(ch: ContextHandler): ArcHttpHandler {
119
137
 
120
138
  return Response.json(result, { headers: ctx.corsHeaders });
121
139
  } catch (error) {
122
- return Response.json(
123
- { error: (error as Error).message },
124
- { status: 500, headers: ctx.corsHeaders },
125
- );
140
+ const { status, body } = errorBody(error);
141
+ if (status === 500) console.error(`[ARC] Query error:`, error);
142
+ return Response.json(body, { status, headers: ctx.corsHeaders });
126
143
  }
127
144
  };
128
145
  }
@@ -153,9 +170,11 @@ export function eventSyncHandler(ch: ContextHandler): ArcHttpHandler {
153
170
  { headers: ctx.corsHeaders },
154
171
  );
155
172
  } catch (error) {
173
+ const { status, body } = errorBody(error);
174
+ if (status === 500) console.error(`[ARC] Event sync error:`, error);
156
175
  return Response.json(
157
- { success: false, error: (error as Error).message },
158
- { status: 500, headers: ctx.corsHeaders },
176
+ { success: false, ...body },
177
+ { status, headers: ctx.corsHeaders },
159
178
  );
160
179
  }
161
180
  };
@@ -269,10 +288,9 @@ export function routeHandler(ch: ContextHandler): ArcHttpHandler {
269
288
  headers: newHeaders,
270
289
  });
271
290
  } catch (error) {
272
- return Response.json(
273
- { error: (error as Error).message },
274
- { status: 500, headers: ctx.corsHeaders },
275
- );
291
+ const { status, body: errBody } = errorBody(error);
292
+ if (status === 500) console.error(`[ARC] Route error:`, error);
293
+ return Response.json(errBody, { status, headers: ctx.corsHeaders });
276
294
  }
277
295
  };
278
296
  }
@@ -28,7 +28,7 @@ export type ArcHttpHandler = (
28
28
  export interface ArcWsContext {
29
29
  contextHandler: ContextHandler;
30
30
  connectionManager: ConnectionManager;
31
- verifyToken: (token: string) => TokenPayload | null;
31
+ verifyToken: (token: string) => Promise<TokenPayload | null>;
32
32
  }
33
33
 
34
34
  /**
@@ -28,7 +28,7 @@ export function cleanupClientSubs(clientId: string): void {
28
28
  export function scopeAuthHandler(): ArcWsHandler {
29
29
  return async (client, message, ctx) => {
30
30
  if (message.type !== "scope:auth") return false;
31
- const decoded = ctx.verifyToken(message.token);
31
+ const decoded = await ctx.verifyToken(message.token);
32
32
  if (decoded) {
33
33
  ctx.connectionManager.setScopeToken(
34
34
  client.id,