@harperfast/vite 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/dist/auth.js ADDED
@@ -0,0 +1,174 @@
1
+ import { databases } from 'harper';
2
+ import { isDevMode } from "./options.js";
3
+ /**
4
+ * Harper's canonical "is this a super_user" check. Matches how Harper itself gates privileged paths
5
+ * (e.g. impersonation, token minting): the resolved user carries `role.permission.super_user === true`.
6
+ */
7
+ export function isSuperUser(user) {
8
+ return user?.role?.permission?.super_user === true;
9
+ }
10
+ /**
11
+ * A Connect middleware that restricts the chain behind it (the Vite dev server) to Harper `super_user`s,
12
+ * authenticated via HTTP Basic auth. It runs the check itself against Harper's APIs:
13
+ *
14
+ * - A super_user Harper already resolved (`req.user`, bridged onto the node request by `registerHttp` —
15
+ * e.g. an authenticated admin) passes straight through.
16
+ * - A loopback peer under `authorizeLocal` (the default in `harper dev`) also passes, authorized here the
17
+ * same way the HMR WebSocket gate does it (`isUpgradeAuthorized`): Harper applies `authorizeLocal` to
18
+ * MQTT (and its Bun HTTP inject path) but NOT the Node HTTP middleware layer this runs in, so `req.user`
19
+ * is unset for a plain loopback `harper dev` request — we trust the real socket peer ourselves rather
20
+ * than relying on Harper to pre-set it. So local development passes straight through with no prompt.
21
+ * - Otherwise we validate an `Authorization: Basic` header directly with `server.authenticateUser` and, on
22
+ * failure, reply `401` with a `WWW-Authenticate: Basic` header so a browser prompts for credentials.
23
+ *
24
+ * NOTE: this guards the HTTP surface — the Vite dev server's asset, module-transform and `/@fs/`
25
+ * (arbitrary file read) endpoints, which is the dangerous part. Vite's HMR *WebSocket* runs on its own
26
+ * port and is not routed through here; keep it bound to localhost (or otherwise unexposed) in any
27
+ * non-local deployment.
28
+ */
29
+ export function superUserAuth(scope, realm) {
30
+ return (req, res, next) => {
31
+ // Fast path: a super_user Harper already resolved (any scheme), or a loopback peer under
32
+ // authorizeLocal — authorized here the same way the HMR WebSocket gate is (see isUpgradeAuthorized),
33
+ // because Harper doesn't resolve authorizeLocal for the Node HTTP middleware layer this runs in.
34
+ if (isSuperUser(req.user) || authorizeLocalAllows(req))
35
+ return next();
36
+ authenticateBasic(scope, req)
37
+ .then((user) => {
38
+ if (isSuperUser(user)) {
39
+ req.user = user; // surface the authenticated user to the rest of the chain
40
+ return next();
41
+ }
42
+ challenge(res, realm);
43
+ })
44
+ .catch(next);
45
+ };
46
+ }
47
+ /** Validate an `Authorization: Basic` header against Harper's user store; `undefined` when absent/invalid. */
48
+ async function authenticateBasic(scope, req) {
49
+ const header = req.headers?.authorization;
50
+ if (typeof header !== 'string' || !header.startsWith('Basic '))
51
+ return undefined;
52
+ const decoded = Buffer.from(header.slice('Basic '.length), 'base64').toString('utf8');
53
+ const separator = decoded.indexOf(':');
54
+ if (separator === -1)
55
+ return undefined; // malformed — no `username:password` pair
56
+ const username = decoded.slice(0, separator);
57
+ const password = decoded.slice(separator + 1);
58
+ try {
59
+ // `authenticateUser` always validates the password. (`getUser` skips it for a null password — the
60
+ // certificate-auth path — so it must not be used here.) The third arg is the request, which Harper
61
+ // uses only for contextual strategies (mTLS, etc.); it's unused for direct credential validation.
62
+ // Bad credentials throw or resolve to a non-super_user; either way the caller falls through to the
63
+ // 401 challenge.
64
+ return await scope.server?.authenticateUser?.(username, password, req);
65
+ }
66
+ catch {
67
+ return undefined;
68
+ }
69
+ }
70
+ /**
71
+ * Whether a WebSocket upgrade request may reach the Vite dev server — i.e. whether it represents a Harper
72
+ * `super_user`.
73
+ *
74
+ * The HMR WebSocket is served on Harper's own port (see `setupDevelopment`), so the same super_user gate as
75
+ * the HTTP surface should apply. But Harper runs its auth layer on the HTTP request chain, NOT the upgrade
76
+ * chain — `req.user` is unset here — so we authorize the raw upgrade ourselves, from the three signals a real
77
+ * client carries on a same-origin upgrade, cheapest first:
78
+ *
79
+ * 1. A loopback peer under `authorizeLocal` — mirrors how Harper trusts localhost for the HTTP surface, so
80
+ * plain `harper dev` works with no prompt (the upgrade carries neither a Basic header nor a cookie, and
81
+ * `authorizeLocal` sets no cookie). We trust the real socket peer, not a spoofable `X-Forwarded-For`.
82
+ * 2. An `Authorization: Basic` header, validated exactly like the HTTP gate. Browsers seldom attach this to
83
+ * a WebSocket handshake, but CLI clients (and some browsers) do.
84
+ * 3. The `hdb-session` cookie Harper sets after any successful login (sessions are on by default). Cookies
85
+ * ARE reliably sent on a same-origin upgrade, so this is the path that carries a logged-in admin's
86
+ * identity from the page to a remotely-exposed HMR socket.
87
+ *
88
+ * Best-effort and fails closed: a missing global, store, or lookup error yields `false` (the upgrade is then
89
+ * refused), never an unauthenticated pass.
90
+ */
91
+ export async function isUpgradeAuthorized(scope, req) {
92
+ if (authorizeLocalAllows(req))
93
+ return true;
94
+ if (isSuperUser(await authenticateBasic(scope, req)))
95
+ return true;
96
+ if (isSuperUser(await authenticateSessionCookie(scope, req)))
97
+ return true;
98
+ return false;
99
+ }
100
+ /** True when the upgrade's real socket peer is loopback and Harper's `authorizeLocal` trust is in effect. */
101
+ function authorizeLocalAllows(req) {
102
+ if (!authorizeLocalEnabled())
103
+ return false;
104
+ const ip = req?.socket?.remoteAddress ?? '';
105
+ // Match Harper's own loopback test (covers `127.0.0.1`, IPv4-mapped `::ffff:127.0.0.1`, and IPv6 `::1`).
106
+ return ip.includes('127.0.0.') || ip === '::1';
107
+ }
108
+ /**
109
+ * Mirror the env-visible part of Harper's `authorizeLocal` resolution: an explicit `AUTHENTICATION_AUTHORIZELOCAL`
110
+ * override wins, otherwise it defaults to dev mode (what `harper dev` sets). Harper also consults its config
111
+ * file, which a plugin can't read here — so a config-file-only override isn't reflected; document accordingly.
112
+ */
113
+ function authorizeLocalEnabled() {
114
+ const explicit = process.env.AUTHENTICATION_AUTHORIZELOCAL;
115
+ if (explicit != null)
116
+ return explicit !== 'false' && explicit !== '0' && explicit !== '';
117
+ return isDevMode();
118
+ }
119
+ /**
120
+ * Resolve the user named by a Harper `hdb-session` cookie on the request, or `undefined`. Harper stores
121
+ * sessions in `system.hdb_session` keyed by the id carried in the cookie; the cookie NAME is origin-prefixed
122
+ * (e.g. `localhost_9926-hdb-session`), so we match any cookie whose name ends in `hdb-session` rather than
123
+ * reconstructing Harper's exact prefix. Resolving a user from a session id needs no password — the id is the
124
+ * proof — which is why this uses `getUser` (and why `authenticateBasic`, validating a password, must not).
125
+ */
126
+ async function authenticateSessionCookie(scope, req) {
127
+ const cookieHeader = req?.headers?.cookie;
128
+ if (typeof cookieHeader !== 'string' || cookieHeader.length === 0)
129
+ return undefined;
130
+ // `system.hdb_session` is an internal Harper table; feature-detect it so this stays a no-op outside Harper
131
+ // (unit tests) or on a host that doesn't expose it.
132
+ const sessionStore = databases?.system?.hdb_session;
133
+ if (typeof sessionStore?.get !== 'function')
134
+ return undefined;
135
+ for (const id of sessionIds(cookieHeader)) {
136
+ try {
137
+ const session = await sessionStore.get(id);
138
+ const username = session?.user;
139
+ if (typeof username !== 'string' || username.length === 0)
140
+ continue;
141
+ const user = await scope.server?.getUser?.(username, null, req);
142
+ if (isSuperUser(user))
143
+ return user;
144
+ }
145
+ catch {
146
+ // Unreadable session / store error — try the next candidate, else fall through to undefined.
147
+ }
148
+ }
149
+ return undefined;
150
+ }
151
+ /** Session ids from every `*hdb-session` cookie in a Cookie header (a request can carry more than one). */
152
+ function sessionIds(cookieHeader) {
153
+ const ids = [];
154
+ for (const pair of cookieHeader.split(/;\s*/)) {
155
+ const eq = pair.indexOf('=');
156
+ if (eq === -1)
157
+ continue;
158
+ const name = pair.slice(0, eq).trim();
159
+ if (name === 'hdb-session' || name.endsWith('-hdb-session')) {
160
+ const value = pair.slice(eq + 1).trim();
161
+ if (value)
162
+ ids.push(value);
163
+ }
164
+ }
165
+ return ids;
166
+ }
167
+ /** Send a `401` asking the browser to collect Basic credentials. */
168
+ function challenge(res, realm) {
169
+ res.statusCode = 401;
170
+ res.setHeader('WWW-Authenticate', `Basic realm="${realm}", charset="UTF-8"`);
171
+ res.setHeader('Content-Type', 'text/plain; charset=utf-8');
172
+ res.end('401 Unauthorized — super_user credentials required.\n');
173
+ }
174
+ //# sourceMappingURL=auth.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,10 @@
1
+ import { type Scope } from 'harper';
2
+ /**
3
+ * Run `build` once across the workers sharing this app's build-info record. The worker that claims the
4
+ * record runs the build; others wait until it finishes and return without building. Resolves once the
5
+ * build is complete (whether performed by this worker or another), so callers can safely refresh from the
6
+ * output afterward.
7
+ *
8
+ * Outside Harper (no `databases` global), it simply runs `build`.
9
+ */
10
+ export declare function withBuildLock(scope: Scope, build: () => Promise<void>): Promise<void>;
@@ -0,0 +1,58 @@
1
+ import { databases } from 'harper';
2
+ import { setTimeout as sleep } from 'node:timers/promises';
3
+ import { log } from "./log.js";
4
+ // Harper runs `handleApplication` in every worker thread, so without coordination each worker would run
5
+ // its own `vite build` concurrently into the same output directory. We coordinate with a shared Harper
6
+ // table (defined in schema.graphql): a worker claims the per-app record with status "building" before
7
+ // compiling, and other workers see it and wait rather than building in parallel. This mirrors the
8
+ // `@harperfast/nextjs` build-info pattern and works across threads and processes that share the database.
9
+ const DATABASE = 'harperfast_vite';
10
+ const TABLE = 'vite_build_info';
11
+ const STALE_MS = 5 * 60 * 1000; // a "building" record older than this is treated as abandoned (crashed build)
12
+ const POLL_MS = 150;
13
+ const WAIT_TIMEOUT_MS = 5 * 60 * 1000;
14
+ /** The build-info table, or undefined when running outside Harper (e.g. unit tests). */
15
+ function buildInfoTable() {
16
+ return databases?.[DATABASE]?.[TABLE];
17
+ }
18
+ /** True while another worker holds a fresh "building" claim on this app. */
19
+ function heldByOther(info) {
20
+ return info?.status === 'building' && Date.now() - info.getUpdatedTime() < STALE_MS;
21
+ }
22
+ /**
23
+ * Run `build` once across the workers sharing this app's build-info record. The worker that claims the
24
+ * record runs the build; others wait until it finishes and return without building. Resolves once the
25
+ * build is complete (whether performed by this worker or another), so callers can safely refresh from the
26
+ * output afterward.
27
+ *
28
+ * Outside Harper (no `databases` global), it simply runs `build`.
29
+ */
30
+ export async function withBuildLock(scope, build) {
31
+ const table = buildInfoTable();
32
+ if (!table) {
33
+ await build();
34
+ return;
35
+ }
36
+ const key = scope.appName;
37
+ // If another worker is already building, wait for it to finish and then skip — it produced the output.
38
+ if (heldByOther(await table.get(key))) {
39
+ log(scope, 'debug', 'another worker is building; waiting for it to finish');
40
+ const start = Date.now();
41
+ while (Date.now() - start < WAIT_TIMEOUT_MS) {
42
+ await sleep(POLL_MS);
43
+ if (!heldByOther(await table.get(key)))
44
+ return;
45
+ }
46
+ log(scope, 'warn', 'timed out waiting for another worker to finish building');
47
+ return;
48
+ }
49
+ // Claim the build. Other workers will observe "building" and wait above.
50
+ await table.put(key, { status: 'building' });
51
+ try {
52
+ await build();
53
+ }
54
+ finally {
55
+ await table.put(key, { status: 'idle' });
56
+ }
57
+ }
58
+ //# sourceMappingURL=buildLock.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"buildLock.js","sourceRoot":"","sources":["../src/buildLock.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAc,MAAM,QAAQ,CAAC;AAC/C,OAAO,EAAE,UAAU,IAAI,KAAK,EAAE,MAAM,sBAAsB,CAAC;AAC3D,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,wGAAwG;AACxG,uGAAuG;AACvG,sGAAsG;AACtG,kGAAkG;AAClG,0GAA0G;AAE1G,MAAM,QAAQ,GAAG,iBAAiB,CAAC;AACnC,MAAM,KAAK,GAAG,iBAAiB,CAAC;AAChC,MAAM,QAAQ,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,CAAC,8EAA8E;AAC9G,MAAM,OAAO,GAAG,GAAG,CAAC;AACpB,MAAM,eAAe,GAAG,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC;AAEtC,wFAAwF;AACxF,SAAS,cAAc;IACtB,OAAO,SAAS,EAAE,CAAC,QAAQ,CAAC,EAAE,CAAC,KAAK,CAAC,CAAC;AACvC,CAAC;AAED,4EAA4E;AAC5E,SAAS,WAAW,CAAC,IAAS;IAC7B,OAAO,IAAI,EAAE,MAAM,KAAK,UAAU,IAAI,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,CAAC,cAAc,EAAE,GAAG,QAAQ,CAAC;AACrF,CAAC;AAED;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CAAC,KAAY,EAAE,KAA0B;IAC3E,MAAM,KAAK,GAAG,cAAc,EAAE,CAAC;IAC/B,IAAI,CAAC,KAAK,EAAE,CAAC;QACZ,MAAM,KAAK,EAAE,CAAC;QACd,OAAO;IACR,CAAC;IAED,MAAM,GAAG,GAAG,KAAK,CAAC,OAAO,CAAC;IAE1B,uGAAuG;IACvG,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC;QACvC,GAAG,CAAC,KAAK,EAAE,OAAO,EAAE,sDAAsD,CAAC,CAAC;QAC5E,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC;QACzB,OAAO,IAAI,CAAC,GAAG,EAAE,GAAG,KAAK,GAAG,eAAe,EAAE,CAAC;YAC7C,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;YACrB,IAAI,CAAC,WAAW,CAAC,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;gBAAE,OAAO;QAChD,CAAC;QACD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,yDAAyD,CAAC,CAAC;QAC9E,OAAO;IACR,CAAC;IAED,yEAAyE;IACzE,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,UAAU,EAAE,CAAC,CAAC;IAC7C,IAAI,CAAC;QACJ,MAAM,KAAK,EAAE,CAAC;IACf,CAAC;YAAS,CAAC;QACV,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC,CAAC;IAC1C,CAAC;AACF,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Scope } from 'harper';
2
+ export declare const HMR_PATH = "/@harper-vite-hmr";
3
+ /**
4
+ * Development mode: a Vite dev server in middleware mode with HMR.
5
+ * - SPA: Vite serves `index.html` and assets; everything else falls through to Harper.
6
+ * - SSR: Vite serves assets; HTML navigations are rendered on the fly via `ssrLoadModule`.
7
+ */
8
+ export declare function setupDevelopment(scope: Scope, ssrEntry?: string): Promise<void>;
@@ -0,0 +1,139 @@
1
+ import { createServer as createBridgeServer } from 'node:http';
2
+ import { join } from 'node:path';
3
+ import { readFileSync } from 'node:fs';
4
+ import { viteWrapper } from "./wrappers.js";
5
+ import { acceptsHtml, chain, registerHttp, registerShutdown } from "./http.js";
6
+ import { superUserAuth, isUpgradeAuthorized } from "./auth.js";
7
+ import { log } from "./log.js";
8
+ // The HMR WebSocket is served on this path on Harper's own port (not Vite's default standalone port 24678),
9
+ // so the whole dev surface — assets, module transforms, AND the HMR socket — sits on one origin behind one
10
+ // super_user gate. Both our upgrade gate and Vite key off this path; any other upgrade falls through to
11
+ // Harper untouched. The `@`-prefix mirrors Vite's own internal route convention (`/@fs`, `/@vite`).
12
+ export const HMR_PATH = '/@harper-vite-hmr';
13
+ /**
14
+ * Development mode: a Vite dev server in middleware mode with HMR.
15
+ * - SPA: Vite serves `index.html` and assets; everything else falls through to Harper.
16
+ * - SSR: Vite serves assets; HTML navigations are rendered on the fly via `ssrLoadModule`.
17
+ */
18
+ export async function setupDevelopment(scope, ssrEntry) {
19
+ const root = scope.directory;
20
+ // Prefer routing HMR through Harper's port so the WebSocket is gated like everything else (below). That
21
+ // needs Harper's `upgrade` hook (Harper >= 5). If the host is older, fall back to Vite's own WebSocket on
22
+ // a separate port — still functional, but ungated, so it must be kept on localhost.
23
+ const canGateWebSocket = typeof scope.server?.upgrade === 'function';
24
+ // A never-listening HTTP server used purely as the target Vite attaches its WebSocket upgrade handler to
25
+ // (`hmr.server`). Our gate forwards authenticated upgrades to it; it opens no port of its own.
26
+ const hmrBridge = canGateWebSocket ? createBridgeServer() : undefined;
27
+ const server = await viteWrapper.createServer({
28
+ root,
29
+ // Omit `configFile` so Vite auto-resolves vite.config.{js,ts,mjs,...} from the root.
30
+ server: {
31
+ middlewareMode: true,
32
+ hmr: hmrBridge ? { server: hmrBridge, path: HMR_PATH } : true,
33
+ // Every request to the dev server — HTTP and the HMR upgrade — is gated as super_user, so Vite's
34
+ // Host-header allowlist (a DNS-rebinding defense) is redundant here. Allowing all hosts is what lets
35
+ // HMR be enabled against a deployed instance (e.g. a cloud IDE) where Host isn't localhost; local
36
+ // dev is unchanged.
37
+ allowedHosts: true,
38
+ },
39
+ appType: ssrEntry ? 'custom' : 'spa',
40
+ });
41
+ // The Vite dev server exposes powerful endpoints (on-the-fly module transforms, arbitrary file reads
42
+ // via `/@fs/`). Run a super_user Basic-auth check ahead of it in the middleware chain so HMR can't be
43
+ // reached by unauthenticated clients if the dev server is ever exposed beyond localhost. Local dev is
44
+ // unaffected: Harper auto-authorizes loopback requests as super_user under `authorizeLocal`.
45
+ const authenticate = superUserAuth(scope, 'Harper Vite dev server (HMR)');
46
+ const vite = ssrEntry
47
+ ? renderSsr(server, root, ssrEntry.startsWith('/') ? ssrEntry : `/${ssrEntry}`)
48
+ : (req, res, next) => server.middlewares(req, res, next);
49
+ registerHttp(scope, chain(authenticate, vite));
50
+ if (hmrBridge) {
51
+ gateHmrWebSocket(scope, hmrBridge);
52
+ }
53
+ else {
54
+ log(scope, 'warn', 'host Harper exposes no WebSocket upgrade hook; HMR uses a separate, ungated port — keep it bound to localhost');
55
+ }
56
+ registerShutdown(scope, () => server.close());
57
+ log(scope, 'info', `dev server started with HMR (${ssrEntry ? 'SSR' : 'SPA'}); super_user auth required${hmrBridge ? ' for HTTP + WebSocket' : ''}`);
58
+ }
59
+ /**
60
+ * Gate the HMR WebSocket behind the same super_user check as the HTTP surface.
61
+ *
62
+ * Registered as a run-first Harper `upgrade` handler, it claims only upgrades on `HMR_PATH` and lets every
63
+ * other upgrade fall through to Harper. Harper does not run its auth layer on the upgrade chain, so we
64
+ * authenticate the raw request ourselves (see `authenticateUpgrade`); an authenticated super_user's upgrade
65
+ * is handed to Vite via the bridge, and anyone else gets a `401` and the socket closed.
66
+ *
67
+ * Harper only wires its upgrade chain to the socket once some WebSocket consumer is registered, so we also
68
+ * register a pass-through `ws` handler to guarantee the chain is active even when the app has no other one
69
+ * (e.g. no `rest`). It forwards every (non-HMR) connection on to the real handlers untouched.
70
+ */
71
+ function gateHmrWebSocket(scope, hmrBridge) {
72
+ const server = scope.server;
73
+ server.upgrade(async (request, socket, head, next) => {
74
+ // On the upgrade chain `request` is the raw Node IncomingMessage (carrying `.url`/`.headers`);
75
+ // tolerate a Harper Request wrapper too.
76
+ const req = request?._nodeRequest ?? request;
77
+ const path = String(req?.url ?? '').split('?', 1)[0];
78
+ if (path !== HMR_PATH)
79
+ return next(request, socket, head); // not an HMR upgrade — leave it for Harper
80
+ try {
81
+ if (await isUpgradeAuthorized(scope, req)) {
82
+ // Hand the authorized upgrade to Vite's listener (attached to the bridge via `hmr.server`),
83
+ // which performs the handshake. We do NOT call `next`, so Harper won't also upgrade this socket.
84
+ hmrBridge.emit('upgrade', req, socket, head);
85
+ return;
86
+ }
87
+ }
88
+ catch (e) {
89
+ log(scope, 'debug', 'HMR WebSocket auth error; refusing upgrade:', e);
90
+ }
91
+ refuseUpgrade(socket);
92
+ }, { runFirst: true });
93
+ // Ensure Harper actually wires its upgrade chain to the socket (it only does so once a `ws` consumer
94
+ // exists). Pure pass-through: forward every connection to the next handler so `rest`/`mqtt` are unaffected.
95
+ // (`next` is a 4th argument Harper passes at runtime that its published types omit, hence optional + any.)
96
+ server.ws?.((ws, request, completion, next) => next?.(ws, request, completion), {
97
+ subProtocol: 'harper-vite-hmr',
98
+ });
99
+ }
100
+ /** Refuse a WebSocket upgrade: emit a minimal `401` and close the socket. */
101
+ function refuseUpgrade(socket) {
102
+ try {
103
+ socket.write('HTTP/1.1 401 Unauthorized\r\nConnection: close\r\nContent-Length: 0\r\n\r\n');
104
+ }
105
+ catch {
106
+ // The socket may already be gone.
107
+ }
108
+ socket.destroy?.();
109
+ }
110
+ /**
111
+ * SSR middleware: Vite serves assets; for HTML navigations it transforms `index.html`, loads the server
112
+ * entry via `ssrLoadModule` (so edits are reflected immediately) and injects the render into the outlet.
113
+ */
114
+ function renderSsr(server, root, ssrUrl) {
115
+ return (req, res, next) => {
116
+ server.middlewares(req, res, async (err) => {
117
+ if (err)
118
+ return next(err);
119
+ if (!acceptsHtml(req))
120
+ return next();
121
+ try {
122
+ const raw = readFileSync(join(root, 'index.html'), 'utf-8');
123
+ const template = await server.transformIndexHtml(req.url, raw);
124
+ const { render } = await server.ssrLoadModule(ssrUrl);
125
+ const appHtml = await render(req.url);
126
+ res.statusCode = 200;
127
+ res.setHeader('Content-Type', 'text/html');
128
+ // Function replacer: a literal string would let `$&`/`$\``/`$'`/`$$` sequences in the
129
+ // rendered markup be interpreted as replacement patterns and corrupt the document.
130
+ res.end(template.replace('<!--ssr-outlet-->', () => appHtml));
131
+ }
132
+ catch (e) {
133
+ server.ssrFixStacktrace(e);
134
+ next(e);
135
+ }
136
+ });
137
+ };
138
+ }
139
+ //# sourceMappingURL=development.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"development.js","sourceRoot":"","sources":["../src/development.ts"],"names":[],"mappings":"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"}
package/dist/http.d.ts ADDED
@@ -0,0 +1,18 @@
1
+ import type { Scope } from 'harper';
2
+ /** A Connect-style middleware: forwards to `next()` when it does not handle the request. */
3
+ export type Middleware = (req: any, res: any, next: (err?: unknown) => void) => void;
4
+ /**
5
+ * Compose Connect middlewares into one. Each runs only after the previous calls `next()` without an
6
+ * error; the first to handle the response (by not calling `next`) ends the chain. When all fall through,
7
+ * the composed middleware calls its own `next`. Lets us run, say, an auth check ahead of Vite.
8
+ */
9
+ export declare function chain(...middlewares: Middleware[]): Middleware;
10
+ /** A request that should receive an HTML document (SPA shell / SSR render) rather than falling through. */
11
+ export declare function acceptsHtml(req: any): boolean;
12
+ /**
13
+ * Register an HTTP handler with Harper that runs `middleware` first and, for any request the middleware
14
+ * does not handle, falls through to the next Harper layer (e.g. `rest: true` resources) via `nextLayer`.
15
+ */
16
+ export declare function registerHttp(scope: Scope, middleware: Middleware, options?: Record<string, unknown>): void;
17
+ /** Close the given instance both when the scope closes and when Harper broadcasts a shutdown message. */
18
+ export declare function registerShutdown(scope: Scope, close: () => unknown): void;
package/dist/http.js ADDED
@@ -0,0 +1,87 @@
1
+ import { parentPort } from 'node:worker_threads';
2
+ /**
3
+ * Compose Connect middlewares into one. Each runs only after the previous calls `next()` without an
4
+ * error; the first to handle the response (by not calling `next`) ends the chain. When all fall through,
5
+ * the composed middleware calls its own `next`. Lets us run, say, an auth check ahead of Vite.
6
+ */
7
+ export function chain(...middlewares) {
8
+ return (req, res, next) => {
9
+ let i = 0;
10
+ const advance = (err) => {
11
+ if (err)
12
+ return next(err);
13
+ const middleware = middlewares[i++];
14
+ if (!middleware)
15
+ return next();
16
+ middleware(req, res, advance);
17
+ };
18
+ advance();
19
+ };
20
+ }
21
+ /** A request that should receive an HTML document (SPA shell / SSR render) rather than falling through. */
22
+ export function acceptsHtml(req) {
23
+ const method = req.method ?? 'GET';
24
+ if (method !== 'GET' && method !== 'HEAD')
25
+ return false;
26
+ return String(req.headers?.accept ?? '').includes('text/html');
27
+ }
28
+ /**
29
+ * Register an HTTP handler with Harper that runs `middleware` first and, for any request the middleware
30
+ * does not handle, falls through to the next Harper layer (e.g. `rest: true` resources) via `nextLayer`.
31
+ */
32
+ export function registerHttp(scope, middleware, options) {
33
+ scope.server?.http?.((request, nextLayer) => new Promise((resolve, reject) => {
34
+ const res = request._nodeResponse;
35
+ // A middleware that handles the request writes the response directly and never calls `next`.
36
+ // Resolve once the response completes so Harper's awaited handler doesn't accumulate a pending
37
+ // promise (and its captured req/res) per handled request — a leak under load. Resolving
38
+ // `undefined` is exactly the "already handled via the node response" signal Harper looks for,
39
+ // so it won't double-send; the fall-through path below resolves with the next layer instead.
40
+ const onComplete = () => {
41
+ cleanup();
42
+ resolve(undefined);
43
+ };
44
+ const cleanup = () => {
45
+ res?.removeListener?.('finish', onComplete);
46
+ res?.removeListener?.('close', onComplete);
47
+ };
48
+ res?.on?.('finish', onComplete);
49
+ res?.on?.('close', onComplete);
50
+ // Bridge Harper's resolved identity onto the node request so a node-level middleware can run
51
+ // its own auth check (e.g. the dev server's super_user gate). Harper itself does this for
52
+ // fall-through requests (`request._nodeRequest.user = request.user`); we do it up front.
53
+ request._nodeRequest.user = request.user;
54
+ middleware(request._nodeRequest, res, (err) => {
55
+ cleanup();
56
+ if (err)
57
+ return reject(err);
58
+ try {
59
+ resolve(nextLayer(request));
60
+ }
61
+ catch (e) {
62
+ reject(e);
63
+ }
64
+ });
65
+ }), options);
66
+ }
67
+ /** Close the given instance both when the scope closes and when Harper broadcasts a shutdown message. */
68
+ export function registerShutdown(scope, close) {
69
+ // `scope`'s `close` event and a Harper `shutdown` message can both fire; run `close` exactly once and
70
+ // memoize its promise so every caller awaits the *same* teardown. Returning that promise from the
71
+ // `close` listener lets Harper's `scope.close()` await Vite's `server.close()` — i.e. wait for the
72
+ // rolldown dev-server runtime to be fully disposed before the worker exits. That ordering matters:
73
+ // tearing the worker down (exit or terminate) while rolldown's native runtime is still live crashes
74
+ // the whole process. The `shutdown` message starts the teardown as early as possible.
75
+ let closing;
76
+ const closeOnce = () => (closing ??= (async () => close())());
77
+ const shutdownHandler = (msg) => {
78
+ if (msg?.type === 'shutdown')
79
+ void closeOnce();
80
+ };
81
+ scope.on('close', () => {
82
+ parentPort?.off('message', shutdownHandler);
83
+ return closeOnce();
84
+ });
85
+ parentPort?.on('message', shutdownHandler);
86
+ }
87
+ //# sourceMappingURL=http.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"http.js","sourceRoot":"","sources":["../src/http.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,MAAM,qBAAqB,CAAC;AAKjD;;;;GAIG;AACH,MAAM,UAAU,KAAK,CAAC,GAAG,WAAyB;IACjD,OAAO,CAAC,GAAG,EAAE,GAAG,EAAE,IAAI,EAAE,EAAE;QACzB,IAAI,CAAC,GAAG,CAAC,CAAC;QACV,MAAM,OAAO,GAAG,CAAC,GAAa,EAAE,EAAE;YACjC,IAAI,GAAG;gBAAE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC;YAC1B,MAAM,UAAU,GAAG,WAAW,CAAC,CAAC,EAAE,CAAC,CAAC;YACpC,IAAI,CAAC,UAAU;gBAAE,OAAO,IAAI,EAAE,CAAC;YAC/B,UAAU,CAAC,GAAG,EAAE,GAAG,EAAE,OAAO,CAAC,CAAC;QAC/B,CAAC,CAAC;QACF,OAAO,EAAE,CAAC;IACX,CAAC,CAAC;AACH,CAAC;AAED,2GAA2G;AAC3G,MAAM,UAAU,WAAW,CAAC,GAAQ;IACnC,MAAM,MAAM,GAAG,GAAG,CAAC,MAAM,IAAI,KAAK,CAAC;IACnC,IAAI,MAAM,KAAK,KAAK,IAAI,MAAM,KAAK,MAAM;QAAE,OAAO,KAAK,CAAC;IACxD,OAAO,MAAM,CAAC,GAAG,CAAC,OAAO,EAAE,MAAM,IAAI,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAChE,CAAC;AAED;;;GAGG;AACH,MAAM,UAAU,YAAY,CAAC,KAAY,EAAE,UAAsB,EAAE,OAAiC;IACnG,KAAK,CAAC,MAAM,EAAE,IAAI,EAAE,CACnB,CAAC,OAAY,EAAE,SAAoC,EAAE,EAAE,CACtD,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;QAC/B,MAAM,GAAG,GAAG,OAAO,CAAC,aAAa,CAAC;QAElC,6FAA6F;QAC7F,+FAA+F;QAC/F,wFAAwF;QACxF,8FAA8F;QAC9F,6FAA6F;QAC7F,MAAM,UAAU,GAAG,GAAG,EAAE;YACvB,OAAO,EAAE,CAAC;YACV,OAAO,CAAC,SAAS,CAAC,CAAC;QACpB,CAAC,CAAC;QACF,MAAM,OAAO,GAAG,GAAG,EAAE;YACpB,GAAG,EAAE,cAAc,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;YAC5C,GAAG,EAAE,cAAc,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAC5C,CAAC,CAAC;QACF,GAAG,EAAE,EAAE,EAAE,CAAC,QAAQ,EAAE,UAAU,CAAC,CAAC;QAChC,GAAG,EAAE,EAAE,EAAE,CAAC,OAAO,EAAE,UAAU,CAAC,CAAC;QAE/B,6FAA6F;QAC7F,0FAA0F;QAC1F,yFAAyF;QACzF,OAAO,CAAC,YAAY,CAAC,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC;QACzC,UAAU,CAAC,OAAO,CAAC,YAAY,EAAE,GAAG,EAAE,CAAC,GAAa,EAAE,EAAE;YACvD,OAAO,EAAE,CAAC;YACV,IAAI,GAAG;gBAAE,OAAO,MAAM,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,CAAC;gBACJ,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;YAC7B,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACZ,MAAM,CAAC,CAAC,CAAC,CAAC;YACX,CAAC;QACF,CAAC,CAAC,CAAC;IACJ,CAAC,CAAC,EACH,OAAO,CACP,CAAC;AACH,CAAC;AAED,yGAAyG;AACzG,MAAM,UAAU,gBAAgB,CAAC,KAAY,EAAE,KAAoB;IAClE,sGAAsG;IACtG,kGAAkG;IAClG,mGAAmG;IACnG,mGAAmG;IACnG,oGAAoG;IACpG,sFAAsF;IACtF,IAAI,OAAqC,CAAC;IAC1C,MAAM,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC,OAAO,KAAK,CAAC,KAAK,IAAI,EAAE,CAAC,KAAK,EAAE,CAAC,EAAE,CAAC,CAAC;IAE9D,MAAM,eAAe,GAAG,CAAC,GAAQ,EAAE,EAAE;QACpC,IAAI,GAAG,EAAE,IAAI,KAAK,UAAU;YAAE,KAAK,SAAS,EAAE,CAAC;IAChD,CAAC,CAAC;IAEF,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,GAAG,EAAE;QACtB,UAAU,EAAE,GAAG,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;QAC5C,OAAO,SAAS,EAAE,CAAC;IACpB,CAAC,CAAC,CAAC;IAEH,UAAU,EAAE,EAAE,CAAC,SAAS,EAAE,eAAe,CAAC,CAAC;AAC5C,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { Scope } from 'harper';
2
+ export { viteWrapper } from './wrappers.ts';
3
+ /**
4
+ * Harper extension entry point. Runs the Vite dev server (HMR) or the hybrid-production build based on the
5
+ * `hmr` option (defaulting to Harper's `DEV_MODE`), then hands off to the matching setup. SSR is enabled
6
+ * when the `ssr` option points at a server entry.
7
+ */
8
+ export declare function handleApplication(scope: Scope): Promise<void>;
package/dist/index.js ADDED
@@ -0,0 +1,23 @@
1
+ import { normalizeSsrEntry, resolveHmr } from "./options.js";
2
+ import { setupDevelopment } from "./development.js";
3
+ import { setupProduction } from "./production.js";
4
+ import { log } from "./log.js";
5
+ // Re-export the mockable wrappers so tests (and consumers) can reach them from the entry point.
6
+ export { viteWrapper } from "./wrappers.js";
7
+ /**
8
+ * Harper extension entry point. Runs the Vite dev server (HMR) or the hybrid-production build based on the
9
+ * `hmr` option (defaulting to Harper's `DEV_MODE`), then hands off to the matching setup. SSR is enabled
10
+ * when the `ssr` option points at a server entry.
11
+ */
12
+ export async function handleApplication(scope) {
13
+ const ssrEntry = normalizeSsrEntry(scope.options?.get?.(['ssr']));
14
+ const hmr = resolveHmr(scope.options?.get?.(['hmr']));
15
+ log(scope, 'info', `handling '${scope.appName}' in ${hmr ? 'development (HMR)' : 'production'} mode`);
16
+ if (hmr) {
17
+ await setupDevelopment(scope, ssrEntry);
18
+ }
19
+ else {
20
+ await setupProduction(scope, ssrEntry);
21
+ }
22
+ }
23
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,iBAAiB,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACpD,OAAO,EAAE,eAAe,EAAE,MAAM,iBAAiB,CAAC;AAClD,OAAO,EAAE,GAAG,EAAE,MAAM,UAAU,CAAC;AAE/B,gGAAgG;AAChG,OAAO,EAAE,WAAW,EAAE,MAAM,eAAe,CAAC;AAE5C;;;;GAIG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,KAAY;IACnD,MAAM,QAAQ,GAAG,iBAAiB,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAClE,MAAM,GAAG,GAAG,UAAU,CAAC,KAAK,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;IAEtD,GAAG,CAAC,KAAK,EAAE,MAAM,EAAE,aAAa,KAAK,CAAC,OAAO,QAAQ,GAAG,CAAC,CAAC,CAAC,mBAAmB,CAAC,CAAC,CAAC,YAAY,OAAO,CAAC,CAAC;IAEtG,IAAI,GAAG,EAAE,CAAC;QACT,MAAM,gBAAgB,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACzC,CAAC;SAAM,CAAC;QACP,MAAM,eAAe,CAAC,KAAK,EAAE,QAAQ,CAAC,CAAC;IACxC,CAAC;AACF,CAAC"}
package/dist/log.d.ts ADDED
@@ -0,0 +1,9 @@
1
+ import type { Scope } from 'harper';
2
+ type Level = 'debug' | 'info' | 'warn' | 'error';
3
+ /**
4
+ * Log through Harper's scoped logger with a consistent prefix. Logger methods are optional, so this is a
5
+ * no-op for any level the host logger doesn't implement. Note Harper's default log level is `warn`, so
6
+ * set `logging.level` to `info` (or `debug`) to see build/rebuild diagnostics.
7
+ */
8
+ export declare function log(scope: Scope, level: Level, message: string, ...args: unknown[]): void;
9
+ export {};
package/dist/log.js ADDED
@@ -0,0 +1,10 @@
1
+ const PREFIX = '[@harperfast/vite]';
2
+ /**
3
+ * Log through Harper's scoped logger with a consistent prefix. Logger methods are optional, so this is a
4
+ * no-op for any level the host logger doesn't implement. Note Harper's default log level is `warn`, so
5
+ * set `logging.level` to `info` (or `debug`) to see build/rebuild diagnostics.
6
+ */
7
+ export function log(scope, level, message, ...args) {
8
+ scope.logger[level]?.(`${PREFIX} ${message}`, ...args);
9
+ }
10
+ //# sourceMappingURL=log.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"log.js","sourceRoot":"","sources":["../src/log.ts"],"names":[],"mappings":"AAEA,MAAM,MAAM,GAAG,oBAAoB,CAAC;AAIpC;;;;GAIG;AACH,MAAM,UAAU,GAAG,CAAC,KAAY,EAAE,KAAY,EAAE,OAAe,EAAE,GAAG,IAAe;IAClF,KAAK,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,GAAG,MAAM,IAAI,OAAO,EAAE,EAAE,GAAG,IAAI,CAAC,CAAC;AACxD,CAAC"}
@@ -0,0 +1,19 @@
1
+ /** Harper sets `DEV_MODE` when started with `harper dev`. */
2
+ export declare function isDevMode(): boolean;
3
+ /**
4
+ * Whether to run the Vite dev server with HMR (vs. the hybrid production build).
5
+ *
6
+ * Controlled by the `hmr` option when it's an explicit boolean; otherwise defaults to Harper's dev mode
7
+ * (the `DEV_MODE` env, set by `harper dev`).
8
+ */
9
+ export declare function resolveHmr(hmrOption: unknown): boolean;
10
+ /** Normalize the optional `ssr` config value (path to the server entry) to a non-empty string or undefined. */
11
+ export declare function normalizeSsrEntry(value: unknown): string | undefined;
12
+ /**
13
+ * The build output directory (the `output` option), relative to the app root. This is where the plugin
14
+ * builds the client and what the `static` plugin should serve. Defaults to `dist` (matching Vite's own
15
+ * default), with any trailing slash trimmed.
16
+ */
17
+ export declare function resolveOutput(value: unknown): string;
18
+ /** True when `files` is configured (string, non-empty array, or object), meaning we should watch for rebuilds. */
19
+ export declare function hasFilesOption(value: unknown): boolean;