crewly 1.20.407 → 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.
Files changed (59) hide show
  1. package/dist/backend/backend/src/constants.d.ts +34 -0
  2. package/dist/backend/backend/src/constants.d.ts.map +1 -1
  3. package/dist/backend/backend/src/constants.js +34 -0
  4. package/dist/backend/backend/src/constants.js.map +1 -1
  5. package/dist/backend/backend/src/controllers/activity/activity.controller.d.ts +29 -0
  6. package/dist/backend/backend/src/controllers/activity/activity.controller.d.ts.map +1 -0
  7. package/dist/backend/backend/src/controllers/activity/activity.controller.js +77 -0
  8. package/dist/backend/backend/src/controllers/activity/activity.controller.js.map +1 -0
  9. package/dist/backend/backend/src/controllers/team/team.controller.d.ts.map +1 -1
  10. package/dist/backend/backend/src/controllers/team/team.controller.js +49 -0
  11. package/dist/backend/backend/src/controllers/team/team.controller.js.map +1 -1
  12. package/dist/backend/backend/src/routes/modules/system.routes.d.ts.map +1 -1
  13. package/dist/backend/backend/src/routes/modules/system.routes.js +3 -0
  14. package/dist/backend/backend/src/routes/modules/system.routes.js.map +1 -1
  15. package/dist/backend/backend/src/services/activity/activity-log.service.d.ts +192 -0
  16. package/dist/backend/backend/src/services/activity/activity-log.service.d.ts.map +1 -0
  17. package/dist/backend/backend/src/services/activity/activity-log.service.js +343 -0
  18. package/dist/backend/backend/src/services/activity/activity-log.service.js.map +1 -0
  19. package/dist/backend/backend/src/services/decisions/decision.service.d.ts +13 -0
  20. package/dist/backend/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  21. package/dist/backend/backend/src/services/decisions/decision.service.js +39 -0
  22. package/dist/backend/backend/src/services/decisions/decision.service.js.map +1 -1
  23. package/dist/backend/backend/src/services/decisions/decision.wiring.d.ts.map +1 -1
  24. package/dist/backend/backend/src/services/decisions/decision.wiring.js +2 -0
  25. package/dist/backend/backend/src/services/decisions/decision.wiring.js.map +1 -1
  26. package/dist/backend/build-info.json +2 -2
  27. package/dist/cli/backend/src/constants.d.ts +34 -0
  28. package/dist/cli/backend/src/constants.d.ts.map +1 -1
  29. package/dist/cli/backend/src/constants.js +34 -0
  30. package/dist/cli/backend/src/constants.js.map +1 -1
  31. package/dist/cli/backend/src/middleware/agent-origin-correction.d.ts +31 -0
  32. package/dist/cli/backend/src/middleware/agent-origin-correction.d.ts.map +1 -0
  33. package/dist/cli/backend/src/middleware/agent-origin-correction.js +31 -0
  34. package/dist/cli/backend/src/middleware/agent-origin-correction.js.map +1 -0
  35. package/dist/cli/backend/src/middleware/api-token.middleware.d.ts +171 -0
  36. package/dist/cli/backend/src/middleware/api-token.middleware.d.ts.map +1 -0
  37. package/dist/cli/backend/src/middleware/api-token.middleware.js +329 -0
  38. package/dist/cli/backend/src/middleware/api-token.middleware.js.map +1 -0
  39. package/dist/cli/backend/src/middleware/caller-identity.middleware.d.ts +290 -0
  40. package/dist/cli/backend/src/middleware/caller-identity.middleware.d.ts.map +1 -0
  41. package/dist/cli/backend/src/middleware/caller-identity.middleware.js +658 -0
  42. package/dist/cli/backend/src/middleware/caller-identity.middleware.js.map +1 -0
  43. package/dist/cli/backend/src/middleware/require-auth.middleware.d.ts +60 -0
  44. package/dist/cli/backend/src/middleware/require-auth.middleware.d.ts.map +1 -0
  45. package/dist/cli/backend/src/middleware/require-auth.middleware.js +127 -0
  46. package/dist/cli/backend/src/middleware/require-auth.middleware.js.map +1 -0
  47. package/dist/cli/backend/src/services/activity/activity-log.service.d.ts +192 -0
  48. package/dist/cli/backend/src/services/activity/activity-log.service.d.ts.map +1 -0
  49. package/dist/cli/backend/src/services/activity/activity-log.service.js +343 -0
  50. package/dist/cli/backend/src/services/activity/activity-log.service.js.map +1 -0
  51. package/dist/cli/backend/src/services/core/peer-process.service.d.ts +168 -0
  52. package/dist/cli/backend/src/services/core/peer-process.service.d.ts.map +1 -0
  53. package/dist/cli/backend/src/services/core/peer-process.service.js +352 -0
  54. package/dist/cli/backend/src/services/core/peer-process.service.js.map +1 -0
  55. package/dist/cli/backend/src/services/decisions/decision.service.d.ts +13 -0
  56. package/dist/cli/backend/src/services/decisions/decision.service.d.ts.map +1 -1
  57. package/dist/cli/backend/src/services/decisions/decision.service.js +39 -0
  58. package/dist/cli/backend/src/services/decisions/decision.service.js.map +1 -1
  59. 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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"}