crewly 1.20.406 → 1.20.408
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/backend/backend/src/constants.d.ts +34 -0
- package/dist/backend/backend/src/constants.d.ts.map +1 -1
- package/dist/backend/backend/src/constants.js +34 -0
- package/dist/backend/backend/src/constants.js.map +1 -1
- package/dist/backend/backend/src/controllers/activity/activity.controller.d.ts +29 -0
- package/dist/backend/backend/src/controllers/activity/activity.controller.d.ts.map +1 -0
- package/dist/backend/backend/src/controllers/activity/activity.controller.js +77 -0
- package/dist/backend/backend/src/controllers/activity/activity.controller.js.map +1 -0
- package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
- package/dist/backend/backend/src/controllers/team/team.controller.js +49 -0
- package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
- package/dist/backend/backend/src/routes/modules/system.routes.d.ts.map +1 -1
- package/dist/backend/backend/src/routes/modules/system.routes.js +3 -0
- package/dist/backend/backend/src/routes/modules/system.routes.js.map +1 -1
- package/dist/backend/backend/src/services/activity/activity-log.service.d.ts +192 -0
- package/dist/backend/backend/src/services/activity/activity-log.service.d.ts.map +1 -0
- package/dist/backend/backend/src/services/activity/activity-log.service.js +343 -0
- package/dist/backend/backend/src/services/activity/activity-log.service.js.map +1 -0
- package/dist/backend/backend/src/services/agent/agent-heartbeat-monitor.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/agent/agent-heartbeat-monitor.service.js +7 -0
- package/dist/backend/backend/src/services/agent/agent-heartbeat-monitor.service.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts +13 -0
- package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.service.js +39 -0
- package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts.map +1 -1
- package/dist/backend/backend/src/services/decisions/decision.wiring.js +2 -0
- package/dist/backend/backend/src/services/decisions/decision.wiring.js.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts +8 -0
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js +24 -5
- package/dist/backend/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/dist/backend/build-info.json +2 -2
- package/dist/cli/backend/src/constants.d.ts +34 -0
- package/dist/cli/backend/src/constants.d.ts.map +1 -1
- package/dist/cli/backend/src/constants.js +34 -0
- package/dist/cli/backend/src/constants.js.map +1 -1
- package/dist/cli/backend/src/middleware/agent-origin-correction.d.ts +31 -0
- package/dist/cli/backend/src/middleware/agent-origin-correction.d.ts.map +1 -0
- package/dist/cli/backend/src/middleware/agent-origin-correction.js +31 -0
- package/dist/cli/backend/src/middleware/agent-origin-correction.js.map +1 -0
- package/dist/cli/backend/src/middleware/api-token.middleware.d.ts +171 -0
- package/dist/cli/backend/src/middleware/api-token.middleware.d.ts.map +1 -0
- package/dist/cli/backend/src/middleware/api-token.middleware.js +329 -0
- package/dist/cli/backend/src/middleware/api-token.middleware.js.map +1 -0
- package/dist/cli/backend/src/middleware/caller-identity.middleware.d.ts +290 -0
- package/dist/cli/backend/src/middleware/caller-identity.middleware.d.ts.map +1 -0
- package/dist/cli/backend/src/middleware/caller-identity.middleware.js +658 -0
- package/dist/cli/backend/src/middleware/caller-identity.middleware.js.map +1 -0
- package/dist/cli/backend/src/middleware/require-auth.middleware.d.ts +60 -0
- package/dist/cli/backend/src/middleware/require-auth.middleware.d.ts.map +1 -0
- package/dist/cli/backend/src/middleware/require-auth.middleware.js +127 -0
- package/dist/cli/backend/src/middleware/require-auth.middleware.js.map +1 -0
- package/dist/cli/backend/src/services/activity/activity-log.service.d.ts +192 -0
- package/dist/cli/backend/src/services/activity/activity-log.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/activity/activity-log.service.js +343 -0
- package/dist/cli/backend/src/services/activity/activity-log.service.js.map +1 -0
- package/dist/cli/backend/src/services/core/peer-process.service.d.ts +168 -0
- package/dist/cli/backend/src/services/core/peer-process.service.d.ts.map +1 -0
- package/dist/cli/backend/src/services/core/peer-process.service.js +352 -0
- package/dist/cli/backend/src/services/core/peer-process.service.js.map +1 -0
- package/dist/cli/backend/src/services/decisions/decision.service.d.ts +13 -0
- package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/decisions/decision.service.js +39 -0
- package/dist/cli/backend/src/services/decisions/decision.service.js.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts +8 -0
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.d.ts.map +1 -1
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js +24 -5
- package/dist/cli/backend/src/services/slack/slack-team-channel.service.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-tree corrections of a request's agent identity, kept out of band.
|
|
3
|
+
*
|
|
4
|
+
* The agent-origin middleware proves from `X-Agent-Pid` which agent PTY a
|
|
5
|
+
* skill shell really runs under. When that contradicts the identity the
|
|
6
|
+
* shell claimed, it records the correction here. The caller-identity
|
|
7
|
+
* middleware (#999) reads it to override a leaked agent badge. A header
|
|
8
|
+
* would not do: a client could send it itself.
|
|
9
|
+
*
|
|
10
|
+
* @module middleware/agent-origin-correction
|
|
11
|
+
*/
|
|
12
|
+
/** The shell claimed `claimed`; its process runs under `actual`. */
|
|
13
|
+
export interface AgentOriginCorrection {
|
|
14
|
+
claimed: string;
|
|
15
|
+
actual: string;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Record a correction for a request.
|
|
19
|
+
*
|
|
20
|
+
* @param req - Request object
|
|
21
|
+
* @param correction - Claimed and actual sessions
|
|
22
|
+
*/
|
|
23
|
+
export declare function setAgentOriginCorrection(req: object, correction: AgentOriginCorrection): void;
|
|
24
|
+
/**
|
|
25
|
+
* The correction recorded for a request, if any.
|
|
26
|
+
*
|
|
27
|
+
* @param req - Request object
|
|
28
|
+
* @returns The correction or undefined
|
|
29
|
+
*/
|
|
30
|
+
export declare function getAgentOriginCorrection(req: object): AgentOriginCorrection | undefined;
|
|
31
|
+
//# sourceMappingURL=agent-origin-correction.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-origin-correction.d.ts","sourceRoot":"","sources":["../../../../../backend/src/middleware/agent-origin-correction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAEH,oEAAoE;AACpE,MAAM,WAAW,qBAAqB;IACrC,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CACf;AAID;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,EAAE,UAAU,EAAE,qBAAqB,GAAG,IAAI,CAE7F;AAED;;;;;GAKG;AACH,wBAAgB,wBAAwB,CAAC,GAAG,EAAE,MAAM,GAAG,qBAAqB,GAAG,SAAS,CAEvF"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Process-tree corrections of a request's agent identity, kept out of band.
|
|
3
|
+
*
|
|
4
|
+
* The agent-origin middleware proves from `X-Agent-Pid` which agent PTY a
|
|
5
|
+
* skill shell really runs under. When that contradicts the identity the
|
|
6
|
+
* shell claimed, it records the correction here. The caller-identity
|
|
7
|
+
* middleware (#999) reads it to override a leaked agent badge. A header
|
|
8
|
+
* would not do: a client could send it itself.
|
|
9
|
+
*
|
|
10
|
+
* @module middleware/agent-origin-correction
|
|
11
|
+
*/
|
|
12
|
+
const corrections = new WeakMap();
|
|
13
|
+
/**
|
|
14
|
+
* Record a correction for a request.
|
|
15
|
+
*
|
|
16
|
+
* @param req - Request object
|
|
17
|
+
* @param correction - Claimed and actual sessions
|
|
18
|
+
*/
|
|
19
|
+
export function setAgentOriginCorrection(req, correction) {
|
|
20
|
+
corrections.set(req, correction);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* The correction recorded for a request, if any.
|
|
24
|
+
*
|
|
25
|
+
* @param req - Request object
|
|
26
|
+
* @returns The correction or undefined
|
|
27
|
+
*/
|
|
28
|
+
export function getAgentOriginCorrection(req) {
|
|
29
|
+
return corrections.get(req);
|
|
30
|
+
}
|
|
31
|
+
//# sourceMappingURL=agent-origin-correction.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"agent-origin-correction.js","sourceRoot":"","sources":["../../../../../backend/src/middleware/agent-origin-correction.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAQH,MAAM,WAAW,GAAG,IAAI,OAAO,EAAiC,CAAC;AAEjE;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAW,EAAE,UAAiC;IACtF,WAAW,CAAC,GAAG,CAAC,GAAG,EAAE,UAAU,CAAC,CAAC;AAClC,CAAC;AAED;;;;;GAKG;AACH,MAAM,UAAU,wBAAwB,CAAC,GAAW;IACnD,OAAO,WAAW,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC7B,CAAC"}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API token middleware.
|
|
3
|
+
*
|
|
4
|
+
* Gates `/api/*`, Socket.IO and raw WebSocket upgrades behind the shared
|
|
5
|
+
* API token for every caller that is NOT on loopback. Loopback callers
|
|
6
|
+
* (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) pass without a token — that is
|
|
7
|
+
* how local skills (`api_call` in `config/skills/_common/lib.sh`) and the
|
|
8
|
+
* local dashboard work with zero setup.
|
|
9
|
+
*
|
|
10
|
+
* The token can be presented as `Authorization: Bearer <token>`,
|
|
11
|
+
* `X-Crewly-Token: <token>`, a `crewly_token` cookie, or (WebSocket /
|
|
12
|
+
* dashboard deep link only) a `?token=` query parameter.
|
|
13
|
+
*
|
|
14
|
+
* `X-Forwarded-For` is honoured ONLY when `CREWLY_TRUST_PROXY=1`, otherwise
|
|
15
|
+
* any LAN caller could spoof a loopback origin.
|
|
16
|
+
*
|
|
17
|
+
* The existing `require-auth.middleware.ts` (HS256 JWT for chat-v2) is
|
|
18
|
+
* untouched; this module only re-uses its verifier so that `/ws/chat`
|
|
19
|
+
* clients holding a valid JWT keep working when `CREWLY_JWT_SECRET` is set.
|
|
20
|
+
*
|
|
21
|
+
* @module middleware/api-token
|
|
22
|
+
*/
|
|
23
|
+
import type { Request, Response, NextFunction } from 'express';
|
|
24
|
+
import type { IncomingMessage, Server as HttpServer } from 'http';
|
|
25
|
+
/** Minimal request shape shared by Express requests and raw upgrade requests. */
|
|
26
|
+
export interface AddressableRequest {
|
|
27
|
+
headers: IncomingMessage['headers'];
|
|
28
|
+
socket?: {
|
|
29
|
+
remoteAddress?: string;
|
|
30
|
+
} | null;
|
|
31
|
+
url?: string;
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether `X-Forwarded-For` should be trusted for loopback classification.
|
|
35
|
+
*
|
|
36
|
+
* @returns True when `CREWLY_TRUST_PROXY` is `1` or `true`
|
|
37
|
+
*/
|
|
38
|
+
export declare function isProxyTrusted(): boolean;
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the address a request originates from.
|
|
41
|
+
*
|
|
42
|
+
* @param req - Express request or raw upgrade request
|
|
43
|
+
* @returns Remote address string, or empty string when unknown
|
|
44
|
+
*/
|
|
45
|
+
export declare function getClientAddress(req: AddressableRequest): string;
|
|
46
|
+
/**
|
|
47
|
+
* Whether an address is one of the loopback forms Node reports.
|
|
48
|
+
*
|
|
49
|
+
* @param address - Remote address
|
|
50
|
+
* @returns True for `127.0.0.1`, `::1` and `::ffff:127.0.0.1`
|
|
51
|
+
*/
|
|
52
|
+
export declare function isLoopbackAddress(address: string): boolean;
|
|
53
|
+
/**
|
|
54
|
+
* Whether a request originates from loopback (honouring the proxy rule).
|
|
55
|
+
*
|
|
56
|
+
* @param req - Express request or raw upgrade request
|
|
57
|
+
* @returns True when the caller is local
|
|
58
|
+
*/
|
|
59
|
+
export declare function isLoopbackRequest(req: AddressableRequest): boolean;
|
|
60
|
+
/**
|
|
61
|
+
* Extract the token a caller presented, if any.
|
|
62
|
+
*
|
|
63
|
+
* Order: `Authorization: Bearer`, `X-Crewly-Token`, `crewly_token` cookie,
|
|
64
|
+
* then (when `allowQuery`) the `?token=` query parameter.
|
|
65
|
+
*
|
|
66
|
+
* @param req - Request carrying headers and url
|
|
67
|
+
* @param allowQuery - Whether to consult the query string (WebSocket handshakes)
|
|
68
|
+
* @returns Presented token or null
|
|
69
|
+
*/
|
|
70
|
+
export declare function extractPresentedToken(req: AddressableRequest, allowQuery: boolean): string | null;
|
|
71
|
+
/**
|
|
72
|
+
* The API token a caller presented in a header (`Authorization: Bearer` or
|
|
73
|
+
* `X-Crewly-Token`) or, failing that, in the `crewly_token` cookie, and which
|
|
74
|
+
* of the two it was. A cookie is sent by the browser on its own, so a
|
|
75
|
+
* cookie-borne token alone does not authorise a write (#999).
|
|
76
|
+
*
|
|
77
|
+
* @param req - Request carrying headers
|
|
78
|
+
* @returns Token and source, or null when none was presented
|
|
79
|
+
*/
|
|
80
|
+
export declare function extractPresentedTokenWithSource(req: AddressableRequest): {
|
|
81
|
+
token: string;
|
|
82
|
+
source: 'header' | 'cookie';
|
|
83
|
+
} | null;
|
|
84
|
+
/**
|
|
85
|
+
* Parse a `Cookie` header (exported for the caller-identity middleware).
|
|
86
|
+
*
|
|
87
|
+
* @param header - Raw cookie header
|
|
88
|
+
* @returns Parsed cookies
|
|
89
|
+
*/
|
|
90
|
+
export declare function parseCookieHeader(header: string | undefined): Record<string, string>;
|
|
91
|
+
/**
|
|
92
|
+
* Whether a request presented a valid API token.
|
|
93
|
+
*
|
|
94
|
+
* @param req - Request carrying headers and url
|
|
95
|
+
* @param allowQuery - Whether to consult the query string
|
|
96
|
+
* @returns True when the presented token matches
|
|
97
|
+
*/
|
|
98
|
+
export declare function hasValidApiToken(req: AddressableRequest, allowQuery: boolean): boolean;
|
|
99
|
+
/**
|
|
100
|
+
* Express middleware: loopback passes, everyone else needs the API token.
|
|
101
|
+
*
|
|
102
|
+
* Mounted in front of `/api` in `backend/src/index.ts`. `/health`, static
|
|
103
|
+
* assets and the SPA shell are not under `/api` and stay open.
|
|
104
|
+
*
|
|
105
|
+
* @param req - Express request
|
|
106
|
+
* @param res - Express response
|
|
107
|
+
* @param next - Express next
|
|
108
|
+
*/
|
|
109
|
+
export declare function apiTokenMiddleware(req: Request, res: Response, next: NextFunction): void;
|
|
110
|
+
/**
|
|
111
|
+
* Whether `GET /health` is configured open to everyone (`CREWLY_PUBLIC_HEALTH`).
|
|
112
|
+
*
|
|
113
|
+
* @returns True when the env var is `1` or `true`
|
|
114
|
+
*/
|
|
115
|
+
export declare function isPublicHealthEnabled(): boolean;
|
|
116
|
+
/**
|
|
117
|
+
* Gate for the root `GET /health` (#825).
|
|
118
|
+
*
|
|
119
|
+
* - Loopback: passes straight to the handler, untouched (CLI, Docker
|
|
120
|
+
* healthchecks, desktop, local dashboard).
|
|
121
|
+
* - Non-loopback with a valid API token (header, Bearer or cookie): passes.
|
|
122
|
+
* - Non-loopback without one: the standard 401 token challenge, same as
|
|
123
|
+
* `/api`, and nothing about the install is disclosed.
|
|
124
|
+
* - `CREWLY_PUBLIC_HEALTH=1`: everyone passes (public monitors behind a
|
|
125
|
+
* reverse proxy).
|
|
126
|
+
*
|
|
127
|
+
* Why: crewly-mobile probes `/health` to choose its LAN transport. While
|
|
128
|
+
* `/health` answered 200 to anyone, a phone without the token chose LAN and
|
|
129
|
+
* then got 401 on every `/api` call, never falling back to the relay. A 401
|
|
130
|
+
* here makes that probe fail, so the app uses the relay, which works.
|
|
131
|
+
* "Loopback" follows {@link getClientAddress}: the socket address, or the
|
|
132
|
+
* first `X-Forwarded-For` hop only when `CREWLY_TRUST_PROXY` is set.
|
|
133
|
+
*
|
|
134
|
+
* @param req - Express request
|
|
135
|
+
* @param res - Express response
|
|
136
|
+
* @param next - Express next
|
|
137
|
+
*/
|
|
138
|
+
export declare function healthGateMiddleware(req: Request, res: Response, next: NextFunction): void;
|
|
139
|
+
/**
|
|
140
|
+
* Decide whether a raw HTTP request (WebSocket upgrade or Engine.IO
|
|
141
|
+
* handshake) may proceed.
|
|
142
|
+
*
|
|
143
|
+
* Loopback passes. Otherwise the API token must be present (query `token`
|
|
144
|
+
* accepted here because browsers cannot set headers on WebSocket
|
|
145
|
+
* handshakes). `/ws/chat` additionally accepts a valid chat JWT when
|
|
146
|
+
* `CREWLY_JWT_SECRET` is configured, since that gateway has its own auth.
|
|
147
|
+
*
|
|
148
|
+
* @param req - Incoming HTTP request
|
|
149
|
+
* @returns True when the connection is allowed
|
|
150
|
+
*/
|
|
151
|
+
export declare function isUpgradeAllowed(req: IncomingMessage): boolean;
|
|
152
|
+
/**
|
|
153
|
+
* Engine.IO `allowRequest` hook for the Socket.IO server. Covers both the
|
|
154
|
+
* polling handshake (which bypasses Express) and the WebSocket transport.
|
|
155
|
+
*
|
|
156
|
+
* @param req - Handshake request
|
|
157
|
+
* @param callback - Engine.IO callback `(err, success)`
|
|
158
|
+
*/
|
|
159
|
+
export declare function socketIoAllowRequest(req: IncomingMessage, callback: (err: string | null | undefined, success: boolean) => void): void;
|
|
160
|
+
/**
|
|
161
|
+
* Install the WebSocket upgrade gate on an HTTP server.
|
|
162
|
+
*
|
|
163
|
+
* Wraps `httpServer.emit` (the same exclusive-handoff pattern the browser
|
|
164
|
+
* bridge and chat gateway use) so an unauthorised `upgrade` is answered
|
|
165
|
+
* with `401` and closed before ANY gateway sees it. Must be installed
|
|
166
|
+
* AFTER every gateway has attached so this wrapper is the outermost one.
|
|
167
|
+
*
|
|
168
|
+
* @param httpServer - Server whose upgrades to gate
|
|
169
|
+
*/
|
|
170
|
+
export declare function installWebSocketGate(httpServer: HttpServer): void;
|
|
171
|
+
//# sourceMappingURL=api-token.middleware.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-token.middleware.d.ts","sourceRoot":"","sources":["../../../../../backend/src/middleware/api-token.middleware.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,QAAQ,EAAE,YAAY,EAAE,MAAM,SAAS,CAAC;AAC/D,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,IAAI,UAAU,EAAE,MAAM,MAAM,CAAC;AAYlE,iFAAiF;AACjF,MAAM,WAAW,kBAAkB;IACjC,OAAO,EAAE,eAAe,CAAC,SAAS,CAAC,CAAC;IACpC,MAAM,CAAC,EAAE;QAAE,aAAa,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,IAAI,CAAC;IAC3C,GAAG,CAAC,EAAE,MAAM,CAAC;CACd;AAED;;;;GAIG;AACH,wBAAgB,cAAc,IAAI,OAAO,CAGxC;AAED;;;;;GAKG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,kBAAkB,GAAG,MAAM,CAShE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAE1D;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,GAAG,EAAE,kBAAkB,GAAG,OAAO,CAElE;AAyBD;;;;;;;;;GASG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,kBAAkB,EAAE,UAAU,EAAE,OAAO,GAAG,MAAM,GAAG,IAAI,CA0BjG;AAED;;;;;;;;GAQG;AACH,wBAAgB,+BAA+B,CAAC,GAAG,EAAE,kBAAkB,GAAG;IAAE,KAAK,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,QAAQ,GAAG,QAAQ,CAAA;CAAE,GAAG,IAAI,CAM9H;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAEpF;AAED;;;;;;GAMG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,kBAAkB,EAAE,UAAU,EAAE,OAAO,GAAG,OAAO,CAEtF;AAgBD;;;;;;;;;GASG;AACH,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAexF;AAED;;;;GAIG;AACH,wBAAgB,qBAAqB,IAAI,OAAO,CAG/C;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,OAAO,EAAE,GAAG,EAAE,QAAQ,EAAE,IAAI,EAAE,YAAY,GAAG,IAAI,CAM1F;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAkB9D;AAED;;;;;;GAMG;AACH,wBAAgB,oBAAoB,CAClC,GAAG,EAAE,eAAe,EACpB,QAAQ,EAAE,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAAE,OAAO,EAAE,OAAO,KAAK,IAAI,GACnE,IAAI,CASN;AAED;;;;;;;;;GASG;AACH,wBAAgB,oBAAoB,CAAC,UAAU,EAAE,UAAU,GAAG,IAAI,CAuBjE"}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API token middleware.
|
|
3
|
+
*
|
|
4
|
+
* Gates `/api/*`, Socket.IO and raw WebSocket upgrades behind the shared
|
|
5
|
+
* API token for every caller that is NOT on loopback. Loopback callers
|
|
6
|
+
* (`127.0.0.1`, `::1`, `::ffff:127.0.0.1`) pass without a token — that is
|
|
7
|
+
* how local skills (`api_call` in `config/skills/_common/lib.sh`) and the
|
|
8
|
+
* local dashboard work with zero setup.
|
|
9
|
+
*
|
|
10
|
+
* The token can be presented as `Authorization: Bearer <token>`,
|
|
11
|
+
* `X-Crewly-Token: <token>`, a `crewly_token` cookie, or (WebSocket /
|
|
12
|
+
* dashboard deep link only) a `?token=` query parameter.
|
|
13
|
+
*
|
|
14
|
+
* `X-Forwarded-For` is honoured ONLY when `CREWLY_TRUST_PROXY=1`, otherwise
|
|
15
|
+
* any LAN caller could spoof a loopback origin.
|
|
16
|
+
*
|
|
17
|
+
* The existing `require-auth.middleware.ts` (HS256 JWT for chat-v2) is
|
|
18
|
+
* untouched; this module only re-uses its verifier so that `/ws/chat`
|
|
19
|
+
* clients holding a valid JWT keep working when `CREWLY_JWT_SECRET` is set.
|
|
20
|
+
*
|
|
21
|
+
* @module middleware/api-token
|
|
22
|
+
*/
|
|
23
|
+
import { API_SECURITY_CONSTANTS } from '../../../config/constants.js';
|
|
24
|
+
import { verifyApiToken } from '../services/core/api-token.service.js';
|
|
25
|
+
import { verifyHs256Token } from './require-auth.middleware.js';
|
|
26
|
+
import { LoggerService } from '../services/core/logger.service.js';
|
|
27
|
+
const logger = LoggerService.getInstance().createComponentLogger('ApiTokenMiddleware');
|
|
28
|
+
/** Path prefix of the chat-v2 WebSocket gateway (has its own JWT verifier). */
|
|
29
|
+
const CHAT_WS_PATH = '/ws/chat';
|
|
30
|
+
/**
|
|
31
|
+
* Whether `X-Forwarded-For` should be trusted for loopback classification.
|
|
32
|
+
*
|
|
33
|
+
* @returns True when `CREWLY_TRUST_PROXY` is `1` or `true`
|
|
34
|
+
*/
|
|
35
|
+
export function isProxyTrusted() {
|
|
36
|
+
const raw = process.env[API_SECURITY_CONSTANTS.ENV.TRUST_PROXY];
|
|
37
|
+
return raw === '1' || raw === 'true';
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve the address a request originates from.
|
|
41
|
+
*
|
|
42
|
+
* @param req - Express request or raw upgrade request
|
|
43
|
+
* @returns Remote address string, or empty string when unknown
|
|
44
|
+
*/
|
|
45
|
+
export function getClientAddress(req) {
|
|
46
|
+
if (isProxyTrusted()) {
|
|
47
|
+
const forwarded = req.headers['x-forwarded-for'];
|
|
48
|
+
const first = (Array.isArray(forwarded) ? forwarded[0] : forwarded)?.split(',')[0]?.trim();
|
|
49
|
+
if (first) {
|
|
50
|
+
return first;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return req.socket?.remoteAddress ?? '';
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Whether an address is one of the loopback forms Node reports.
|
|
57
|
+
*
|
|
58
|
+
* @param address - Remote address
|
|
59
|
+
* @returns True for `127.0.0.1`, `::1` and `::ffff:127.0.0.1`
|
|
60
|
+
*/
|
|
61
|
+
export function isLoopbackAddress(address) {
|
|
62
|
+
return API_SECURITY_CONSTANTS.LOOPBACK_ADDRESSES.includes(address);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* Whether a request originates from loopback (honouring the proxy rule).
|
|
66
|
+
*
|
|
67
|
+
* @param req - Express request or raw upgrade request
|
|
68
|
+
* @returns True when the caller is local
|
|
69
|
+
*/
|
|
70
|
+
export function isLoopbackRequest(req) {
|
|
71
|
+
return isLoopbackAddress(getClientAddress(req));
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Parse a `Cookie` header into a name → value map.
|
|
75
|
+
*
|
|
76
|
+
* @param header - Raw cookie header
|
|
77
|
+
* @returns Parsed cookies (URL-decoded values)
|
|
78
|
+
*/
|
|
79
|
+
function parseCookies(header) {
|
|
80
|
+
const out = {};
|
|
81
|
+
if (!header)
|
|
82
|
+
return out;
|
|
83
|
+
for (const part of header.split(';')) {
|
|
84
|
+
const eq = part.indexOf('=');
|
|
85
|
+
if (eq <= 0)
|
|
86
|
+
continue;
|
|
87
|
+
const name = part.slice(0, eq).trim();
|
|
88
|
+
const value = part.slice(eq + 1).trim();
|
|
89
|
+
try {
|
|
90
|
+
out[name] = decodeURIComponent(value);
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
out[name] = value;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return out;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Extract the token a caller presented, if any.
|
|
100
|
+
*
|
|
101
|
+
* Order: `Authorization: Bearer`, `X-Crewly-Token`, `crewly_token` cookie,
|
|
102
|
+
* then (when `allowQuery`) the `?token=` query parameter.
|
|
103
|
+
*
|
|
104
|
+
* @param req - Request carrying headers and url
|
|
105
|
+
* @param allowQuery - Whether to consult the query string (WebSocket handshakes)
|
|
106
|
+
* @returns Presented token or null
|
|
107
|
+
*/
|
|
108
|
+
export function extractPresentedToken(req, allowQuery) {
|
|
109
|
+
const auth = req.headers.authorization;
|
|
110
|
+
if (typeof auth === 'string' && auth.startsWith('Bearer ')) {
|
|
111
|
+
const value = auth.slice('Bearer '.length).trim();
|
|
112
|
+
if (value)
|
|
113
|
+
return value;
|
|
114
|
+
}
|
|
115
|
+
const headerToken = req.headers[API_SECURITY_CONSTANTS.TOKEN_HEADER];
|
|
116
|
+
const headerValue = Array.isArray(headerToken) ? headerToken[0] : headerToken;
|
|
117
|
+
if (headerValue)
|
|
118
|
+
return headerValue.trim();
|
|
119
|
+
const cookies = parseCookies(req.headers.cookie);
|
|
120
|
+
const cookieValue = cookies[API_SECURITY_CONSTANTS.TOKEN_COOKIE];
|
|
121
|
+
if (cookieValue)
|
|
122
|
+
return cookieValue;
|
|
123
|
+
if (allowQuery && req.url) {
|
|
124
|
+
try {
|
|
125
|
+
const host = req.headers.host || 'localhost';
|
|
126
|
+
const queryValue = new URL(req.url, `http://${host}`).searchParams.get(API_SECURITY_CONSTANTS.TOKEN_QUERY_PARAM);
|
|
127
|
+
if (queryValue)
|
|
128
|
+
return queryValue;
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
// Malformed URL — treat as no token
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
/**
|
|
137
|
+
* The API token a caller presented in a header (`Authorization: Bearer` or
|
|
138
|
+
* `X-Crewly-Token`) or, failing that, in the `crewly_token` cookie, and which
|
|
139
|
+
* of the two it was. A cookie is sent by the browser on its own, so a
|
|
140
|
+
* cookie-borne token alone does not authorise a write (#999).
|
|
141
|
+
*
|
|
142
|
+
* @param req - Request carrying headers
|
|
143
|
+
* @returns Token and source, or null when none was presented
|
|
144
|
+
*/
|
|
145
|
+
export function extractPresentedTokenWithSource(req) {
|
|
146
|
+
const headers = req.headers ?? {};
|
|
147
|
+
const fromHeaders = extractPresentedToken({ headers: { ...headers, cookie: undefined } }, false);
|
|
148
|
+
if (fromHeaders)
|
|
149
|
+
return { token: fromHeaders, source: 'header' };
|
|
150
|
+
const fromCookie = parseCookies(typeof headers.cookie === 'string' ? headers.cookie : undefined)[API_SECURITY_CONSTANTS.TOKEN_COOKIE];
|
|
151
|
+
return fromCookie ? { token: fromCookie, source: 'cookie' } : null;
|
|
152
|
+
}
|
|
153
|
+
/**
|
|
154
|
+
* Parse a `Cookie` header (exported for the caller-identity middleware).
|
|
155
|
+
*
|
|
156
|
+
* @param header - Raw cookie header
|
|
157
|
+
* @returns Parsed cookies
|
|
158
|
+
*/
|
|
159
|
+
export function parseCookieHeader(header) {
|
|
160
|
+
return parseCookies(header);
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Whether a request presented a valid API token.
|
|
164
|
+
*
|
|
165
|
+
* @param req - Request carrying headers and url
|
|
166
|
+
* @param allowQuery - Whether to consult the query string
|
|
167
|
+
* @returns True when the presented token matches
|
|
168
|
+
*/
|
|
169
|
+
export function hasValidApiToken(req, allowQuery) {
|
|
170
|
+
return verifyApiToken(extractPresentedToken(req, allowQuery));
|
|
171
|
+
}
|
|
172
|
+
/**
|
|
173
|
+
* Send the standard 401 challenge.
|
|
174
|
+
*
|
|
175
|
+
* @param res - Express response
|
|
176
|
+
*/
|
|
177
|
+
function sendUnauthorized(res) {
|
|
178
|
+
res.setHeader('WWW-Authenticate', API_SECURITY_CONSTANTS.AUTH_SCHEME);
|
|
179
|
+
res.status(401).json({
|
|
180
|
+
success: false,
|
|
181
|
+
error: API_SECURITY_CONSTANTS.ERRORS.UNAUTHORIZED,
|
|
182
|
+
hint: API_SECURITY_CONSTANTS.UNAUTHORIZED_HINT,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Express middleware: loopback passes, everyone else needs the API token.
|
|
187
|
+
*
|
|
188
|
+
* Mounted in front of `/api` in `backend/src/index.ts`. `/health`, static
|
|
189
|
+
* assets and the SPA shell are not under `/api` and stay open.
|
|
190
|
+
*
|
|
191
|
+
* @param req - Express request
|
|
192
|
+
* @param res - Express response
|
|
193
|
+
* @param next - Express next
|
|
194
|
+
*/
|
|
195
|
+
export function apiTokenMiddleware(req, res, next) {
|
|
196
|
+
if (isLoopbackRequest(req)) {
|
|
197
|
+
next();
|
|
198
|
+
return;
|
|
199
|
+
}
|
|
200
|
+
if (hasValidApiToken(req, false)) {
|
|
201
|
+
next();
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
logger.warn('Rejected unauthenticated non-loopback API request', {
|
|
205
|
+
address: getClientAddress(req),
|
|
206
|
+
method: req.method,
|
|
207
|
+
path: req.originalUrl,
|
|
208
|
+
});
|
|
209
|
+
sendUnauthorized(res);
|
|
210
|
+
}
|
|
211
|
+
/**
|
|
212
|
+
* Whether `GET /health` is configured open to everyone (`CREWLY_PUBLIC_HEALTH`).
|
|
213
|
+
*
|
|
214
|
+
* @returns True when the env var is `1` or `true`
|
|
215
|
+
*/
|
|
216
|
+
export function isPublicHealthEnabled() {
|
|
217
|
+
const v = (process.env[API_SECURITY_CONSTANTS.ENV.PUBLIC_HEALTH] ?? '').trim().toLowerCase();
|
|
218
|
+
return v === '1' || v === 'true';
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* Gate for the root `GET /health` (#825).
|
|
222
|
+
*
|
|
223
|
+
* - Loopback: passes straight to the handler, untouched (CLI, Docker
|
|
224
|
+
* healthchecks, desktop, local dashboard).
|
|
225
|
+
* - Non-loopback with a valid API token (header, Bearer or cookie): passes.
|
|
226
|
+
* - Non-loopback without one: the standard 401 token challenge, same as
|
|
227
|
+
* `/api`, and nothing about the install is disclosed.
|
|
228
|
+
* - `CREWLY_PUBLIC_HEALTH=1`: everyone passes (public monitors behind a
|
|
229
|
+
* reverse proxy).
|
|
230
|
+
*
|
|
231
|
+
* Why: crewly-mobile probes `/health` to choose its LAN transport. While
|
|
232
|
+
* `/health` answered 200 to anyone, a phone without the token chose LAN and
|
|
233
|
+
* then got 401 on every `/api` call, never falling back to the relay. A 401
|
|
234
|
+
* here makes that probe fail, so the app uses the relay, which works.
|
|
235
|
+
* "Loopback" follows {@link getClientAddress}: the socket address, or the
|
|
236
|
+
* first `X-Forwarded-For` hop only when `CREWLY_TRUST_PROXY` is set.
|
|
237
|
+
*
|
|
238
|
+
* @param req - Express request
|
|
239
|
+
* @param res - Express response
|
|
240
|
+
* @param next - Express next
|
|
241
|
+
*/
|
|
242
|
+
export function healthGateMiddleware(req, res, next) {
|
|
243
|
+
if (isPublicHealthEnabled() || isLoopbackRequest(req) || hasValidApiToken(req, false)) {
|
|
244
|
+
next();
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
sendUnauthorized(res);
|
|
248
|
+
}
|
|
249
|
+
/**
|
|
250
|
+
* Decide whether a raw HTTP request (WebSocket upgrade or Engine.IO
|
|
251
|
+
* handshake) may proceed.
|
|
252
|
+
*
|
|
253
|
+
* Loopback passes. Otherwise the API token must be present (query `token`
|
|
254
|
+
* accepted here because browsers cannot set headers on WebSocket
|
|
255
|
+
* handshakes). `/ws/chat` additionally accepts a valid chat JWT when
|
|
256
|
+
* `CREWLY_JWT_SECRET` is configured, since that gateway has its own auth.
|
|
257
|
+
*
|
|
258
|
+
* @param req - Incoming HTTP request
|
|
259
|
+
* @returns True when the connection is allowed
|
|
260
|
+
*/
|
|
261
|
+
export function isUpgradeAllowed(req) {
|
|
262
|
+
if (isLoopbackRequest(req))
|
|
263
|
+
return true;
|
|
264
|
+
if (hasValidApiToken(req, true))
|
|
265
|
+
return true;
|
|
266
|
+
const jwtSecret = process.env['CREWLY_JWT_SECRET'];
|
|
267
|
+
if (jwtSecret && req.url) {
|
|
268
|
+
try {
|
|
269
|
+
const host = req.headers.host || 'localhost';
|
|
270
|
+
const parsed = new URL(req.url, `http://${host}`);
|
|
271
|
+
if (parsed.pathname === CHAT_WS_PATH) {
|
|
272
|
+
const jwt = parsed.searchParams.get(API_SECURITY_CONSTANTS.TOKEN_QUERY_PARAM);
|
|
273
|
+
if (jwt && verifyHs256Token(jwt, jwtSecret)?.sub)
|
|
274
|
+
return true;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch {
|
|
278
|
+
// fall through
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
return false;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Engine.IO `allowRequest` hook for the Socket.IO server. Covers both the
|
|
285
|
+
* polling handshake (which bypasses Express) and the WebSocket transport.
|
|
286
|
+
*
|
|
287
|
+
* @param req - Handshake request
|
|
288
|
+
* @param callback - Engine.IO callback `(err, success)`
|
|
289
|
+
*/
|
|
290
|
+
export function socketIoAllowRequest(req, callback) {
|
|
291
|
+
if (isUpgradeAllowed(req)) {
|
|
292
|
+
callback(null, true);
|
|
293
|
+
return;
|
|
294
|
+
}
|
|
295
|
+
logger.warn('Rejected unauthenticated non-loopback Socket.IO handshake', {
|
|
296
|
+
address: getClientAddress(req),
|
|
297
|
+
});
|
|
298
|
+
callback(API_SECURITY_CONSTANTS.ERRORS.UNAUTHORIZED, false);
|
|
299
|
+
}
|
|
300
|
+
/**
|
|
301
|
+
* Install the WebSocket upgrade gate on an HTTP server.
|
|
302
|
+
*
|
|
303
|
+
* Wraps `httpServer.emit` (the same exclusive-handoff pattern the browser
|
|
304
|
+
* bridge and chat gateway use) so an unauthorised `upgrade` is answered
|
|
305
|
+
* with `401` and closed before ANY gateway sees it. Must be installed
|
|
306
|
+
* AFTER every gateway has attached so this wrapper is the outermost one.
|
|
307
|
+
*
|
|
308
|
+
* @param httpServer - Server whose upgrades to gate
|
|
309
|
+
*/
|
|
310
|
+
export function installWebSocketGate(httpServer) {
|
|
311
|
+
const originalEmit = httpServer.emit.bind(httpServer);
|
|
312
|
+
httpServer.emit = (event, ...args) => {
|
|
313
|
+
if (event === 'upgrade') {
|
|
314
|
+
const request = args[0];
|
|
315
|
+
const socket = args[1];
|
|
316
|
+
if (!isUpgradeAllowed(request)) {
|
|
317
|
+
logger.warn('Rejected unauthenticated non-loopback WebSocket upgrade', {
|
|
318
|
+
address: getClientAddress(request),
|
|
319
|
+
path: request.url,
|
|
320
|
+
});
|
|
321
|
+
socket.write(`HTTP/1.1 401 Unauthorized\r\nWWW-Authenticate: ${API_SECURITY_CONSTANTS.AUTH_SCHEME}\r\nConnection: close\r\nContent-Length: 0\r\n\r\n`);
|
|
322
|
+
socket.destroy();
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
return originalEmit(event, ...args);
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
//# sourceMappingURL=api-token.middleware.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-token.middleware.js","sourceRoot":"","sources":["../../../../../backend/src/middleware/api-token.middleware.ts"],"names":[],"mappings":"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"}
|