@shipfox/api-server 2.0.0

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 (47) hide show
  1. package/.turbo/turbo-build.log +2 -0
  2. package/.turbo/turbo-type$colon$emit.log +1 -0
  3. package/.turbo/turbo-type.log +1 -0
  4. package/CHANGELOG.md +35 -0
  5. package/LICENSE +21 -0
  6. package/README.md +59 -0
  7. package/dist/config.d.ts +9 -0
  8. package/dist/config.d.ts.map +1 -0
  9. package/dist/config.js +43 -0
  10. package/dist/config.js.map +1 -0
  11. package/dist/e2e.d.ts +10 -0
  12. package/dist/e2e.d.ts.map +1 -0
  13. package/dist/e2e.js +42 -0
  14. package/dist/e2e.js.map +1 -0
  15. package/dist/index.d.ts +4 -0
  16. package/dist/index.d.ts.map +1 -0
  17. package/dist/index.js +4 -0
  18. package/dist/index.js.map +1 -0
  19. package/dist/instrumentation.d.ts +2 -0
  20. package/dist/instrumentation.d.ts.map +1 -0
  21. package/dist/instrumentation.js +13 -0
  22. package/dist/instrumentation.js.map +1 -0
  23. package/dist/modules.d.ts +3 -0
  24. package/dist/modules.d.ts.map +1 -0
  25. package/dist/modules.js +76 -0
  26. package/dist/modules.js.map +1 -0
  27. package/dist/server.d.ts +16 -0
  28. package/dist/server.d.ts.map +1 -0
  29. package/dist/server.js +215 -0
  30. package/dist/server.js.map +1 -0
  31. package/dist/tsconfig.test.tsbuildinfo +1 -0
  32. package/package.json +80 -0
  33. package/src/config.test.ts +33 -0
  34. package/src/config.ts +54 -0
  35. package/src/e2e.test.ts +101 -0
  36. package/src/e2e.ts +61 -0
  37. package/src/index.ts +3 -0
  38. package/src/instrumentation.ts +8 -0
  39. package/src/modules.test.ts +137 -0
  40. package/src/modules.ts +90 -0
  41. package/src/server.test.ts +539 -0
  42. package/src/server.ts +242 -0
  43. package/tsconfig.build.json +9 -0
  44. package/tsconfig.build.tsbuildinfo +1 -0
  45. package/tsconfig.json +4 -0
  46. package/tsconfig.test.json +8 -0
  47. package/vitest.config.ts +3 -0
@@ -0,0 +1,2 @@
1
+ $ shipfox-swc
2
+ Successfully compiled: 6 files with swc (77.53ms)
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-emit
@@ -0,0 +1 @@
1
+ $ shipfox-tsc-check
package/CHANGELOG.md ADDED
@@ -0,0 +1,35 @@
1
+ # @shipfox/api-server
2
+
3
+ ## 2.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 7fc3ab5: Adds a startup-failure hook so applications can report errors before server cleanup closes monitoring.
8
+ - 521e006: Adds the @shipfox/api-server package with server lifecycle, default module composition, and an
9
+ instrumentation preload entry, and exposes shutdownServiceMetrics from @shipfox/node-opentelemetry.
10
+
11
+ ### Patch Changes
12
+
13
+ - Updated dependencies [0cd6dd4]
14
+ - Updated dependencies [c31a7e0]
15
+ - Updated dependencies [1b0d344]
16
+ - Updated dependencies [521e006]
17
+ - @shipfox/node-module@0.2.0
18
+ - @shipfox/api-integration-core@2.0.0
19
+ - @shipfox/api-auth@2.0.0
20
+ - @shipfox/api-workspaces@2.0.0
21
+ - @shipfox/annotations@2.0.0
22
+ - @shipfox/api-agent@2.0.0
23
+ - @shipfox/api-definitions@2.0.0
24
+ - @shipfox/api-dispatcher@2.0.0
25
+ - @shipfox/api-logs@2.0.0
26
+ - @shipfox/api-projects@2.0.0
27
+ - @shipfox/api-runners@2.0.0
28
+ - @shipfox/api-secrets@2.0.0
29
+ - @shipfox/api-triggers@2.0.0
30
+ - @shipfox/api-workflows@2.0.0
31
+ - @shipfox/config@1.2.1
32
+ - @shipfox/node-error-monitoring@0.1.2
33
+ - @shipfox/node-fastify@0.2.1
34
+ - @shipfox/node-opentelemetry@0.5.0
35
+ - @shipfox/node-postgres@0.4.1
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Shipfox
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,59 @@
1
+ # Shipfox API Server
2
+
3
+ Runs a Shipfox API server.
4
+
5
+ ## What it does
6
+
7
+ - **`defaultModules()`**: Returns the standard module list.
8
+ - **`createServer()`**: Builds an API server. The caller owns process signals.
9
+ - **`runServer()`**: Starts the server. It listens for SIGTERM and SIGINT.
10
+ - **Instrumentation preload**: Starts metrics early. Load it before feature modules.
11
+
12
+ ## Installation
13
+
14
+ ```sh
15
+ pnpm add @shipfox/api-server
16
+ ```
17
+
18
+ ## Usage
19
+
20
+ ```ts
21
+ import {defaultModules, runServer} from '@shipfox/api-server';
22
+
23
+ await runServer({modules: await defaultModules()});
24
+ ```
25
+
26
+ Load the instrumentation entry before feature modules:
27
+
28
+ ```sh
29
+ node --import @shipfox/api-server/instrumentation ./dist/index.js
30
+ ```
31
+
32
+ ## Environment
33
+
34
+ | Variable | Default | Purpose |
35
+ | --- | --- | --- |
36
+ | `E2E_ENABLED` | `false` | Enables routes under `/__e2e` when `E2E_ADMIN_API_KEY` is set. |
37
+ | `E2E_ADMIN_API_KEY` | none | Required to enable and protect E2E routes. |
38
+ | `API_PORT` | shared `PORT` | Sets the listener port. |
39
+ | `API_TRUST_PROXY` | `false` | Sets proxy IP checks. |
40
+
41
+ ## Behavior Notes
42
+
43
+ - **Custom composition**: Pass a module list to make a custom server.
44
+ - **Signal handling**: `createServer` does not install signal handlers.
45
+ - **Lifecycle**: `start` starts workers before the HTTP listener. `stop` is safe to call again.
46
+ - **Process scope**: Run one server at a time.
47
+
48
+ ## Development
49
+
50
+ ```sh
51
+ turbo build --filter=@shipfox/api-server
52
+ turbo check --filter=@shipfox/api-server
53
+ turbo type --filter=@shipfox/api-server
54
+ turbo test --filter=@shipfox/api-server
55
+ ```
56
+
57
+ ## License
58
+
59
+ MIT
@@ -0,0 +1,9 @@
1
+ export type ApiTrustProxy = false | true | number | string;
2
+ export declare const config: Readonly<{
3
+ E2E_ENABLED: boolean;
4
+ E2E_ADMIN_API_KEY: string | undefined;
5
+ API_PORT: number | undefined;
6
+ API_TRUST_PROXY: string;
7
+ } & import("@shipfox/config").CleanedEnvAccessors>;
8
+ export declare function parseApiTrustProxy(value: string): ApiTrustProxy;
9
+ //# sourceMappingURL=config.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"config.d.ts","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAGA,MAAM,MAAM,aAAa,GAAG,KAAK,GAAG,IAAI,GAAG,MAAM,GAAG,MAAM,CAAC;AAI3D,eAAO,MAAM,MAAM;;;;;kDAiBjB,CAAC;AAaH,wBAAgB,kBAAkB,CAAC,KAAK,EAAE,MAAM,GAAG,aAAa,CAgB/D"}
package/dist/config.js ADDED
@@ -0,0 +1,43 @@
1
+ import { isIP } from 'node:net';
2
+ import { bool, createConfig, port, str } from '@shipfox/config';
3
+ const INTEGER_PATTERN = /^\d+$/;
4
+ export const config = createConfig({
5
+ E2E_ENABLED: bool({
6
+ desc: 'Enables the end-to-end test routes under /__e2e. Keep it false in production.',
7
+ default: false
8
+ }),
9
+ E2E_ADMIN_API_KEY: str({
10
+ desc: 'Bearer token that protects the E2E admin routes. Set it when E2E_ENABLED is true.',
11
+ default: undefined
12
+ }),
13
+ API_PORT: port({
14
+ desc: 'Port the API HTTP server listens on. When unset, the shared PORT setting controls the server port.',
15
+ default: undefined
16
+ }),
17
+ API_TRUST_PROXY: str({
18
+ desc: 'Controls how the API trusts proxy headers for client IP detection. Use false when clients connect directly, true only when every caller is a trusted proxy, a positive hop count such as 1, or a trusted proxy IP/CIDR such as 10.0.0.0/8.',
19
+ default: 'false'
20
+ })
21
+ });
22
+ function parseCidr(value) {
23
+ const [address, prefix, extra] = value.split('/');
24
+ if (!address || !prefix || extra !== undefined || isIP(address) === 0) return false;
25
+ const maxPrefix = isIP(address) === 4 ? 32 : 128;
26
+ if (!INTEGER_PATTERN.test(prefix)) return false;
27
+ const prefixNumber = Number(prefix);
28
+ return prefixNumber >= 0 && prefixNumber <= maxPrefix;
29
+ }
30
+ export function parseApiTrustProxy(value) {
31
+ const trimmed = value.trim();
32
+ if (trimmed === 'false') return false;
33
+ if (trimmed === 'true') return true;
34
+ if (INTEGER_PATTERN.test(trimmed)) {
35
+ const hops = Number(trimmed);
36
+ if (Number.isSafeInteger(hops) && hops > 0) return hops;
37
+ throw new Error('API_TRUST_PROXY hop count must be a positive safe integer');
38
+ }
39
+ if (isIP(trimmed) !== 0 || parseCidr(trimmed)) return trimmed;
40
+ throw new Error('API_TRUST_PROXY must be false, true, a positive hop count, or a trusted proxy IP/CIDR');
41
+ }
42
+
43
+ //# sourceMappingURL=config.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/config.ts"],"sourcesContent":["import {isIP} from 'node:net';\nimport {bool, createConfig, port, str} from '@shipfox/config';\n\nexport type ApiTrustProxy = false | true | number | string;\n\nconst INTEGER_PATTERN = /^\\d+$/;\n\nexport const config = createConfig({\n E2E_ENABLED: bool({\n desc: 'Enables the end-to-end test routes under /__e2e. Keep it false in production.',\n default: false,\n }),\n E2E_ADMIN_API_KEY: str({\n desc: 'Bearer token that protects the E2E admin routes. Set it when E2E_ENABLED is true.',\n default: undefined,\n }),\n API_PORT: port({\n desc: 'Port the API HTTP server listens on. When unset, the shared PORT setting controls the server port.',\n default: undefined,\n }),\n API_TRUST_PROXY: str({\n desc: 'Controls how the API trusts proxy headers for client IP detection. Use false when clients connect directly, true only when every caller is a trusted proxy, a positive hop count such as 1, or a trusted proxy IP/CIDR such as 10.0.0.0/8.',\n default: 'false',\n }),\n});\n\nfunction parseCidr(value: string): boolean {\n const [address, prefix, extra] = value.split('/');\n if (!address || !prefix || extra !== undefined || isIP(address) === 0) return false;\n\n const maxPrefix = isIP(address) === 4 ? 32 : 128;\n if (!INTEGER_PATTERN.test(prefix)) return false;\n\n const prefixNumber = Number(prefix);\n return prefixNumber >= 0 && prefixNumber <= maxPrefix;\n}\n\nexport function parseApiTrustProxy(value: string): ApiTrustProxy {\n const trimmed = value.trim();\n if (trimmed === 'false') return false;\n if (trimmed === 'true') return true;\n\n if (INTEGER_PATTERN.test(trimmed)) {\n const hops = Number(trimmed);\n if (Number.isSafeInteger(hops) && hops > 0) return hops;\n throw new Error('API_TRUST_PROXY hop count must be a positive safe integer');\n }\n\n if (isIP(trimmed) !== 0 || parseCidr(trimmed)) return trimmed;\n\n throw new Error(\n 'API_TRUST_PROXY must be false, true, a positive hop count, or a trusted proxy IP/CIDR',\n );\n}\n"],"names":["isIP","bool","createConfig","port","str","INTEGER_PATTERN","config","E2E_ENABLED","desc","default","E2E_ADMIN_API_KEY","undefined","API_PORT","API_TRUST_PROXY","parseCidr","value","address","prefix","extra","split","maxPrefix","test","prefixNumber","Number","parseApiTrustProxy","trimmed","trim","hops","isSafeInteger","Error"],"mappings":"AAAA,SAAQA,IAAI,QAAO,WAAW;AAC9B,SAAQC,IAAI,EAAEC,YAAY,EAAEC,IAAI,EAAEC,GAAG,QAAO,kBAAkB;AAI9D,MAAMC,kBAAkB;AAExB,OAAO,MAAMC,SAASJ,aAAa;IACjCK,aAAaN,KAAK;QAChBO,MAAM;QACNC,SAAS;IACX;IACAC,mBAAmBN,IAAI;QACrBI,MAAM;QACNC,SAASE;IACX;IACAC,UAAUT,KAAK;QACbK,MAAM;QACNC,SAASE;IACX;IACAE,iBAAiBT,IAAI;QACnBI,MAAM;QACNC,SAAS;IACX;AACF,GAAG;AAEH,SAASK,UAAUC,KAAa;IAC9B,MAAM,CAACC,SAASC,QAAQC,MAAM,GAAGH,MAAMI,KAAK,CAAC;IAC7C,IAAI,CAACH,WAAW,CAACC,UAAUC,UAAUP,aAAaX,KAAKgB,aAAa,GAAG,OAAO;IAE9E,MAAMI,YAAYpB,KAAKgB,aAAa,IAAI,KAAK;IAC7C,IAAI,CAACX,gBAAgBgB,IAAI,CAACJ,SAAS,OAAO;IAE1C,MAAMK,eAAeC,OAAON;IAC5B,OAAOK,gBAAgB,KAAKA,gBAAgBF;AAC9C;AAEA,OAAO,SAASI,mBAAmBT,KAAa;IAC9C,MAAMU,UAAUV,MAAMW,IAAI;IAC1B,IAAID,YAAY,SAAS,OAAO;IAChC,IAAIA,YAAY,QAAQ,OAAO;IAE/B,IAAIpB,gBAAgBgB,IAAI,CAACI,UAAU;QACjC,MAAME,OAAOJ,OAAOE;QACpB,IAAIF,OAAOK,aAAa,CAACD,SAASA,OAAO,GAAG,OAAOA;QACnD,MAAM,IAAIE,MAAM;IAClB;IAEA,IAAI7B,KAAKyB,aAAa,KAAKX,UAAUW,UAAU,OAAOA;IAEtD,MAAM,IAAII,MACR;AAEJ"}
package/dist/e2e.d.ts ADDED
@@ -0,0 +1,10 @@
1
+ import { type AuthMethod, type RouteExport, type RouteGroup } from '@shipfox/node-fastify';
2
+ export declare const AUTH_E2E_ADMIN = "e2e-admin";
3
+ export interface E2eConfig {
4
+ E2E_ENABLED: boolean;
5
+ E2E_ADMIN_API_KEY?: string | undefined;
6
+ }
7
+ export declare function shouldMountE2eRoutes(config: E2eConfig): boolean;
8
+ export declare function createE2eAdminAuthMethod(config: E2eConfig): AuthMethod;
9
+ export declare function createE2eRouteGroup(routes: RouteExport[], config: E2eConfig): RouteGroup[];
10
+ //# sourceMappingURL=e2e.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"e2e.d.ts","sourceRoot":"","sources":["../src/e2e.ts"],"names":[],"mappings":"AACA,OAAO,EACL,KAAK,UAAU,EAEf,KAAK,WAAW,EAChB,KAAK,UAAU,EAChB,MAAM,uBAAuB,CAAC;AAE/B,eAAO,MAAM,cAAc,cAAc,CAAC;AAE1C,MAAM,WAAW,SAAS;IACxB,WAAW,EAAE,OAAO,CAAC;IACrB,iBAAiB,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;CACxC;AAID,wBAAgB,oBAAoB,CAAC,MAAM,EAAE,SAAS,GAAG,OAAO,CAE/D;AAcD,wBAAgB,wBAAwB,CAAC,MAAM,EAAE,SAAS,GAAG,UAAU,CAetE;AAED,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,WAAW,EAAE,EAAE,MAAM,EAAE,SAAS,GAAG,UAAU,EAAE,CAU1F"}
package/dist/e2e.js ADDED
@@ -0,0 +1,42 @@
1
+ import { timingSafeEqual } from 'node:crypto';
2
+ import { ClientError } from '@shipfox/node-fastify';
3
+ export const AUTH_E2E_ADMIN = 'e2e-admin';
4
+ const BEARER_RE = /^Bearer /iu;
5
+ export function shouldMountE2eRoutes(config) {
6
+ return config.E2E_ENABLED && Boolean(config.E2E_ADMIN_API_KEY);
7
+ }
8
+ function extractBearerToken(header) {
9
+ return header?.replace(BEARER_RE, '');
10
+ }
11
+ function tokensMatch(actual, expected) {
12
+ const actualBuffer = Buffer.from(actual);
13
+ const expectedBuffer = Buffer.from(expected);
14
+ return actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer);
15
+ }
16
+ export function createE2eAdminAuthMethod(config) {
17
+ return {
18
+ name: AUTH_E2E_ADMIN,
19
+ authenticate: async (request)=>{
20
+ await Promise.resolve();
21
+ const expected = config.E2E_ADMIN_API_KEY;
22
+ const actual = extractBearerToken(request.headers.authorization);
23
+ if (!expected || !actual || !tokensMatch(actual, expected)) {
24
+ throw new ClientError('Missing or invalid E2E admin API key', 'unauthorized', {
25
+ status: 401
26
+ });
27
+ }
28
+ }
29
+ };
30
+ }
31
+ export function createE2eRouteGroup(routes, config) {
32
+ if (!shouldMountE2eRoutes(config) || routes.length === 0) return [];
33
+ return [
34
+ {
35
+ prefix: '/__e2e',
36
+ auth: AUTH_E2E_ADMIN,
37
+ routes
38
+ }
39
+ ];
40
+ }
41
+
42
+ //# sourceMappingURL=e2e.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/e2e.ts"],"sourcesContent":["import {timingSafeEqual} from 'node:crypto';\nimport {\n type AuthMethod,\n ClientError,\n type RouteExport,\n type RouteGroup,\n} from '@shipfox/node-fastify';\n\nexport const AUTH_E2E_ADMIN = 'e2e-admin';\n\nexport interface E2eConfig {\n E2E_ENABLED: boolean;\n E2E_ADMIN_API_KEY?: string | undefined;\n}\n\nconst BEARER_RE = /^Bearer /iu;\n\nexport function shouldMountE2eRoutes(config: E2eConfig): boolean {\n return config.E2E_ENABLED && Boolean(config.E2E_ADMIN_API_KEY);\n}\n\nfunction extractBearerToken(header: string | undefined): string | undefined {\n return header?.replace(BEARER_RE, '');\n}\n\nfunction tokensMatch(actual: string, expected: string): boolean {\n const actualBuffer = Buffer.from(actual);\n const expectedBuffer = Buffer.from(expected);\n return (\n actualBuffer.length === expectedBuffer.length && timingSafeEqual(actualBuffer, expectedBuffer)\n );\n}\n\nexport function createE2eAdminAuthMethod(config: E2eConfig): AuthMethod {\n return {\n name: AUTH_E2E_ADMIN,\n authenticate: async (request) => {\n await Promise.resolve();\n const expected = config.E2E_ADMIN_API_KEY;\n const actual = extractBearerToken(request.headers.authorization);\n\n if (!expected || !actual || !tokensMatch(actual, expected)) {\n throw new ClientError('Missing or invalid E2E admin API key', 'unauthorized', {\n status: 401,\n });\n }\n },\n };\n}\n\nexport function createE2eRouteGroup(routes: RouteExport[], config: E2eConfig): RouteGroup[] {\n if (!shouldMountE2eRoutes(config) || routes.length === 0) return [];\n\n return [\n {\n prefix: '/__e2e',\n auth: AUTH_E2E_ADMIN,\n routes,\n },\n ];\n}\n"],"names":["timingSafeEqual","ClientError","AUTH_E2E_ADMIN","BEARER_RE","shouldMountE2eRoutes","config","E2E_ENABLED","Boolean","E2E_ADMIN_API_KEY","extractBearerToken","header","replace","tokensMatch","actual","expected","actualBuffer","Buffer","from","expectedBuffer","length","createE2eAdminAuthMethod","name","authenticate","request","Promise","resolve","headers","authorization","status","createE2eRouteGroup","routes","prefix","auth"],"mappings":"AAAA,SAAQA,eAAe,QAAO,cAAc;AAC5C,SAEEC,WAAW,QAGN,wBAAwB;AAE/B,OAAO,MAAMC,iBAAiB,YAAY;AAO1C,MAAMC,YAAY;AAElB,OAAO,SAASC,qBAAqBC,MAAiB;IACpD,OAAOA,OAAOC,WAAW,IAAIC,QAAQF,OAAOG,iBAAiB;AAC/D;AAEA,SAASC,mBAAmBC,MAA0B;IACpD,OAAOA,QAAQC,QAAQR,WAAW;AACpC;AAEA,SAASS,YAAYC,MAAc,EAAEC,QAAgB;IACnD,MAAMC,eAAeC,OAAOC,IAAI,CAACJ;IACjC,MAAMK,iBAAiBF,OAAOC,IAAI,CAACH;IACnC,OACEC,aAAaI,MAAM,KAAKD,eAAeC,MAAM,IAAInB,gBAAgBe,cAAcG;AAEnF;AAEA,OAAO,SAASE,yBAAyBf,MAAiB;IACxD,OAAO;QACLgB,MAAMnB;QACNoB,cAAc,OAAOC;YACnB,MAAMC,QAAQC,OAAO;YACrB,MAAMX,WAAWT,OAAOG,iBAAiB;YACzC,MAAMK,SAASJ,mBAAmBc,QAAQG,OAAO,CAACC,aAAa;YAE/D,IAAI,CAACb,YAAY,CAACD,UAAU,CAACD,YAAYC,QAAQC,WAAW;gBAC1D,MAAM,IAAIb,YAAY,wCAAwC,gBAAgB;oBAC5E2B,QAAQ;gBACV;YACF;QACF;IACF;AACF;AAEA,OAAO,SAASC,oBAAoBC,MAAqB,EAAEzB,MAAiB;IAC1E,IAAI,CAACD,qBAAqBC,WAAWyB,OAAOX,MAAM,KAAK,GAAG,OAAO,EAAE;IAEnE,OAAO;QACL;YACEY,QAAQ;YACRC,MAAM9B;YACN4B;QACF;KACD;AACH"}
@@ -0,0 +1,4 @@
1
+ export { defaultModules } from './modules.js';
2
+ export type { CreateServerOptions, RunServerOptions, ServerHandle } from './server.js';
3
+ export { createServer, runServer } from './server.js';
4
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAC,cAAc,EAAC,MAAM,cAAc,CAAC;AAC5C,YAAY,EAAC,mBAAmB,EAAE,gBAAgB,EAAE,YAAY,EAAC,MAAM,aAAa,CAAC;AACrF,OAAO,EAAC,YAAY,EAAE,SAAS,EAAC,MAAM,aAAa,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,4 @@
1
+ export { defaultModules } from './modules.js';
2
+ export { createServer, runServer } from './server.js';
3
+
4
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts"],"sourcesContent":["export {defaultModules} from './modules.js';\nexport type {CreateServerOptions, RunServerOptions, ServerHandle} from './server.js';\nexport {createServer, runServer} from './server.js';\n"],"names":["defaultModules","createServer","runServer"],"mappings":"AAAA,SAAQA,cAAc,QAAO,eAAe;AAE5C,SAAQC,YAAY,EAAEC,SAAS,QAAO,cAAc"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=instrumentation.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"instrumentation.d.ts","sourceRoot":"","sources":["../src/instrumentation.ts"],"names":[],"mappings":""}
@@ -0,0 +1,13 @@
1
+ import { startInstanceInstrumentation } from '@shipfox/node-opentelemetry';
2
+ // The metrics API has no proxy meter: instruments created before this preload
3
+ // completes bind to a no-op provider for the process lifetime.
4
+ await startInstanceInstrumentation({
5
+ serviceName: 'api',
6
+ instrumentations: {
7
+ fastify: true,
8
+ http: true,
9
+ pg: true
10
+ }
11
+ });
12
+
13
+ //# sourceMappingURL=instrumentation.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/instrumentation.ts"],"sourcesContent":["import {startInstanceInstrumentation} from '@shipfox/node-opentelemetry';\n\n// The metrics API has no proxy meter: instruments created before this preload\n// completes bind to a no-op provider for the process lifetime.\nawait startInstanceInstrumentation({\n serviceName: 'api',\n instrumentations: {fastify: true, http: true, pg: true},\n});\n"],"names":["startInstanceInstrumentation","serviceName","instrumentations","fastify","http","pg"],"mappings":"AAAA,SAAQA,4BAA4B,QAAO,8BAA8B;AAEzE,8EAA8E;AAC9E,+DAA+D;AAC/D,MAAMA,6BAA6B;IACjCC,aAAa;IACbC,kBAAkB;QAACC,SAAS;QAAMC,MAAM;QAAMC,IAAI;IAAI;AACxD"}
@@ -0,0 +1,3 @@
1
+ import type { ShipfoxModule } from '@shipfox/node-module';
2
+ export declare function defaultModules(): Promise<ShipfoxModule[]>;
3
+ //# sourceMappingURL=modules.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"modules.d.ts","sourceRoot":"","sources":["../src/modules.ts"],"names":[],"mappings":"AAwBA,OAAO,KAAK,EAAC,aAAa,EAAC,MAAM,sBAAsB,CAAC;AAExD,wBAAsB,cAAc,IAAI,OAAO,CAAC,aAAa,EAAE,CAAC,CA+D/D"}
@@ -0,0 +1,76 @@
1
+ import { annotationsModule } from '@shipfox/annotations';
2
+ import { agentModule } from '@shipfox/api-agent';
3
+ import { authModule } from '@shipfox/api-auth';
4
+ import { createDefinitionsModule } from '@shipfox/api-definitions';
5
+ import { dispatcherModule } from '@shipfox/api-dispatcher';
6
+ import { buildAgentToolCatalogs, buildAgentToolSelectionCatalogs, createIntegrationsContext, createWorkspaceConnectionSnapshotLoader, getIntegrationConnectionById } from '@shipfox/api-integration-core';
7
+ import { logsModule } from '@shipfox/api-logs';
8
+ import { createProjectsModule } from '@shipfox/api-projects';
9
+ import { runnersModule } from '@shipfox/api-runners';
10
+ import { deleteSecrets, getSecret, secretsModule, setSecrets } from '@shipfox/api-secrets';
11
+ import { triggersModule } from '@shipfox/api-triggers';
12
+ import { loadRunningLeasedStep, setAgentToolMaterializationServices, setSourceControl, workflowsModule } from '@shipfox/api-workflows';
13
+ import { workspacesModule } from '@shipfox/api-workspaces';
14
+ export async function defaultModules() {
15
+ const integrations = await createIntegrationsContext({
16
+ secrets: {
17
+ deleteSecrets,
18
+ linear: {
19
+ getSecret: (params)=>getSecret({
20
+ ...params,
21
+ namespace: `system/integrations/linear/${params.namespace}`
22
+ }),
23
+ setSecrets: (params)=>setSecrets({
24
+ ...params,
25
+ namespace: `system/integrations/linear/${params.namespace}`
26
+ }),
27
+ deleteSecrets: (params)=>deleteSecrets({
28
+ ...params,
29
+ namespace: `system/integrations/linear/${params.namespace}`
30
+ })
31
+ }
32
+ },
33
+ agentTools: {
34
+ loadLeasedAgentStep: loadRunningLeasedStep
35
+ }
36
+ });
37
+ const [agentToolSelectionCatalogs, agentToolCatalogs] = await Promise.all([
38
+ buildAgentToolSelectionCatalogs(integrations.registry),
39
+ buildAgentToolCatalogs(integrations.registry)
40
+ ]);
41
+ const loadWorkspaceConnectionSnapshot = createWorkspaceConnectionSnapshotLoader(integrations.registry);
42
+ // The checkout-token route resolves intents and mints credentials through the
43
+ // source-control service; wire it into the workflows module before serving.
44
+ setSourceControl(integrations.sourceControl);
45
+ setAgentToolMaterializationServices({
46
+ catalogs: agentToolCatalogs,
47
+ loadWorkspaceConnectionSnapshot,
48
+ getIntegrationConnectionById
49
+ });
50
+ const projectsModule = createProjectsModule({
51
+ sourceControl: integrations.sourceControl
52
+ });
53
+ const definitionsModule = createDefinitionsModule({
54
+ sourceControl: integrations.sourceControl,
55
+ agentToolSelectionCatalogs,
56
+ loadWorkspaceConnectionSnapshot,
57
+ getIntegrationConnectionById
58
+ });
59
+ return [
60
+ authModule,
61
+ workspacesModule,
62
+ secretsModule,
63
+ agentModule,
64
+ integrations.module,
65
+ projectsModule,
66
+ definitionsModule,
67
+ workflowsModule,
68
+ annotationsModule,
69
+ runnersModule,
70
+ logsModule,
71
+ triggersModule,
72
+ dispatcherModule
73
+ ];
74
+ }
75
+
76
+ //# sourceMappingURL=modules.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/modules.ts"],"sourcesContent":["import {annotationsModule} from '@shipfox/annotations';\nimport {agentModule} from '@shipfox/api-agent';\nimport {authModule} from '@shipfox/api-auth';\nimport {createDefinitionsModule} from '@shipfox/api-definitions';\nimport {dispatcherModule} from '@shipfox/api-dispatcher';\nimport {\n buildAgentToolCatalogs,\n buildAgentToolSelectionCatalogs,\n createIntegrationsContext,\n createWorkspaceConnectionSnapshotLoader,\n getIntegrationConnectionById,\n} from '@shipfox/api-integration-core';\nimport {logsModule} from '@shipfox/api-logs';\nimport {createProjectsModule} from '@shipfox/api-projects';\nimport {runnersModule} from '@shipfox/api-runners';\nimport {deleteSecrets, getSecret, secretsModule, setSecrets} from '@shipfox/api-secrets';\nimport {triggersModule} from '@shipfox/api-triggers';\nimport {\n loadRunningLeasedStep,\n setAgentToolMaterializationServices,\n setSourceControl,\n workflowsModule,\n} from '@shipfox/api-workflows';\nimport {workspacesModule} from '@shipfox/api-workspaces';\nimport type {ShipfoxModule} from '@shipfox/node-module';\n\nexport async function defaultModules(): Promise<ShipfoxModule[]> {\n const integrations = await createIntegrationsContext({\n secrets: {\n deleteSecrets,\n linear: {\n getSecret: (params) =>\n getSecret({\n ...params,\n namespace: `system/integrations/linear/${params.namespace}`,\n }),\n setSecrets: (params) =>\n setSecrets({\n ...params,\n namespace: `system/integrations/linear/${params.namespace}`,\n }),\n deleteSecrets: (params) =>\n deleteSecrets({\n ...params,\n namespace: `system/integrations/linear/${params.namespace}`,\n }),\n },\n },\n agentTools: {loadLeasedAgentStep: loadRunningLeasedStep},\n });\n const [agentToolSelectionCatalogs, agentToolCatalogs] = await Promise.all([\n buildAgentToolSelectionCatalogs(integrations.registry),\n buildAgentToolCatalogs(integrations.registry),\n ]);\n const loadWorkspaceConnectionSnapshot = createWorkspaceConnectionSnapshotLoader(\n integrations.registry,\n );\n\n // The checkout-token route resolves intents and mints credentials through the\n // source-control service; wire it into the workflows module before serving.\n setSourceControl(integrations.sourceControl);\n setAgentToolMaterializationServices({\n catalogs: agentToolCatalogs,\n loadWorkspaceConnectionSnapshot,\n getIntegrationConnectionById,\n });\n const projectsModule = createProjectsModule({sourceControl: integrations.sourceControl});\n const definitionsModule = createDefinitionsModule({\n sourceControl: integrations.sourceControl,\n agentToolSelectionCatalogs,\n loadWorkspaceConnectionSnapshot,\n getIntegrationConnectionById,\n });\n\n return [\n authModule,\n workspacesModule,\n secretsModule,\n agentModule,\n integrations.module,\n projectsModule,\n definitionsModule,\n workflowsModule,\n annotationsModule,\n runnersModule,\n logsModule,\n triggersModule,\n dispatcherModule,\n ];\n}\n"],"names":["annotationsModule","agentModule","authModule","createDefinitionsModule","dispatcherModule","buildAgentToolCatalogs","buildAgentToolSelectionCatalogs","createIntegrationsContext","createWorkspaceConnectionSnapshotLoader","getIntegrationConnectionById","logsModule","createProjectsModule","runnersModule","deleteSecrets","getSecret","secretsModule","setSecrets","triggersModule","loadRunningLeasedStep","setAgentToolMaterializationServices","setSourceControl","workflowsModule","workspacesModule","defaultModules","integrations","secrets","linear","params","namespace","agentTools","loadLeasedAgentStep","agentToolSelectionCatalogs","agentToolCatalogs","Promise","all","registry","loadWorkspaceConnectionSnapshot","sourceControl","catalogs","projectsModule","definitionsModule","module"],"mappings":"AAAA,SAAQA,iBAAiB,QAAO,uBAAuB;AACvD,SAAQC,WAAW,QAAO,qBAAqB;AAC/C,SAAQC,UAAU,QAAO,oBAAoB;AAC7C,SAAQC,uBAAuB,QAAO,2BAA2B;AACjE,SAAQC,gBAAgB,QAAO,0BAA0B;AACzD,SACEC,sBAAsB,EACtBC,+BAA+B,EAC/BC,yBAAyB,EACzBC,uCAAuC,EACvCC,4BAA4B,QACvB,gCAAgC;AACvC,SAAQC,UAAU,QAAO,oBAAoB;AAC7C,SAAQC,oBAAoB,QAAO,wBAAwB;AAC3D,SAAQC,aAAa,QAAO,uBAAuB;AACnD,SAAQC,aAAa,EAAEC,SAAS,EAAEC,aAAa,EAAEC,UAAU,QAAO,uBAAuB;AACzF,SAAQC,cAAc,QAAO,wBAAwB;AACrD,SACEC,qBAAqB,EACrBC,mCAAmC,EACnCC,gBAAgB,EAChBC,eAAe,QACV,yBAAyB;AAChC,SAAQC,gBAAgB,QAAO,0BAA0B;AAGzD,OAAO,eAAeC;IACpB,MAAMC,eAAe,MAAMjB,0BAA0B;QACnDkB,SAAS;YACPZ;YACAa,QAAQ;gBACNZ,WAAW,CAACa,SACVb,UAAU;wBACR,GAAGa,MAAM;wBACTC,WAAW,CAAC,2BAA2B,EAAED,OAAOC,SAAS,EAAE;oBAC7D;gBACFZ,YAAY,CAACW,SACXX,WAAW;wBACT,GAAGW,MAAM;wBACTC,WAAW,CAAC,2BAA2B,EAAED,OAAOC,SAAS,EAAE;oBAC7D;gBACFf,eAAe,CAACc,SACdd,cAAc;wBACZ,GAAGc,MAAM;wBACTC,WAAW,CAAC,2BAA2B,EAAED,OAAOC,SAAS,EAAE;oBAC7D;YACJ;QACF;QACAC,YAAY;YAACC,qBAAqBZ;QAAqB;IACzD;IACA,MAAM,CAACa,4BAA4BC,kBAAkB,GAAG,MAAMC,QAAQC,GAAG,CAAC;QACxE5B,gCAAgCkB,aAAaW,QAAQ;QACrD9B,uBAAuBmB,aAAaW,QAAQ;KAC7C;IACD,MAAMC,kCAAkC5B,wCACtCgB,aAAaW,QAAQ;IAGvB,8EAA8E;IAC9E,4EAA4E;IAC5Ef,iBAAiBI,aAAaa,aAAa;IAC3ClB,oCAAoC;QAClCmB,UAAUN;QACVI;QACA3B;IACF;IACA,MAAM8B,iBAAiB5B,qBAAqB;QAAC0B,eAAeb,aAAaa,aAAa;IAAA;IACtF,MAAMG,oBAAoBrC,wBAAwB;QAChDkC,eAAeb,aAAaa,aAAa;QACzCN;QACAK;QACA3B;IACF;IAEA,OAAO;QACLP;QACAoB;QACAP;QACAd;QACAuB,aAAaiB,MAAM;QACnBF;QACAC;QACAnB;QACArB;QACAY;QACAF;QACAO;QACAb;KACD;AACH"}
@@ -0,0 +1,16 @@
1
+ import { type ModuleWorker, type ShipfoxModule } from '@shipfox/node-module';
2
+ export interface CreateServerOptions {
3
+ modules: ShipfoxModule[];
4
+ onWorkerFailure?: (error: unknown, worker: ModuleWorker) => void | Promise<void>;
5
+ }
6
+ export interface ServerHandle {
7
+ start(): Promise<string>;
8
+ stop(): Promise<void>;
9
+ }
10
+ export interface RunServerOptions {
11
+ modules: ShipfoxModule[];
12
+ onStartupFailure?: (error: unknown) => void | Promise<void>;
13
+ }
14
+ export declare function createServer(options: CreateServerOptions): Promise<ServerHandle>;
15
+ export declare function runServer(options: RunServerOptions): Promise<ServerHandle>;
16
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAEA,OAAO,EAEL,KAAK,YAAY,EAMjB,KAAK,aAAa,EAEnB,MAAM,sBAAsB,CAAC;AAW9B,MAAM,WAAW,mBAAmB;IAClC,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,eAAe,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,YAAY,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAClF;AAED,MAAM,WAAW,YAAY;IAC3B,KAAK,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IACzB,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;CACvB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,aAAa,EAAE,CAAC;IACzB,gBAAgB,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D;AAED,wBAAsB,YAAY,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,YAAY,CAAC,CA2FtF;AAED,wBAAsB,SAAS,CAAC,OAAO,EAAE,gBAAgB,GAAG,OAAO,CAAC,YAAY,CAAC,CAgChF"}
package/dist/server.js ADDED
@@ -0,0 +1,215 @@
1
+ import { captureException, closeErrorMonitoring } from '@shipfox/node-error-monitoring';
2
+ import { closeApp, createApp, listen } from '@shipfox/node-fastify';
3
+ import { initializeModules, registerModuleMetrics, resetPublishers, resetSubscribers, runModuleStartupTasks, startModuleWorkers } from '@shipfox/node-module';
4
+ import { logger, shutdownServiceMetrics, startServiceMetrics } from '@shipfox/node-opentelemetry';
5
+ import { closePostgresClient, createPostgresClient } from '@shipfox/node-postgres';
6
+ import { config, parseApiTrustProxy } from './config.js';
7
+ import { createE2eAdminAuthMethod, createE2eRouteGroup } from './e2e.js';
8
+ const WORKER_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS = 10_000;
9
+ const ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS = 2_000;
10
+ let hasActiveServer = false;
11
+ export async function createServer(options) {
12
+ if (hasActiveServer) {
13
+ throw new Error('Cannot create a second API server before the existing server stops');
14
+ }
15
+ hasActiveServer = true;
16
+ try {
17
+ startServiceMetrics({
18
+ serviceName: 'api'
19
+ });
20
+ createPostgresClient();
21
+ const { auth, routes, e2eRoutes, workers } = await initializeModules({
22
+ modules: options.modules
23
+ });
24
+ registerModuleMetrics({
25
+ modules: options.modules
26
+ });
27
+ await runModuleStartupTasks({
28
+ modules: options.modules
29
+ });
30
+ const e2eAuth = config.E2E_ENABLED ? [
31
+ createE2eAdminAuthMethod(config)
32
+ ] : [];
33
+ const mountedE2eRoutes = createE2eRouteGroup(e2eRoutes, config);
34
+ logger().info('Creating HTTP server');
35
+ await createApp({
36
+ auth: [
37
+ ...auth,
38
+ ...e2eAuth
39
+ ],
40
+ routes: [
41
+ ...routes,
42
+ ...mountedE2eRoutes
43
+ ],
44
+ fastifyOptions: {
45
+ trustProxy: parseApiTrustProxy(config.API_TRUST_PROXY)
46
+ }
47
+ });
48
+ let workersHandle;
49
+ let startPromise;
50
+ let stopPromise;
51
+ let stopped = false;
52
+ const start = ()=>{
53
+ if (stopPromise || stopped) return Promise.reject(new Error('Cannot start a stopped API server'));
54
+ startPromise ??= (async ()=>{
55
+ logger().info('Starting module workers');
56
+ workersHandle = await startModuleWorkers({
57
+ workers,
58
+ onWorkerFailure: (error, worker)=>handleModuleWorkerFailure(error, worker, options.onWorkerFailure)
59
+ });
60
+ if (stopPromise) throw new Error('API server stopped during startup');
61
+ logger().info('Starting HTTP server');
62
+ const address = config.API_PORT === undefined ? await listen() : await listen({
63
+ port: config.API_PORT
64
+ });
65
+ logger().info({
66
+ address
67
+ }, 'HTTP server listening');
68
+ return address;
69
+ })();
70
+ return startPromise;
71
+ };
72
+ const stop = ()=>{
73
+ if (stopped) return Promise.resolve();
74
+ stopPromise ??= (async ()=>{
75
+ try {
76
+ await startPromise?.catch(()=>undefined);
77
+ const cleanupErrors = await runCleanupSteps([
78
+ ()=>closeApp(),
79
+ ()=>workersHandle?.stop(),
80
+ ()=>shutdownServiceMetrics(),
81
+ ()=>closePostgresClient(),
82
+ ()=>resetPublishers(),
83
+ ()=>resetSubscribers(),
84
+ ()=>closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS)
85
+ ]);
86
+ throwCleanupErrors(cleanupErrors, 'Failed to stop API server');
87
+ stopped = true;
88
+ hasActiveServer = false;
89
+ } finally{
90
+ stopPromise = undefined;
91
+ }
92
+ })();
93
+ return stopPromise;
94
+ };
95
+ return {
96
+ start,
97
+ stop
98
+ };
99
+ } catch (error) {
100
+ const cleanupErrors = await runCleanupSteps([
101
+ ()=>shutdownServiceMetrics(),
102
+ ()=>closePostgresClient(),
103
+ ()=>resetPublishers(),
104
+ ()=>resetSubscribers()
105
+ ]);
106
+ for (const cleanupError of cleanupErrors){
107
+ logger().error({
108
+ err: cleanupError,
109
+ bootError: error
110
+ }, 'Failed to clean up API server boot');
111
+ }
112
+ hasActiveServer = false;
113
+ throw error;
114
+ }
115
+ }
116
+ export async function runServer(options) {
117
+ let handle;
118
+ try {
119
+ handle = await createServer({
120
+ modules: options.modules,
121
+ onWorkerFailure: ()=>process.exit(1)
122
+ });
123
+ } catch (error) {
124
+ await reportStartupFailure(error, options.onStartupFailure);
125
+ throw error;
126
+ }
127
+ try {
128
+ await handle.start();
129
+ } catch (error) {
130
+ await reportStartupFailure(error, options.onStartupFailure);
131
+ try {
132
+ await handle.stop();
133
+ } catch (cleanupError) {
134
+ logger().error({
135
+ err: cleanupError,
136
+ startError: error
137
+ }, 'Failed to clean up API server startup');
138
+ }
139
+ throw error;
140
+ }
141
+ const stopAndExit = ()=>void handle.stop().finally(()=>process.exit(0));
142
+ process.once('SIGTERM', stopAndExit);
143
+ process.once('SIGINT', stopAndExit);
144
+ return handle;
145
+ }
146
+ async function reportStartupFailure(error, onStartupFailure) {
147
+ try {
148
+ await onStartupFailure?.(error);
149
+ } catch (reportingError) {
150
+ logger().error({
151
+ err: reportingError,
152
+ startError: error
153
+ }, 'Failed to report API server startup error');
154
+ }
155
+ }
156
+ async function runCleanupSteps(steps) {
157
+ const cleanupErrors = [];
158
+ for (const step of steps){
159
+ try {
160
+ await step();
161
+ } catch (error) {
162
+ cleanupErrors.push(error);
163
+ }
164
+ }
165
+ return cleanupErrors;
166
+ }
167
+ function throwCleanupErrors(cleanupErrors, message) {
168
+ if (cleanupErrors.length === 0) return;
169
+ if (cleanupErrors.length === 1) throw cleanupErrors[0];
170
+ throw new AggregateError(cleanupErrors, message);
171
+ }
172
+ async function handleModuleWorkerFailure(error, worker, onWorkerFailure) {
173
+ logger().error({
174
+ err: error,
175
+ taskQueue: worker.taskQueue
176
+ }, 'Module worker stopped unexpectedly');
177
+ captureException(error);
178
+ try {
179
+ await closeHttpServerAfterWorkerFailure();
180
+ await closeErrorMonitoring(ERROR_MONITORING_SHUTDOWN_TIMEOUT_MS);
181
+ } finally{
182
+ await onWorkerFailure?.(error, worker);
183
+ }
184
+ }
185
+ async function closeHttpServerAfterWorkerFailure() {
186
+ let timeoutId;
187
+ const timeout = new Promise((resolve)=>{
188
+ timeoutId = setTimeout(()=>resolve('timeout'), WORKER_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS);
189
+ });
190
+ try {
191
+ const result = await Promise.race([
192
+ closeApp().then(()=>'closed'),
193
+ timeout
194
+ ]).finally(()=>{
195
+ if (timeoutId) {
196
+ clearTimeout(timeoutId);
197
+ }
198
+ });
199
+ if (result === 'timeout') {
200
+ logger().error({
201
+ timeoutMs: WORKER_FAILURE_HTTP_SHUTDOWN_TIMEOUT_MS
202
+ }, 'Timed out closing HTTP server after worker failure');
203
+ }
204
+ } catch (error) {
205
+ logger().error({
206
+ err: error
207
+ }, 'Failed to close HTTP server after worker failure');
208
+ } finally{
209
+ if (timeoutId) {
210
+ clearTimeout(timeoutId);
211
+ }
212
+ }
213
+ }
214
+
215
+ //# sourceMappingURL=server.js.map