@repolore/server 0.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.d.ts +16 -0
- package/dist/auth.js +40 -0
- package/dist/auth.js.map +1 -0
- package/dist/config.d.ts +15 -0
- package/dist/config.js +9 -0
- package/dist/config.js.map +1 -0
- package/dist/index.d.ts +19 -0
- package/dist/index.js +53 -0
- package/dist/index.js.map +1 -0
- package/dist/routes/chat.d.ts +3 -0
- package/dist/routes/chat.js +8 -0
- package/dist/routes/chat.js.map +1 -0
- package/dist/routes/repos.d.ts +8 -0
- package/dist/routes/repos.js +58 -0
- package/dist/routes/repos.js.map +1 -0
- package/dist/routes/settings.d.ts +3 -0
- package/dist/routes/settings.js +8 -0
- package/dist/routes/settings.js.map +1 -0
- package/dist/sse.d.ts +10 -0
- package/dist/sse.js +45 -0
- package/dist/sse.js.map +1 -0
- package/package.json +54 -0
package/dist/auth.d.ts
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
import type { RagStore } from "@repolore/core";
|
|
3
|
+
/** Static bearer-token auth. `/health` and
|
|
4
|
+
* anything outside `PROTECTED_ROUTE_PREFIXES` are always open. `Settings.allowAnonymous`
|
|
5
|
+
* is checked per request (not cached at startup) so flipping it via `PUT /settings`
|
|
6
|
+
* takes effect immediately without a restart.
|
|
7
|
+
*
|
|
8
|
+
* Checked against `req.routeOptions.url` — the matched *route pattern* (e.g.
|
|
9
|
+
* `/repos/:id/manifest`) — rather than `req.url` (the raw request path). Fastify
|
|
10
|
+
* resolves routing before running `onRequest` hooks, so this is already populated
|
|
11
|
+
* here. Matching on the raw URL would be wrong: the web app's own client-side route
|
|
12
|
+
* for its repo detail page is also literally `/repos/:id` (react-router, not a server
|
|
13
|
+
* route) — a raw-URL prefix check would gate a browser hard-refresh on that page
|
|
14
|
+
* behind the API key even though no server API route matches it, when it should fall
|
|
15
|
+
* through to the SPA's `index.html` like any other unmatched path. */
|
|
16
|
+
export declare function createAuthHook(store: RagStore, apiKey: string | undefined): (req: FastifyRequest, reply: FastifyReply) => Promise<void>;
|
package/dist/auth.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const PUBLIC_PATHS = new Set(["/health"]);
|
|
2
|
+
/** The only registered *route patterns* (not raw request URLs — see below) that
|
|
3
|
+
* actually serve data/trigger actions the API key is meant to protect. Everything
|
|
4
|
+
* else — `/health`, unmatched paths, and (phase 13) the built web app's static
|
|
5
|
+
* assets/SPA fallback when `repolore serve` is used — is public, the same way a
|
|
6
|
+
* compiled JS/CSS bundle on any public website is public: it's not sensitive, and
|
|
7
|
+
* gating it would break the web GUI from ever loading with no `Authorization` header
|
|
8
|
+
* to send on its very first request. */
|
|
9
|
+
const PROTECTED_ROUTE_PREFIXES = ["/repos", "/settings"];
|
|
10
|
+
/** Static bearer-token auth. `/health` and
|
|
11
|
+
* anything outside `PROTECTED_ROUTE_PREFIXES` are always open. `Settings.allowAnonymous`
|
|
12
|
+
* is checked per request (not cached at startup) so flipping it via `PUT /settings`
|
|
13
|
+
* takes effect immediately without a restart.
|
|
14
|
+
*
|
|
15
|
+
* Checked against `req.routeOptions.url` — the matched *route pattern* (e.g.
|
|
16
|
+
* `/repos/:id/manifest`) — rather than `req.url` (the raw request path). Fastify
|
|
17
|
+
* resolves routing before running `onRequest` hooks, so this is already populated
|
|
18
|
+
* here. Matching on the raw URL would be wrong: the web app's own client-side route
|
|
19
|
+
* for its repo detail page is also literally `/repos/:id` (react-router, not a server
|
|
20
|
+
* route) — a raw-URL prefix check would gate a browser hard-refresh on that page
|
|
21
|
+
* behind the API key even though no server API route matches it, when it should fall
|
|
22
|
+
* through to the SPA's `index.html` like any other unmatched path. */
|
|
23
|
+
export function createAuthHook(store, apiKey) {
|
|
24
|
+
return async (req, reply) => {
|
|
25
|
+
if (PUBLIC_PATHS.has(req.url))
|
|
26
|
+
return;
|
|
27
|
+
const matchedRoute = req.routeOptions?.url;
|
|
28
|
+
if (!matchedRoute ||
|
|
29
|
+
!PROTECTED_ROUTE_PREFIXES.some((prefix) => matchedRoute.startsWith(prefix)))
|
|
30
|
+
return;
|
|
31
|
+
if ((await store.getSettings()).allowAnonymous)
|
|
32
|
+
return;
|
|
33
|
+
const header = req.headers.authorization;
|
|
34
|
+
const token = header?.startsWith("Bearer ") ? header.slice("Bearer ".length) : undefined;
|
|
35
|
+
if (!apiKey || token !== apiKey) {
|
|
36
|
+
await reply.code(401).send({ error: "Unauthorized" });
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
//# sourceMappingURL=auth.js.map
|
package/dist/auth.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"auth.js","sourceRoot":"","sources":["../src/auth.ts"],"names":[],"mappings":"AAGA,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC;AAC1C;;;;;;wCAMwC;AACxC,MAAM,wBAAwB,GAAG,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;AAEzD;;;;;;;;;;;;sEAYsE;AACtE,MAAM,UAAU,cAAc,CAAC,KAAe,EAAE,MAA0B;IACxE,OAAO,KAAK,EAAE,GAAmB,EAAE,KAAmB,EAAiB,EAAE;QACvE,IAAI,YAAY,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC;YAAE,OAAO;QACtC,MAAM,YAAY,GAAG,GAAG,CAAC,YAAY,EAAE,GAAG,CAAC;QAC3C,IACE,CAAC,YAAY;YACb,CAAC,wBAAwB,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,EAAE,CAAC,YAAY,CAAC,UAAU,CAAC,MAAM,CAAC,CAAC;YAE3E,OAAO;QACT,IAAI,CAAC,MAAM,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC,cAAc;YAAE,OAAO;QAEvD,MAAM,MAAM,GAAG,GAAG,CAAC,OAAO,CAAC,aAAa,CAAC;QACzC,MAAM,KAAK,GAAG,MAAM,EAAE,UAAU,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;QACzF,IAAI,CAAC,MAAM,IAAI,KAAK,KAAK,MAAM,EAAE,CAAC;YAChC,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,cAAc,EAAE,CAAC,CAAC;QACxD,CAAC;IACH,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/** Server-side config — deliberately separate from the CLI's client config:
|
|
2
|
+
* data directory, port, and the static API key live here; `mode`/`serverUrl`/
|
|
3
|
+
* `apiKey`-as-a-client-credential live in `@repolore/cli`.
|
|
4
|
+
*
|
|
5
|
+
* Cloud LLM provider API keys used to be plumbed through here too, but that was
|
|
6
|
+
* removed: `LocalRagStore` (which `buildServer` always constructs internally)
|
|
7
|
+
* resolves them itself via the shared `resolveProviderApiKey` from the
|
|
8
|
+
* dataDir-scoped secrets store + env vars, so there's nothing left for this config to
|
|
9
|
+
* carry — env vars still work exactly as before with zero code here. */
|
|
10
|
+
export interface ServerConfig {
|
|
11
|
+
port: number;
|
|
12
|
+
dataDir: string;
|
|
13
|
+
apiKey?: string;
|
|
14
|
+
}
|
|
15
|
+
export declare function readServerConfig(env?: NodeJS.ProcessEnv): ServerConfig;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { defaultDataDir } from "@repolore/core";
|
|
2
|
+
export function readServerConfig(env = process.env) {
|
|
3
|
+
return {
|
|
4
|
+
port: Number(env.PORT ?? 3000),
|
|
5
|
+
dataDir: env.REPOLORE_DATA_DIR ?? defaultDataDir(),
|
|
6
|
+
apiKey: env.REPOLORE_API_KEY,
|
|
7
|
+
};
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=config.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"config.js","sourceRoot":"","sources":["../src/config.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,cAAc,EAAE,MAAM,gBAAgB,CAAC;AAiBhD,MAAM,UAAU,gBAAgB,CAAC,MAAyB,OAAO,CAAC,GAAG;IACnE,OAAO;QACL,IAAI,EAAE,MAAM,CAAC,GAAG,CAAC,IAAI,IAAI,IAAI,CAAC;QAC9B,OAAO,EAAE,GAAG,CAAC,iBAAiB,IAAI,cAAc,EAAE;QAClD,MAAM,EAAE,GAAG,CAAC,gBAAgB;KAC7B,CAAC;AACJ,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { type FastifyInstance } from "fastify";
|
|
2
|
+
import { type RagStore } from "@repolore/core";
|
|
3
|
+
export { readServerConfig, type ServerConfig } from "./config.js";
|
|
4
|
+
export interface BuildServerOptions {
|
|
5
|
+
/** Inject a `RagStore` directly — used by tests to swap in a fake or a
|
|
6
|
+
* `LocalRagStore` pointed at a temp dir with mocked embed/chat clients. Defaults
|
|
7
|
+
* to a real `LocalRagStore` over `dataDir` (or the default `~/.repolore`). */
|
|
8
|
+
store?: RagStore;
|
|
9
|
+
dataDir?: string;
|
|
10
|
+
apiKey?: string;
|
|
11
|
+
/** Absolute path to `web`'s built `dist/` directory. When set (phase 13's
|
|
12
|
+
* `repolore serve`), the server also serves the built web app as static files —
|
|
13
|
+
* every non-API GET falls through to `index.html` (client-side routing) so the
|
|
14
|
+
* SPA's own router handles the path. Left unset on every other boot path (plain
|
|
15
|
+
* `repolore serve` isn't invoked), so a normal `server` start never registers this
|
|
16
|
+
* plugin or pays its cost. */
|
|
17
|
+
staticDir?: string;
|
|
18
|
+
}
|
|
19
|
+
export declare function buildServer(opts?: BuildServerOptions): FastifyInstance;
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { pathToFileURL } from "node:url";
|
|
2
|
+
import Fastify from "fastify";
|
|
3
|
+
import cors from "@fastify/cors";
|
|
4
|
+
import fastifyStatic from "@fastify/static";
|
|
5
|
+
import { createRagStore, defaultDataDir, loadRepoEnv } from "@repolore/core";
|
|
6
|
+
import { createAuthHook } from "./auth.js";
|
|
7
|
+
import { readServerConfig } from "./config.js";
|
|
8
|
+
import { registerChatRoutes } from "./routes/chat.js";
|
|
9
|
+
import { registerReposRoutes } from "./routes/repos.js";
|
|
10
|
+
import { registerSettingsRoutes } from "./routes/settings.js";
|
|
11
|
+
export { readServerConfig } from "./config.js";
|
|
12
|
+
export function buildServer(opts = {}) {
|
|
13
|
+
const dataDir = opts.dataDir ?? defaultDataDir();
|
|
14
|
+
const store = opts.store ?? createRagStore({ mode: "local", dataDir });
|
|
15
|
+
const app = Fastify();
|
|
16
|
+
// `web` (phase 8) runs on its own dev-server origin (Vite) and talks to this
|
|
17
|
+
// server cross-origin; the API key in the Authorization header is the real
|
|
18
|
+
// access control, so a permissive reflected origin is fine here.
|
|
19
|
+
void app.register(cors, { origin: true, methods: ["GET", "POST", "PUT", "DELETE"] });
|
|
20
|
+
app.addHook("onRequest", createAuthHook(store, opts.apiKey));
|
|
21
|
+
app.get("/health", async () => ({ status: "ok" }));
|
|
22
|
+
registerReposRoutes(app, store, { dataDir });
|
|
23
|
+
registerChatRoutes(app, store);
|
|
24
|
+
registerSettingsRoutes(app, store);
|
|
25
|
+
if (opts.staticDir) {
|
|
26
|
+
void app.register(fastifyStatic, { root: opts.staticDir });
|
|
27
|
+
// Client-side routing (react-router): any GET that didn't match an API route or
|
|
28
|
+
// an actual static asset falls through to `index.html`, letting the SPA's own
|
|
29
|
+
// router decide what to render. Non-GET requests (or a genuinely unknown API
|
|
30
|
+
// path) still get a plain 404 rather than HTML.
|
|
31
|
+
app.setNotFoundHandler((req, reply) => {
|
|
32
|
+
if (req.method !== "GET") {
|
|
33
|
+
void reply.code(404).send({ error: "Not found" });
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
void reply.sendFile("index.html", opts.staticDir);
|
|
37
|
+
});
|
|
38
|
+
}
|
|
39
|
+
return app;
|
|
40
|
+
}
|
|
41
|
+
const isMain = process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
42
|
+
if (isMain) {
|
|
43
|
+
loadRepoEnv();
|
|
44
|
+
const config = readServerConfig();
|
|
45
|
+
const app = buildServer({ dataDir: config.dataDir, apiKey: config.apiKey });
|
|
46
|
+
app.listen({ port: config.port }, (err) => {
|
|
47
|
+
if (err) {
|
|
48
|
+
app.log.error(err);
|
|
49
|
+
process.exit(1);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { streamSse } from "../sse.js";
|
|
2
|
+
export function registerChatRoutes(app, store) {
|
|
3
|
+
app.post("/repos/:id/chat", async (req, reply) => {
|
|
4
|
+
const { message, history } = req.body;
|
|
5
|
+
await streamSse(reply, store.chat(req.params.id, message, history ?? []), "token");
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=chat.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chat.js","sourceRoot":"","sources":["../../src/routes/chat.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AAOtC,MAAM,UAAU,kBAAkB,CAAC,GAAoB,EAAE,KAAe;IACtE,GAAG,CAAC,IAAI,CACN,iBAAiB,EACjB,KAAK,EAAE,GAAG,EAAE,KAAK,EAAE,EAAE;QACnB,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,GAAG,GAAG,CAAC,IAAI,CAAC;QACtC,MAAM,SAAS,CAAC,KAAK,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,MAAM,CAAC,EAAE,EAAE,OAAO,EAAE,OAAO,IAAI,EAAE,CAAC,EAAE,OAAO,CAAC,CAAC;IACrF,CAAC,CACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyInstance } from "fastify";
|
|
2
|
+
import { type RagStore } from "@repolore/core";
|
|
3
|
+
export interface ReposRouteOptions {
|
|
4
|
+
/** Server's data dir — used to scope temp clone dirs for git-URL indexing
|
|
5
|
+
* (phase 9), cleaned up after every attempt, success or failure. */
|
|
6
|
+
dataDir: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function registerReposRoutes(app: FastifyInstance, store: RagStore, opts: ReposRouteOptions): void;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { mkdirSync, mkdtempSync, rmSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { cloneRepo, isGitUrl, } from "@repolore/core";
|
|
4
|
+
import { streamSse } from "../sse.js";
|
|
5
|
+
export function registerReposRoutes(app, store, opts) {
|
|
6
|
+
app.post("/repos", async (req, reply) => {
|
|
7
|
+
const { source, ...rest } = req.body;
|
|
8
|
+
await streamSse(reply, indexRepoWithGitCloneSupport(store, source, rest, opts.dataDir), "progress");
|
|
9
|
+
});
|
|
10
|
+
app.get("/repos", async () => store.listRepos());
|
|
11
|
+
app.get("/repos/:id/manifest", async (req, reply) => {
|
|
12
|
+
const repos = await store.listRepos();
|
|
13
|
+
const repo = repos.find((r) => r.id === req.params.id);
|
|
14
|
+
if (!repo) {
|
|
15
|
+
reply.code(404);
|
|
16
|
+
return { message: `Repo '${req.params.id}' is not indexed.` };
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
fileHashes: repo.fileHashes,
|
|
20
|
+
isGit: repo.isGit,
|
|
21
|
+
lastIndexedCommit: repo.lastIndexedCommit,
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
app.delete("/repos/:id", async (req, reply) => {
|
|
25
|
+
await store.removeRepo(req.params.id);
|
|
26
|
+
reply.code(204);
|
|
27
|
+
});
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Phase 9's server-side git-URL clone path: if `source` is a git URL, clones it into
|
|
31
|
+
* a scratch dir under the server's data dir (so it's not left in the OS temp dir
|
|
32
|
+
* indefinitely), then runs the normal indexing pipeline against the clone with
|
|
33
|
+
* `sourceId: source` so the repo's identity is stable across clones. The clone dir
|
|
34
|
+
* is always removed afterward, success or failure. Local-path `source`s (either
|
|
35
|
+
* indexed directly on this machine, or phase 9's client-streamed-content payload)
|
|
36
|
+
* pass through unchanged — this wrapper only ever branches on git-URL shape, never
|
|
37
|
+
* on local-vs-remote mode (that distinction is `RemoteRagStore`'s job, not this
|
|
38
|
+
* route's).
|
|
39
|
+
*/
|
|
40
|
+
async function* indexRepoWithGitCloneSupport(store, source, opts, dataDir) {
|
|
41
|
+
if (!isGitUrl(source)) {
|
|
42
|
+
yield* store.indexRepo(source, opts);
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
const clonesRoot = join(dataDir, ".clones");
|
|
46
|
+
mkdirSync(clonesRoot, { recursive: true });
|
|
47
|
+
const cloneDir = mkdtempSync(join(clonesRoot, "clone-"));
|
|
48
|
+
yield { phase: "walk", filesProcessed: 0, totalFiles: 0, message: `cloning ${source}...` };
|
|
49
|
+
try {
|
|
50
|
+
await cloneRepo(source, cloneDir, { token: opts.deployToken });
|
|
51
|
+
const { deployToken: _deployToken, ...rest } = opts;
|
|
52
|
+
yield* store.indexRepo(cloneDir, { ...rest, sourceId: source });
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
rmSync(cloneDir, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=repos.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"repos.js","sourceRoot":"","sources":["../../src/routes/repos.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export function registerSettingsRoutes(app, store) {
|
|
2
|
+
app.get("/settings", async () => store.getSettings());
|
|
3
|
+
app.put("/settings", async (req) => {
|
|
4
|
+
await store.updateSettings(req.body);
|
|
5
|
+
return store.getSettings();
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
//# sourceMappingURL=settings.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"settings.js","sourceRoot":"","sources":["../../src/routes/settings.ts"],"names":[],"mappings":"AAGA,MAAM,UAAU,sBAAsB,CAAC,GAAoB,EAAE,KAAe;IAC1E,GAAG,CAAC,GAAG,CAAC,WAAW,EAAE,KAAK,IAAI,EAAE,CAAC,KAAK,CAAC,WAAW,EAAE,CAAC,CAAC;IAEtD,GAAG,CAAC,GAAG,CAA8B,WAAW,EAAE,KAAK,EAAE,GAAG,EAAE,EAAE;QAC9D,MAAM,KAAK,CAAC,cAAc,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACrC,OAAO,KAAK,CAAC,WAAW,EAAE,CAAC;IAC7B,CAAC,CAAC,CAAC;AACL,CAAC"}
|
package/dist/sse.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { FastifyReply } from "fastify";
|
|
2
|
+
/**
|
|
3
|
+
* Streams an `AsyncGenerator` over SSE: one `event: <eventName>` per yielded value,
|
|
4
|
+
* a terminal `event: done` on normal completion, or `event: error` (JSON
|
|
5
|
+
* `{ message }` payload) if the generator throws mid-stream. The error case matters
|
|
6
|
+
* because the HTTP status (200) and headers are already committed by the time
|
|
7
|
+
* streaming starts — a mid-stream failure has no other way to signal itself to the
|
|
8
|
+
* client than an in-band event, which is what `RemoteRagStore` looks for.
|
|
9
|
+
*/
|
|
10
|
+
export declare function streamSse<T>(reply: FastifyReply, source: AsyncGenerator<T>, eventName: string): Promise<void>;
|
package/dist/sse.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Streams an `AsyncGenerator` over SSE: one `event: <eventName>` per yielded value,
|
|
3
|
+
* a terminal `event: done` on normal completion, or `event: error` (JSON
|
|
4
|
+
* `{ message }` payload) if the generator throws mid-stream. The error case matters
|
|
5
|
+
* because the HTTP status (200) and headers are already committed by the time
|
|
6
|
+
* streaming starts — a mid-stream failure has no other way to signal itself to the
|
|
7
|
+
* client than an in-band event, which is what `RemoteRagStore` looks for.
|
|
8
|
+
*/
|
|
9
|
+
export async function streamSse(reply, source, eventName) {
|
|
10
|
+
// `@fastify/cors`'s hook runs before the route handler, so by this point it has
|
|
11
|
+
// already queued CORS response headers (Access-Control-Allow-Origin, etc.) onto
|
|
12
|
+
// `reply` — but `reply.hijack()` tells Fastify to stop managing the response
|
|
13
|
+
// entirely, so those headers are never written unless we copy them over ourselves.
|
|
14
|
+
// Without this, every SSE response (index/chat progress) is CORS-blocked in a real
|
|
15
|
+
// browser even though the preflight OPTIONS request (which isn't hijacked) succeeds.
|
|
16
|
+
const corsHeaders = reply.getHeaders();
|
|
17
|
+
reply.hijack();
|
|
18
|
+
const res = reply.raw;
|
|
19
|
+
for (const [key, value] of Object.entries(corsHeaders)) {
|
|
20
|
+
if (value !== undefined)
|
|
21
|
+
res.setHeader(key, value);
|
|
22
|
+
}
|
|
23
|
+
res.writeHead(200, {
|
|
24
|
+
"Content-Type": "text/event-stream",
|
|
25
|
+
"Cache-Control": "no-cache",
|
|
26
|
+
Connection: "keep-alive",
|
|
27
|
+
});
|
|
28
|
+
try {
|
|
29
|
+
for await (const value of source) {
|
|
30
|
+
writeEvent(res, eventName, value);
|
|
31
|
+
}
|
|
32
|
+
writeEvent(res, "done", {});
|
|
33
|
+
}
|
|
34
|
+
catch (err) {
|
|
35
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
36
|
+
writeEvent(res, "error", { message });
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
res.end();
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function writeEvent(res, event, data) {
|
|
43
|
+
res.write(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`);
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=sse.js.map
|
package/dist/sse.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"sse.js","sourceRoot":"","sources":["../src/sse.ts"],"names":[],"mappings":"AAGA;;;;;;;GAOG;AACH,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,KAAmB,EACnB,MAAyB,EACzB,SAAiB;IAEjB,gFAAgF;IAChF,gFAAgF;IAChF,6EAA6E;IAC7E,mFAAmF;IACnF,mFAAmF;IACnF,qFAAqF;IACrF,MAAM,WAAW,GAAG,KAAK,CAAC,UAAU,EAAE,CAAC;IACvC,KAAK,CAAC,MAAM,EAAE,CAAC;IACf,MAAM,GAAG,GAAG,KAAK,CAAC,GAAG,CAAC;IACtB,KAAK,MAAM,CAAC,GAAG,EAAE,KAAK,CAAC,IAAI,MAAM,CAAC,OAAO,CAAC,WAAW,CAAC,EAAE,CAAC;QACvD,IAAI,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;IACrD,CAAC;IACD,GAAG,CAAC,SAAS,CAAC,GAAG,EAAE;QACjB,cAAc,EAAE,mBAAmB;QACnC,eAAe,EAAE,UAAU;QAC3B,UAAU,EAAE,YAAY;KACzB,CAAC,CAAC;IAEH,IAAI,CAAC;QACH,IAAI,KAAK,EAAE,MAAM,KAAK,IAAI,MAAM,EAAE,CAAC;YACjC,UAAU,CAAC,GAAG,EAAE,SAAS,EAAE,KAAK,CAAC,CAAC;QACpC,CAAC;QACD,UAAU,CAAC,GAAG,EAAE,MAAM,EAAE,EAAE,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,MAAM,OAAO,GAAG,GAAG,YAAY,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QACjE,UAAU,CAAC,GAAG,EAAE,OAAO,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC;IACxC,CAAC;YAAS,CAAC;QACT,GAAG,CAAC,GAAG,EAAE,CAAC;IACZ,CAAC;AACH,CAAC;AAED,SAAS,UAAU,CAAC,GAAmB,EAAE,KAAa,EAAE,IAAa;IACnE,GAAG,CAAC,KAAK,CAAC,UAAU,KAAK,WAAW,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAClE,CAAC"}
|
package/package.json
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@repolore/server",
|
|
3
|
+
"version": "0.0.0",
|
|
4
|
+
"description": "HTTP/SSE API server for repolore — powers 'repolore serve' and remote/self-hosted indexing.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"repolore",
|
|
7
|
+
"rag",
|
|
8
|
+
"codebase",
|
|
9
|
+
"chat",
|
|
10
|
+
"server",
|
|
11
|
+
"fastify"
|
|
12
|
+
],
|
|
13
|
+
"author": "aykutspohrdev",
|
|
14
|
+
"license": "MIT",
|
|
15
|
+
"homepage": "https://github.com/aykutspohrdev/repolore#readme",
|
|
16
|
+
"bugs": "https://github.com/aykutspohrdev/repolore/issues",
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "git+https://github.com/aykutspohrdev/repolore.git",
|
|
20
|
+
"directory": "packages/server"
|
|
21
|
+
},
|
|
22
|
+
"type": "module",
|
|
23
|
+
"main": "./dist/index.js",
|
|
24
|
+
"types": "./dist/index.d.ts",
|
|
25
|
+
"exports": {
|
|
26
|
+
".": "./dist/index.js"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"dist",
|
|
30
|
+
"!dist/**/*.test.js",
|
|
31
|
+
"!dist/**/*.test.js.map",
|
|
32
|
+
"!dist/**/*.test.d.ts"
|
|
33
|
+
],
|
|
34
|
+
"engines": {
|
|
35
|
+
"node": ">=20"
|
|
36
|
+
},
|
|
37
|
+
"dependencies": {
|
|
38
|
+
"@fastify/cors": "^11.3.0",
|
|
39
|
+
"@fastify/static": "^10.1.0",
|
|
40
|
+
"fastify": "^5.1.0",
|
|
41
|
+
"@repolore/core": "0.0.0"
|
|
42
|
+
},
|
|
43
|
+
"devDependencies": {
|
|
44
|
+
"@types/node": "^22.9.0",
|
|
45
|
+
"tsx": "^4.19.2",
|
|
46
|
+
"typescript": "^5.6.3",
|
|
47
|
+
"vitest": "^4.1.10"
|
|
48
|
+
},
|
|
49
|
+
"scripts": {
|
|
50
|
+
"build": "tsc -b",
|
|
51
|
+
"test": "vitest run",
|
|
52
|
+
"dev": "tsx src/index.ts"
|
|
53
|
+
}
|
|
54
|
+
}
|