@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.
- package/.turbo/turbo-build.log +2 -0
- package/.turbo/turbo-type$colon$emit.log +1 -0
- package/.turbo/turbo-type.log +1 -0
- package/CHANGELOG.md +35 -0
- package/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/config.d.ts +9 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +43 -0
- package/dist/config.js.map +1 -0
- package/dist/e2e.d.ts +10 -0
- package/dist/e2e.d.ts.map +1 -0
- package/dist/e2e.js +42 -0
- package/dist/e2e.js.map +1 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/instrumentation.d.ts +2 -0
- package/dist/instrumentation.d.ts.map +1 -0
- package/dist/instrumentation.js +13 -0
- package/dist/instrumentation.js.map +1 -0
- package/dist/modules.d.ts +3 -0
- package/dist/modules.d.ts.map +1 -0
- package/dist/modules.js +76 -0
- package/dist/modules.js.map +1 -0
- package/dist/server.d.ts +16 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +215 -0
- package/dist/server.js.map +1 -0
- package/dist/tsconfig.test.tsbuildinfo +1 -0
- package/package.json +80 -0
- package/src/config.test.ts +33 -0
- package/src/config.ts +54 -0
- package/src/e2e.test.ts +101 -0
- package/src/e2e.ts +61 -0
- package/src/index.ts +3 -0
- package/src/instrumentation.ts +8 -0
- package/src/modules.test.ts +137 -0
- package/src/modules.ts +90 -0
- package/src/server.test.ts +539 -0
- package/src/server.ts +242 -0
- package/tsconfig.build.json +9 -0
- package/tsconfig.build.tsbuildinfo +1 -0
- package/tsconfig.json +4 -0
- package/tsconfig.test.json +8 -0
- package/vitest.config.ts +3 -0
|
@@ -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
|
package/dist/config.d.ts
ADDED
|
@@ -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
|
package/dist/e2e.js.map
ADDED
|
@@ -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"}
|
package/dist/index.d.ts
ADDED
|
@@ -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 @@
|
|
|
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 @@
|
|
|
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 @@
|
|
|
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"}
|
package/dist/modules.js
ADDED
|
@@ -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"}
|
package/dist/server.d.ts
ADDED
|
@@ -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
|