@arcote.tech/arc-host 0.7.26 → 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.
- package/dist/index.js +166 -3776
- package/dist/index.js.map +8 -84
- package/dist/src/context-handler.d.ts +31 -0
- package/dist/src/context-handler.d.ts.map +1 -1
- package/dist/src/create-server.d.ts +17 -0
- package/dist/src/create-server.d.ts.map +1 -1
- package/dist/src/event-auth.d.ts.map +1 -1
- package/dist/src/middleware/http.d.ts.map +1 -1
- package/dist/src/middleware/types.d.ts +1 -1
- package/dist/src/middleware/types.d.ts.map +1 -1
- package/package.json +4 -4
- package/src/context-handler.ts +155 -1
- package/src/create-server.ts +74 -41
- package/src/event-auth.ts +8 -1
- package/src/middleware/http.ts +34 -16
- package/src/middleware/types.ts +1 -1
- package/src/middleware/ws.ts +1 -1
|
@@ -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,
|
|
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;
|
|
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;
|
|
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;
|
|
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;
|
|
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.
|
|
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.
|
|
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.
|
|
28
|
-
"@arcote.tech/arc-otel": "^0.7.
|
|
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": {
|
package/src/context-handler.ts
CHANGED
|
@@ -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
|
|
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
|
*/
|
package/src/create-server.ts
CHANGED
|
@@ -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
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
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
|
-
|
|
189
|
-
|
|
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
|
-
|
|
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
|
}
|
package/src/middleware/http.ts
CHANGED
|
@@ -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
|
-
|
|
80
|
-
|
|
81
|
-
{ error
|
|
82
|
-
|
|
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
|
-
|
|
123
|
-
|
|
124
|
-
|
|
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,
|
|
158
|
-
{ status
|
|
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
|
-
|
|
273
|
-
|
|
274
|
-
|
|
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
|
}
|
package/src/middleware/types.ts
CHANGED
|
@@ -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
|
/**
|
package/src/middleware/ws.ts
CHANGED
|
@@ -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,
|