@molecule/api-server-default-express 1.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/LICENSE ADDED
@@ -0,0 +1,115 @@
1
+ Apache License
2
+ Version 2.0, January 2004
3
+ http://www.apache.org/licenses/
4
+
5
+ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6
+
7
+ 1. Definitions.
8
+
9
+ "License" shall mean the terms and conditions for use, reproduction,
10
+ and distribution as defined by Sections 1 through 9 of this document.
11
+
12
+ "Licensor" shall mean the copyright owner or entity authorized by
13
+ the copyright owner that is granting the License.
14
+
15
+ "Legal Entity" shall mean the union of the acting entity and all
16
+ other entities that control, are controlled by, or are under common
17
+ control with that entity. For the purposes of this definition,
18
+ "control" means (i) the power, direct or indirect, to cause the
19
+ direction or management of such entity, whether by contract or
20
+ otherwise, or (ii) ownership of fifty percent (50%) or more of the
21
+ outstanding shares, or (iii) beneficial ownership of such entity.
22
+
23
+ "You" (or "Your") shall mean an individual or Legal Entity
24
+ exercising permissions granted by this License.
25
+
26
+ "Source" form shall mean the preferred form for making modifications,
27
+ including but not limited to software source code, documentation
28
+ source, and configuration files.
29
+
30
+ "Object" form shall mean any form resulting from mechanical
31
+ transformation or translation of a Source form, including but
32
+ not limited to compiled object code, generated documentation,
33
+ and conversions to other media types.
34
+
35
+ "Work" shall mean the work of authorship, whether in Source or
36
+ Object form, made available under the License, as indicated by a
37
+ copyright notice that is included in or attached to the work.
38
+
39
+ "Derivative Works" shall mean any work, whether in Source or Object
40
+ form, that is based on (or derived from) the Work and for which the
41
+ editorial revisions, annotations, elaborations, or other modifications
42
+ represent, as a whole, an original work of authorship.
43
+
44
+ "Contribution" shall mean any work of authorship, including the
45
+ original version of the Work and any modifications or additions
46
+ to that Work, that is intentionally submitted to the Licensor for
47
+ inclusion in the Work by the copyright owner or by an individual or
48
+ Legal Entity authorized to submit on behalf of the copyright owner.
49
+
50
+ "Contributor" shall mean Licensor and any individual or Legal Entity
51
+ on behalf of whom a Contribution has been received by the Licensor and
52
+ subsequently incorporated within the Work.
53
+
54
+ 2. Grant of Copyright License. Subject to the terms and conditions of
55
+ this License, each Contributor hereby grants to You a perpetual,
56
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
57
+ copyright license to reproduce, prepare Derivative Works of,
58
+ publicly display, publicly perform, sublicense, and distribute the
59
+ Work and such Derivative Works in Source or Object form.
60
+
61
+ 3. Grant of Patent License. Subject to the terms and conditions of
62
+ this License, each Contributor hereby grants to You a perpetual,
63
+ worldwide, non-exclusive, no-charge, royalty-free, irrevocable
64
+ patent license to make, have made, use, offer to sell, sell, import,
65
+ and otherwise transfer the Work.
66
+
67
+ 4. Redistribution. You may reproduce and distribute copies of the
68
+ Work or Derivative Works thereof in any medium, with or without
69
+ modifications, and in Source or Object form, provided that You
70
+ meet the following conditions:
71
+
72
+ (a) You must give any other recipients of the Work or
73
+ Derivative Works a copy of this License; and
74
+
75
+ (b) You must cause any modified files to carry prominent notices
76
+ stating that You changed the files; and
77
+
78
+ (c) You must retain, in the Source form of any Derivative Works
79
+ that You distribute, all copyright, patent, trademark, and
80
+ attribution notices from the Source form of the Work,
81
+ excluding those notices that do not pertain to any part of
82
+ the Derivative Works; and
83
+
84
+ (d) If the Work includes a "NOTICE" text file as part of its
85
+ distribution, then any Derivative Works that You distribute must
86
+ include a readable copy of the attribution notices contained
87
+ within such NOTICE file.
88
+
89
+ 5. Submission of Contributions.
90
+
91
+ 6. Trademarks. This License does not grant permission to use the trade
92
+ names, trademarks, service marks, or product names of the Licensor.
93
+
94
+ 7. Disclaimer of Warranty. Unless required by applicable law or
95
+ agreed to in writing, Licensor provides the Work on an "AS IS" BASIS,
96
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND.
97
+
98
+ 8. Limitation of Liability. In no event and under no legal theory shall
99
+ any Contributor be liable to You for damages.
100
+
101
+ 9. Accepting Warranty or Additional Liability.
102
+
103
+ Copyright 2026 Molecule Dev, Inc.
104
+
105
+ Licensed under the Apache License, Version 2.0 (the "License");
106
+ you may not use this file except in compliance with the License.
107
+ You may obtain a copy of the License at
108
+
109
+ http://www.apache.org/licenses/LICENSE-2.0
110
+
111
+ Unless required by applicable law or agreed to in writing, software
112
+ distributed under the License is distributed on an "AS IS" BASIS,
113
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
114
+ See the License for the specific language governing permissions and
115
+ limitations under the License.
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=browser-guard.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"browser-guard.d.ts","sourceRoot":"","sources":["../src/browser-guard.ts"],"names":[],"mappings":"AAuBA,OAAO,EAAE,CAAA"}
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Browser guard — `@molecule/api-server-default-express` is SERVER-ONLY.
3
+ *
4
+ * Generated by scripts/gen-browser-guards.mjs (workspace root) — edit THAT, not this.
5
+ * Evaluating a server package in a browser bundle is always an import-graph mistake
6
+ * (node APIs, secrets); without this guard it surfaces as a cryptic downstream crash
7
+ * ("Buffer is not defined") far from the culprit. Throwing here names the package and
8
+ * the fix at the exact moment the client bundle evaluates it. jsdom tests and SSR are
9
+ * unaffected: the throw requires browser globals AND the absence of a node runtime.
10
+ */
11
+ const g = globalThis;
12
+ if (g.window !== undefined && g.document !== undefined && !g.process?.versions?.node) {
13
+ throw new Error('@molecule/api-server-default-express is SERVER-ONLY: it was bundled into browser/client code. Import it only ' +
14
+ 'from server code (a server route/function or your API), or dynamic-import it inside ' +
15
+ 'the server handler — never from components or shared client modules, and never ' +
16
+ 'polyfill Buffer/process to silence this.');
17
+ }
18
+ export {};
@@ -0,0 +1,36 @@
1
+ /**
2
+ * `@molecule/api-server-default-express` — drop-in Express server
3
+ * factory used by the molecule fleet's `api/src/server.ts`.
4
+ *
5
+ * `createServerFactory({ setupBonds, runMigrations, getRouter })`
6
+ * returns a `(port?) => Promise<server>` function that runs
7
+ * migrations, wires bonds, mounts router + middleware, and starts
8
+ * an HTTP (or self-signed HTTPS for local dev) listener.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createServerFactory } from '@molecule/api-server-default-express'
13
+ *
14
+ * // In your app's api/src/server.ts these come from the scaffolded files:
15
+ * // import { setupBonds } from './bonds/index.js'
16
+ * // import { runMigrations } from './scripts/migrate.js'
17
+ * const create = createServerFactory({
18
+ * setupBonds,
19
+ * runMigrations,
20
+ * // Router loads lazily AFTER setupBonds() so bond-conditional route
21
+ * // maps see fully-registered providers. The module must export
22
+ * // `router`. In your app: getRouter: () => import('./App/router.js')
23
+ * getRouter: async () => ({ router }),
24
+ * })
25
+ *
26
+ * // Runs migrations → wires bonds → mounts middleware + router at /api →
27
+ * // listens on PORT (default 4000). The scaffolded server.ts exports
28
+ * // `create` and invokes it when the file is run directly.
29
+ * await create()
30
+ * ```
31
+ *
32
+ * @module
33
+ */
34
+ export * from './browser-guard.js';
35
+ export * from './server.js';
36
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAEH,cAAc,oBAAoB,CAAA;AAClC,cAAc,aAAa,CAAA"}
package/dist/index.js ADDED
@@ -0,0 +1,35 @@
1
+ /**
2
+ * `@molecule/api-server-default-express` — drop-in Express server
3
+ * factory used by the molecule fleet's `api/src/server.ts`.
4
+ *
5
+ * `createServerFactory({ setupBonds, runMigrations, getRouter })`
6
+ * returns a `(port?) => Promise<server>` function that runs
7
+ * migrations, wires bonds, mounts router + middleware, and starts
8
+ * an HTTP (or self-signed HTTPS for local dev) listener.
9
+ *
10
+ * @example
11
+ * ```ts
12
+ * import { createServerFactory } from '@molecule/api-server-default-express'
13
+ *
14
+ * // In your app's api/src/server.ts these come from the scaffolded files:
15
+ * // import { setupBonds } from './bonds/index.js'
16
+ * // import { runMigrations } from './scripts/migrate.js'
17
+ * const create = createServerFactory({
18
+ * setupBonds,
19
+ * runMigrations,
20
+ * // Router loads lazily AFTER setupBonds() so bond-conditional route
21
+ * // maps see fully-registered providers. The module must export
22
+ * // `router`. In your app: getRouter: () => import('./App/router.js')
23
+ * getRouter: async () => ({ router }),
24
+ * })
25
+ *
26
+ * // Runs migrations → wires bonds → mounts middleware + router at /api →
27
+ * // listens on PORT (default 4000). The scaffolded server.ts exports
28
+ * // `create` and invokes it when the file is run directly.
29
+ * await create()
30
+ * ```
31
+ *
32
+ * @module
33
+ */
34
+ export * from './browser-guard.js';
35
+ export * from './server.js';
@@ -0,0 +1,148 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import express from 'express';
4
+ /** A deliberately-tagged molecule error mapped to a real HTTP status by the API. */
5
+ export interface TaggedError {
6
+ /** HTTP status to return (e.g. 503 for a missing provider config). */
7
+ statusCode: number;
8
+ /** Machine-readable key the app/IDE maps to a friendly message. */
9
+ errorKey: string;
10
+ /** Human-readable message. */
11
+ message: string;
12
+ }
13
+ /**
14
+ * Classify a thrown value for the API error middleware. Returns a {@link TaggedError}
15
+ * ONLY for values deliberately tagged by molecule with BOTH a numeric `statusCode`
16
+ * AND a string `errorKey` — e.g. a provider's config-missing throw (`statusCode: 503`,
17
+ * `errorKey: 'config.notConfigured'`). These are expected, actionable conditions a
18
+ * user must resolve (a missing `STRIPE_SECRET_KEY` is theirs to set, not a server bug),
19
+ * so the middleware surfaces the real status + errorKey instead of an opaque 500 — the
20
+ * app/IDE can then show "configure X to enable this feature".
21
+ *
22
+ * Requiring BOTH fields is deliberate: it keeps arbitrary library errors that merely
23
+ * carry a `.statusCode` (e.g. an AWS SDK error) from being silently surfaced with a
24
+ * status molecule never chose. Returns `null` for everything else (→ default 500 path).
25
+ *
26
+ * @param error - The thrown value caught by the error middleware.
27
+ * @returns The classified tagged error, or `null` if it isn't a molecule-tagged error.
28
+ */
29
+ export declare function classifyTaggedError(error: unknown): TaggedError | null;
30
+ /**
31
+ * Terminal Express error middleware for the canonical molecule fleet server.
32
+ *
33
+ * Resolves a thrown value to exactly one of three sanitized responses and NEVER
34
+ * delegates to Express's built-in `finalhandler`:
35
+ *
36
+ * 1. Bare-string `Unauthorized` / `Unauthorized.` → `401` with the string body
37
+ * (so authSelf-style middleware routes to 401 instead of a 500 page).
38
+ * 2. A deliberately-tagged molecule error ({@link classifyTaggedError}) → its real
39
+ * `statusCode` + `{ error, errorKey }` JSON (expected, user-actionable config
40
+ * conditions, e.g. a missing `STRIPE_SECRET_KEY` → 503 `config.notConfigured`).
41
+ * 3. EVERYTHING else (untagged library throws, null derefs, driver errors) → a
42
+ * generic `500 { error: 'Internal Server Error' }`, logged server-side AND
43
+ * reported to the bonded error tracker (`@molecule/api-error-tracking`'s
44
+ * `captureException`, a documented no-op when no tracker is bonded).
45
+ *
46
+ * Only case 3 is captured: cases 1–2 (401s, tagged config-missing 503s, and any
47
+ * other tagged 4xx/5xx) are expected, user-actionable conditions — not defects —
48
+ * so reporting them would drown real faults in noise.
49
+ *
50
+ * Case 3 is the security-critical branch: it is safe-by-construction and does NOT
51
+ * depend on `NODE_ENV`. Calling `next(error)` here would fall through to Express's
52
+ * `finalhandler`, which embeds `err.stack` in the HTTP response body whenever
53
+ * `app.get('env') !== 'production'` (the default when `NODE_ENV` is unset or
54
+ * `development`), disclosing absolute server paths, module layout, dependency
55
+ * versions, and query/data fragments. Returning the opaque 500 unconditionally
56
+ * removes that leak for every flagship app regardless of how it is deployed.
57
+ *
58
+ * @param error - The thrown value caught by Express.
59
+ * @param req - The request (used only as capture context for error tracking).
60
+ * @param res - The response to write the sanitized error to.
61
+ * @param _next - The next function (intentionally never called for untagged errors).
62
+ */
63
+ export declare const errorMiddleware: express.ErrorRequestHandler;
64
+ /** Options for `createServerFactory`. */
65
+ export interface CreateServerOptions {
66
+ /** App-specific bond wiring (resolves secrets + wires providers). */
67
+ setupBonds: () => Promise<void>;
68
+ /** DB migration runner (typically the `createMigrator()`-bound function). */
69
+ runMigrations: () => Promise<void>;
70
+ /**
71
+ * Lazy router import. Loaded AFTER `setupBonds()` so bond-conditional
72
+ * route maps see fully-registered providers at module-evaluation time.
73
+ */
74
+ getRouter: () => Promise<{
75
+ router: express.Router;
76
+ }>;
77
+ /**
78
+ * Optional hook to mount middleware AFTER cors+cookieParser but
79
+ * BEFORE the body parser. Use this for routes that need their own
80
+ * multipart streaming (file uploads via busboy) — the body parser's
81
+ * `files: 0` config would silently consume the multipart stream.
82
+ */
83
+ preBodyParser?: (app: express.Express) => Promise<void> | void;
84
+ /**
85
+ * Optional hook called after `setupBonds()` but before the router
86
+ * import. Use for additional one-shot setup (e.g. entitlements
87
+ * tier-registry registration that runs after the bonds are wired).
88
+ */
89
+ postBondsSetup?: () => Promise<void> | void;
90
+ /**
91
+ * Optional hook to mount middleware on `/api` BEFORE the canonical
92
+ * `app.use('/api', router)` mount. Use for app-specific authed
93
+ * content handlers (`/api`-prefixed) that need to run before the
94
+ * resource router.
95
+ */
96
+ preApiRouter?: (app: express.Express) => Promise<void> | void;
97
+ }
98
+ /**
99
+ * Register a hook to run with the HTTP(S) server right before it listens.
100
+ * Typically called from a bond's setup (e.g. `setupRealtimeSocketio`) during
101
+ * `setupBonds()`, which runs earlier in `create()` than server construction.
102
+ *
103
+ * @param hook - Receives the real `http.Server`/`https.Server`.
104
+ */
105
+ export declare function registerServerCreatedHook(hook: (server: http.Server | https.Server) => void | Promise<void>): void;
106
+ /**
107
+ * Global browser-security headers applied to EVERY response (mounted before the
108
+ * routers in `createServerFactory`, mirroring the molecule.dev platform server).
109
+ *
110
+ * Defaults are conservative and framework-agnostic — no app-specific CSP source
111
+ * lists, just the clickjacking / MIME-sniffing / referrer baseline a JSON API
112
+ * should always ship:
113
+ *
114
+ * - `X-Content-Type-Options: nosniff` — stop MIME-type sniffing.
115
+ * - `X-Frame-Options: DENY` + `Content-Security-Policy: frame-ancestors 'none'`
116
+ * — anti-clickjacking. A generated app that intends to be embedded (iframe)
117
+ * can override these in its own middleware.
118
+ * - `X-XSS-Protection: 0` — disable the legacy, buggy XSS auditor (modern
119
+ * correct value; CSP is the real defense).
120
+ * - `Referrer-Policy: strict-origin-when-cross-origin` — don't leak full URLs
121
+ * cross-origin.
122
+ * - `Strict-Transport-Security` — production only (mirrors the platform server's
123
+ * `NODE_ENV` check) so local plain-HTTP dev isn't force-upgraded to HTTPS.
124
+ *
125
+ * @param _req - The request (unused).
126
+ * @param res - The response to set headers on.
127
+ * @param next - Express next.
128
+ */
129
+ export declare const securityHeadersMiddleware: express.RequestHandler;
130
+ /**
131
+ * Returns an Express server-creation function bound to the given
132
+ * setupBonds / runMigrations / router loaders. The returned `create`
133
+ * builds the canonical molecule fleet server:
134
+ *
135
+ * - Migrations run first, then bonds, then router import.
136
+ * - Global browser-security headers ({@link securityHeadersMiddleware})
137
+ * applied before any router (anti-clickjacking + nosniff + referrer baseline).
138
+ * - `bodyParser` / `cookieParser` / `cors` middleware applied.
139
+ * - Router mounted at `/api`.
140
+ * - `/health` endpoint with `{ status: 'ok', timestamp }`.
141
+ * - Bare-string `Unauthorized` / `Unauthorized.` errors normalized to 401.
142
+ * - HTTPS in dev if `process.env.HTTPS` is set, using self-signed
143
+ * certs from optional dependency `pem`.
144
+ * - `process.on('uncaughtException')` + `unhandledRejection` registered
145
+ * on first call (idempotent across multiple `create()` invocations).
146
+ */
147
+ export declare function createServerFactory(opts: CreateServerOptions): (port?: number) => Promise<express.Express | https.Server>;
148
+ //# sourceMappingURL=server.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.ts","sourceRoot":"","sources":["../src/server.ts"],"names":[],"mappings":"AAAA,OAAO,IAAI,MAAM,WAAW,CAAA;AAC5B,OAAO,KAAK,MAAM,YAAY,CAAA;AAG9B,OAAO,OAAO,MAAM,SAAS,CAAA;AAS7B,oFAAoF;AACpF,MAAM,WAAW,WAAW;IAC1B,sEAAsE;IACtE,UAAU,EAAE,MAAM,CAAA;IAClB,mEAAmE;IACnE,QAAQ,EAAE,MAAM,CAAA;IAChB,8BAA8B;IAC9B,OAAO,EAAE,MAAM,CAAA;CAChB;AAED;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,WAAW,GAAG,IAAI,CAetE;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,eAAO,MAAM,eAAe,EAAE,OAAO,CAAC,mBA6CrC,CAAA;AAED,yCAAyC;AACzC,MAAM,WAAW,mBAAmB;IAClC,qEAAqE;IACrE,UAAU,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAC/B,6EAA6E;IAC7E,aAAa,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAA;IAClC;;;OAGG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;QAAE,MAAM,EAAE,OAAO,CAAC,MAAM,CAAA;KAAE,CAAC,CAAA;IACpD;;;;;OAKG;IACH,aAAa,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC9D;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;IAC3C;;;;;OAKG;IACH,YAAY,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,CAAC,OAAO,KAAK,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAA;CAC9D;AAYD;;;;;;GAMG;AACH,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,CAAC,MAAM,EAAE,IAAI,CAAC,MAAM,GAAG,KAAK,CAAC,MAAM,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,GACjE,IAAI,CAEN;AAYD;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,eAAO,MAAM,yBAAyB,EAAE,OAAO,CAAC,cAU/C,CAAA;AAID;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,mBAAmB,CACjC,IAAI,EAAE,mBAAmB,GACxB,CAAC,IAAI,CAAC,EAAE,MAAM,KAAK,OAAO,CAAC,OAAO,CAAC,OAAO,GAAG,KAAK,CAAC,MAAM,CAAC,CAgH5D"}
package/dist/server.js ADDED
@@ -0,0 +1,294 @@
1
+ import http from 'node:http';
2
+ import https from 'node:https';
3
+ import process from 'node:process';
4
+ import express from 'express';
5
+ import { captureException } from '@molecule/api-error-tracking';
6
+ import { logger } from '@molecule/api-logger';
7
+ import { bodyParser as bodyParserMiddleware } from '@molecule/api-middleware-body-parser';
8
+ import { cookieParser as cookieParserMiddleware } from '@molecule/api-middleware-cookie-parser';
9
+ import { cors as corsMiddleware } from '@molecule/api-middleware-cors';
10
+ import { buildConfigReport, logConfigReport } from '@molecule/api-secrets';
11
+ /**
12
+ * Classify a thrown value for the API error middleware. Returns a {@link TaggedError}
13
+ * ONLY for values deliberately tagged by molecule with BOTH a numeric `statusCode`
14
+ * AND a string `errorKey` — e.g. a provider's config-missing throw (`statusCode: 503`,
15
+ * `errorKey: 'config.notConfigured'`). These are expected, actionable conditions a
16
+ * user must resolve (a missing `STRIPE_SECRET_KEY` is theirs to set, not a server bug),
17
+ * so the middleware surfaces the real status + errorKey instead of an opaque 500 — the
18
+ * app/IDE can then show "configure X to enable this feature".
19
+ *
20
+ * Requiring BOTH fields is deliberate: it keeps arbitrary library errors that merely
21
+ * carry a `.statusCode` (e.g. an AWS SDK error) from being silently surfaced with a
22
+ * status molecule never chose. Returns `null` for everything else (→ default 500 path).
23
+ *
24
+ * @param error - The thrown value caught by the error middleware.
25
+ * @returns The classified tagged error, or `null` if it isn't a molecule-tagged error.
26
+ */
27
+ export function classifyTaggedError(error) {
28
+ if (typeof error === 'object' &&
29
+ error !== null &&
30
+ typeof error.statusCode === 'number' &&
31
+ typeof error.errorKey === 'string') {
32
+ const e = error;
33
+ return {
34
+ statusCode: e.statusCode,
35
+ errorKey: e.errorKey,
36
+ message: typeof e.message === 'string' ? e.message : 'Error',
37
+ };
38
+ }
39
+ return null;
40
+ }
41
+ /**
42
+ * Terminal Express error middleware for the canonical molecule fleet server.
43
+ *
44
+ * Resolves a thrown value to exactly one of three sanitized responses and NEVER
45
+ * delegates to Express's built-in `finalhandler`:
46
+ *
47
+ * 1. Bare-string `Unauthorized` / `Unauthorized.` → `401` with the string body
48
+ * (so authSelf-style middleware routes to 401 instead of a 500 page).
49
+ * 2. A deliberately-tagged molecule error ({@link classifyTaggedError}) → its real
50
+ * `statusCode` + `{ error, errorKey }` JSON (expected, user-actionable config
51
+ * conditions, e.g. a missing `STRIPE_SECRET_KEY` → 503 `config.notConfigured`).
52
+ * 3. EVERYTHING else (untagged library throws, null derefs, driver errors) → a
53
+ * generic `500 { error: 'Internal Server Error' }`, logged server-side AND
54
+ * reported to the bonded error tracker (`@molecule/api-error-tracking`'s
55
+ * `captureException`, a documented no-op when no tracker is bonded).
56
+ *
57
+ * Only case 3 is captured: cases 1–2 (401s, tagged config-missing 503s, and any
58
+ * other tagged 4xx/5xx) are expected, user-actionable conditions — not defects —
59
+ * so reporting them would drown real faults in noise.
60
+ *
61
+ * Case 3 is the security-critical branch: it is safe-by-construction and does NOT
62
+ * depend on `NODE_ENV`. Calling `next(error)` here would fall through to Express's
63
+ * `finalhandler`, which embeds `err.stack` in the HTTP response body whenever
64
+ * `app.get('env') !== 'production'` (the default when `NODE_ENV` is unset or
65
+ * `development`), disclosing absolute server paths, module layout, dependency
66
+ * versions, and query/data fragments. Returning the opaque 500 unconditionally
67
+ * removes that leak for every flagship app regardless of how it is deployed.
68
+ *
69
+ * @param error - The thrown value caught by Express.
70
+ * @param req - The request (used only as capture context for error tracking).
71
+ * @param res - The response to write the sanitized error to.
72
+ * @param _next - The next function (intentionally never called for untagged errors).
73
+ */
74
+ export const errorMiddleware = (error, req, res, _next) => {
75
+ // Match both the bare `Unauthorized` string and the i18n-resolved
76
+ // `Unauthorized.` (with trailing period from the resource locale
77
+ // bond) so authSelf-style middleware reliably routes to 401 instead
78
+ // of the default Express 500 error page.
79
+ if (typeof error === 'string' && /^Unauthorized\.?$/.test(error)) {
80
+ if (!res.headersSent) {
81
+ res.status(401);
82
+ res.send(error);
83
+ }
84
+ return;
85
+ }
86
+ // Deliberately-tagged molecule errors (e.g. a provider config-missing throw:
87
+ // 503 + 'config.notConfigured') are expected, user-actionable conditions — not
88
+ // internal bugs. Surface the real status + errorKey so the app/IDE can render a
89
+ // clean "configure X to enable this feature" instead of an opaque 500.
90
+ const tagged = classifyTaggedError(error);
91
+ if (tagged) {
92
+ // 5xx here means a dependency/config isn't ready, not a server fault — warn,
93
+ // don't error (an error-level log would falsely read as an app defect).
94
+ if (tagged.statusCode >= 500) {
95
+ logger.warn(tagged.message, { errorKey: tagged.errorKey });
96
+ }
97
+ if (!res.headersSent) {
98
+ res.status(tagged.statusCode).json({ error: tagged.message, errorKey: tagged.errorKey });
99
+ }
100
+ return;
101
+ }
102
+ // Untagged error: a real internal fault. Log the full detail SERVER-SIDE only,
103
+ // then respond with a generic 500. We deliberately do NOT call `next(error)` —
104
+ // that would hand control to Express's finalhandler, which leaks `err.stack`
105
+ // into the response body unless NODE_ENV is exactly 'production'. Safe-by-
106
+ // construction: no stack, no paths, no NODE_ENV dependency.
107
+ logger.error(error);
108
+ // Report the genuine unexpected error to the bonded error tracker. A no-op
109
+ // when no tracker is bonded, and never throws — safe inside this terminal
110
+ // handler. The 401/tagged branches above are deliberately NOT captured.
111
+ // Method/URL only: headers and bodies can carry credentials.
112
+ captureException(error, {
113
+ tags: { source: 'express' },
114
+ request: { method: req.method, url: req.originalUrl },
115
+ });
116
+ if (!res.headersSent) {
117
+ res.status(500).json({ error: 'Internal Server Error' });
118
+ }
119
+ };
120
+ /**
121
+ * Generic server-lifecycle hooks invoked with the real HTTP(S) server once it
122
+ * has been created in `createServerFactory`, before it starts listening. Lets a
123
+ * bond that must bind to the HTTP server itself — e.g. a Socket.IO realtime
124
+ * provider sharing the API's port at `/socket.io/` — attach at the right moment,
125
+ * without the factory hardcoding any specific bond. Drained (run once then
126
+ * cleared) per `create()` so repeat calls (tests) don't accumulate stale hooks.
127
+ */
128
+ const serverCreatedHooks = [];
129
+ /**
130
+ * Register a hook to run with the HTTP(S) server right before it listens.
131
+ * Typically called from a bond's setup (e.g. `setupRealtimeSocketio`) during
132
+ * `setupBonds()`, which runs earlier in `create()` than server construction.
133
+ *
134
+ * @param hook - Receives the real `http.Server`/`https.Server`.
135
+ */
136
+ export function registerServerCreatedHook(hook) {
137
+ serverCreatedHooks.push(hook);
138
+ }
139
+ /**
140
+ * Run and drain all registered server-created hooks.
141
+ *
142
+ * @param server - The HTTP(S) server to hand to each hook.
143
+ */
144
+ async function runServerCreatedHooks(server) {
145
+ const hooks = serverCreatedHooks.splice(0);
146
+ for (const hook of hooks)
147
+ await hook(server);
148
+ }
149
+ /**
150
+ * Global browser-security headers applied to EVERY response (mounted before the
151
+ * routers in `createServerFactory`, mirroring the molecule.dev platform server).
152
+ *
153
+ * Defaults are conservative and framework-agnostic — no app-specific CSP source
154
+ * lists, just the clickjacking / MIME-sniffing / referrer baseline a JSON API
155
+ * should always ship:
156
+ *
157
+ * - `X-Content-Type-Options: nosniff` — stop MIME-type sniffing.
158
+ * - `X-Frame-Options: DENY` + `Content-Security-Policy: frame-ancestors 'none'`
159
+ * — anti-clickjacking. A generated app that intends to be embedded (iframe)
160
+ * can override these in its own middleware.
161
+ * - `X-XSS-Protection: 0` — disable the legacy, buggy XSS auditor (modern
162
+ * correct value; CSP is the real defense).
163
+ * - `Referrer-Policy: strict-origin-when-cross-origin` — don't leak full URLs
164
+ * cross-origin.
165
+ * - `Strict-Transport-Security` — production only (mirrors the platform server's
166
+ * `NODE_ENV` check) so local plain-HTTP dev isn't force-upgraded to HTTPS.
167
+ *
168
+ * @param _req - The request (unused).
169
+ * @param res - The response to set headers on.
170
+ * @param next - Express next.
171
+ */
172
+ export const securityHeadersMiddleware = (_req, res, next) => {
173
+ res.setHeader('X-Content-Type-Options', 'nosniff');
174
+ res.setHeader('X-Frame-Options', 'DENY');
175
+ res.setHeader('X-XSS-Protection', '0');
176
+ res.setHeader('Referrer-Policy', 'strict-origin-when-cross-origin');
177
+ res.setHeader('Content-Security-Policy', "frame-ancestors 'none'");
178
+ if (process.env.NODE_ENV === 'production') {
179
+ res.setHeader('Strict-Transport-Security', 'max-age=31536000; includeSubDomains; preload');
180
+ }
181
+ next();
182
+ };
183
+ let processHandlersRegistered = false;
184
+ /**
185
+ * Returns an Express server-creation function bound to the given
186
+ * setupBonds / runMigrations / router loaders. The returned `create`
187
+ * builds the canonical molecule fleet server:
188
+ *
189
+ * - Migrations run first, then bonds, then router import.
190
+ * - Global browser-security headers ({@link securityHeadersMiddleware})
191
+ * applied before any router (anti-clickjacking + nosniff + referrer baseline).
192
+ * - `bodyParser` / `cookieParser` / `cors` middleware applied.
193
+ * - Router mounted at `/api`.
194
+ * - `/health` endpoint with `{ status: 'ok', timestamp }`.
195
+ * - Bare-string `Unauthorized` / `Unauthorized.` errors normalized to 401.
196
+ * - HTTPS in dev if `process.env.HTTPS` is set, using self-signed
197
+ * certs from optional dependency `pem`.
198
+ * - `process.on('uncaughtException')` + `unhandledRejection` registered
199
+ * on first call (idempotent across multiple `create()` invocations).
200
+ */
201
+ export function createServerFactory(opts) {
202
+ return async (port = Number(process.env.PORT) || 4000) => {
203
+ if (!processHandlersRegistered) {
204
+ processHandlersRegistered = true;
205
+ // Genuine unexpected errors — log AND report to the bonded error tracker
206
+ // (captureException no-ops when no tracker is bonded and never throws).
207
+ process.on('uncaughtException', (error) => {
208
+ logger.error(error);
209
+ captureException(error, { tags: { source: 'uncaughtException' } });
210
+ });
211
+ process.on('unhandledRejection', (error) => {
212
+ logger.error(error);
213
+ captureException(error, { tags: { source: 'unhandledRejection' } });
214
+ });
215
+ }
216
+ await opts.runMigrations();
217
+ await opts.setupBonds();
218
+ if (opts.postBondsSetup)
219
+ await opts.postBondsSetup();
220
+ // Import router after bonds are set up — handler maps are built at
221
+ // module-evaluation time, so bond-conditional handlers need providers
222
+ // registered first.
223
+ const { router } = await opts.getRouter();
224
+ const app = express();
225
+ app.disable('x-powered-by');
226
+ // Browser-security headers on every response — mounted before any router so
227
+ // /api, /health and error responses all carry them (mirrors the platform
228
+ // server). Anti-clickjacking + nosniff + referrer baseline; HSTS in prod only.
229
+ app.use(securityHeadersMiddleware);
230
+ if (opts.preBodyParser) {
231
+ // Custom-ordering path: mount cors+cookie before custom hooks
232
+ // (e.g. multipart uploads) which run BEFORE the body parser
233
+ // would otherwise swallow their streams.
234
+ app.use(corsMiddleware);
235
+ app.use(cookieParserMiddleware);
236
+ await opts.preBodyParser(app);
237
+ app.use(bodyParserMiddleware);
238
+ }
239
+ else {
240
+ // Canonical ordering — body/cookie/cors stacked in that order.
241
+ app.use(bodyParserMiddleware);
242
+ app.use(cookieParserMiddleware);
243
+ app.use(corsMiddleware);
244
+ }
245
+ if (opts.preApiRouter)
246
+ await opts.preApiRouter(app);
247
+ app.use('/api', router);
248
+ app.get('/health', (_req, res) => {
249
+ res.json({ status: 'ok', timestamp: new Date().toISOString() });
250
+ });
251
+ // Terminal sanitizing error handler — safe-by-construction. Untagged errors
252
+ // get a generic 500 here and never reach Express's stack-leaking finalhandler.
253
+ app.use(errorMiddleware);
254
+ let server = app;
255
+ if (process.env.HTTPS) {
256
+ // Self-signed cert for local HTTPS dev. `pem` is an optional dep.
257
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
258
+ const pem = require('pem');
259
+ server = await new Promise((resolve, reject) => {
260
+ pem.createCertificate({ days: 1, selfSigned: true }, (error, keys) => {
261
+ if (error)
262
+ reject(error);
263
+ else
264
+ resolve(https.createServer({ key: keys.serviceKey, cert: keys.certificate }, app));
265
+ });
266
+ });
267
+ }
268
+ // The real HTTP(S) server that lifecycle-coupled bonds (e.g. a Socket.IO
269
+ // realtime provider) must bind to. In HTTP mode `server` is the Express app,
270
+ // whose underlying http.Server is otherwise created+discarded by
271
+ // `app.listen()`; create it explicitly so server-created hooks attach to the
272
+ // SAME server the API listens on (Socket.IO then serves `/socket.io/` on the
273
+ // API port). Hooks run before `listen()` so the upgrade handler is ready.
274
+ const httpServer = process.env.HTTPS
275
+ ? server
276
+ : http.createServer(app);
277
+ await runServerCreatedHooks(httpServer);
278
+ httpServer.listen(port, () => {
279
+ logger.info(`Started ${process.env.NODE_ENV} ${process.env.HTTPS ? 'HTTPS' : 'HTTP'} server on port ${port}.`);
280
+ // Boot-time configuration report: bonds self-register their secret
281
+ // definitions at import; log which are configured vs missing (each
282
+ // missing REQUIRED key warns with its description + setup URL) so a
283
+ // degraded integration is loud and actionable at startup — instead of
284
+ // surfacing later as a lazy 503 on the first request that needs it.
285
+ // Fire-and-forget: reporting must never delay or fail the boot.
286
+ buildConfigReport()
287
+ .then(logConfigReport)
288
+ .catch((error) => {
289
+ logger.debug('boot config report failed', { error });
290
+ });
291
+ });
292
+ return server;
293
+ };
294
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@molecule/api-server-default-express",
3
+ "version": "1.0.0",
4
+ "description": "Default Express server factory: bonds setup, DB migrations, body/cookie/cors middleware, /api router mount, /health endpoint, 401 normalization, optional HTTPS via self-signed pem certs. Extracts 80-line server.ts shipped by 79 fleet apps.",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "scripts": {
9
+ "build": "tsc",
10
+ "test": "vitest run",
11
+ "test:watch": "vitest"
12
+ },
13
+ "exports": {
14
+ ".": {
15
+ "types": "./dist/index.d.ts",
16
+ "import": "./dist/index.js"
17
+ }
18
+ },
19
+ "files": [
20
+ "dist"
21
+ ],
22
+ "keywords": [
23
+ "molecule",
24
+ "server",
25
+ "default",
26
+ "express"
27
+ ],
28
+ "license": "Apache-2.0",
29
+ "devDependencies": {
30
+ "@types/node": "26.1.2",
31
+ "typescript": "6.0.3",
32
+ "vitest": "4.1.10",
33
+ "@molecule/api-error-tracking": "1.0.0",
34
+ "@molecule/api-logger": "1.0.0",
35
+ "@molecule/api-middleware-body-parser": "1.0.0",
36
+ "@molecule/api-middleware-cookie-parser": "1.0.0",
37
+ "@molecule/api-middleware-cors": "1.0.0",
38
+ "express": "5.2.1",
39
+ "@types/express": "5.0.6"
40
+ },
41
+ "peerDependencies": {
42
+ "@molecule/api-error-tracking": "^1.0.0",
43
+ "@molecule/api-logger": "^1.0.0",
44
+ "@molecule/api-middleware-body-parser": "^1.0.0",
45
+ "@molecule/api-middleware-cookie-parser": "^1.0.0",
46
+ "@molecule/api-middleware-cors": "^1.0.0",
47
+ "@molecule/api-secrets": "^1.0.0",
48
+ "express": "^4.0.0 || ^5.0.0"
49
+ }
50
+ }