@versdotsh/reef 0.1.2
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/.github/workflows/test.yml +47 -0
- package/README.md +257 -0
- package/bun.lock +587 -0
- package/examples/services/board/board.test.ts +215 -0
- package/examples/services/board/index.ts +155 -0
- package/examples/services/board/routes.ts +335 -0
- package/examples/services/board/store.ts +329 -0
- package/examples/services/board/tools.ts +214 -0
- package/examples/services/commits/commits.test.ts +74 -0
- package/examples/services/commits/index.ts +14 -0
- package/examples/services/commits/routes.ts +43 -0
- package/examples/services/commits/store.ts +114 -0
- package/examples/services/feed/behaviors.ts +23 -0
- package/examples/services/feed/feed.test.ts +101 -0
- package/examples/services/feed/index.ts +117 -0
- package/examples/services/feed/routes.ts +224 -0
- package/examples/services/feed/store.ts +194 -0
- package/examples/services/feed/tools.ts +83 -0
- package/examples/services/journal/index.ts +15 -0
- package/examples/services/journal/journal.test.ts +57 -0
- package/examples/services/journal/routes.ts +45 -0
- package/examples/services/journal/store.ts +119 -0
- package/examples/services/journal/tools.ts +32 -0
- package/examples/services/log/index.ts +15 -0
- package/examples/services/log/log.test.ts +70 -0
- package/examples/services/log/routes.ts +44 -0
- package/examples/services/log/store.ts +105 -0
- package/examples/services/log/tools.ts +57 -0
- package/examples/services/registry/behaviors.ts +128 -0
- package/examples/services/registry/index.ts +37 -0
- package/examples/services/registry/registry.test.ts +135 -0
- package/examples/services/registry/routes.ts +76 -0
- package/examples/services/registry/store.ts +224 -0
- package/examples/services/registry/tools.ts +116 -0
- package/examples/services/reports/index.ts +14 -0
- package/examples/services/reports/reports.test.ts +75 -0
- package/examples/services/reports/routes.ts +42 -0
- package/examples/services/reports/store.ts +110 -0
- package/examples/services/ui/auth.ts +61 -0
- package/examples/services/ui/index.ts +16 -0
- package/examples/services/ui/routes.ts +160 -0
- package/examples/services/ui/static/app.js +369 -0
- package/examples/services/ui/static/index.html +42 -0
- package/examples/services/ui/static/style.css +157 -0
- package/examples/services/usage/behaviors.ts +166 -0
- package/examples/services/usage/index.ts +19 -0
- package/examples/services/usage/routes.ts +53 -0
- package/examples/services/usage/store.ts +341 -0
- package/examples/services/usage/tools.ts +75 -0
- package/examples/services/usage/usage.test.ts +91 -0
- package/package.json +29 -0
- package/services/agent/index.ts +465 -0
- package/services/board/index.ts +155 -0
- package/services/board/routes.ts +335 -0
- package/services/board/store.ts +329 -0
- package/services/board/tools.ts +214 -0
- package/services/docs/index.ts +391 -0
- package/services/feed/behaviors.ts +23 -0
- package/services/feed/index.ts +117 -0
- package/services/feed/routes.ts +224 -0
- package/services/feed/store.ts +194 -0
- package/services/feed/tools.ts +83 -0
- package/services/installer/index.ts +574 -0
- package/services/services/index.ts +165 -0
- package/services/ui/auth.ts +61 -0
- package/services/ui/index.ts +16 -0
- package/services/ui/routes.ts +160 -0
- package/services/ui/static/app.js +369 -0
- package/services/ui/static/index.html +42 -0
- package/services/ui/static/style.css +157 -0
- package/skills/create-service/SKILL.md +698 -0
- package/src/core/auth.ts +28 -0
- package/src/core/client.ts +99 -0
- package/src/core/discover.ts +152 -0
- package/src/core/events.ts +44 -0
- package/src/core/extension.ts +66 -0
- package/src/core/server.ts +262 -0
- package/src/core/testing.ts +155 -0
- package/src/core/types.ts +194 -0
- package/src/extension.ts +16 -0
- package/src/main.ts +11 -0
- package/tests/server.test.ts +1338 -0
- package/tsconfig.json +29 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Services manager module — runtime management of service modules.
|
|
3
|
+
*
|
|
4
|
+
* This is the service that manages all other services. It's loaded by the
|
|
5
|
+
* same discovery scan as everything else, but uses the enriched ServiceContext
|
|
6
|
+
* to add, update, and remove modules at runtime.
|
|
7
|
+
*
|
|
8
|
+
* GET /services — list loaded modules
|
|
9
|
+
* POST /services/reload — re-scan directory, load new & update changed
|
|
10
|
+
* POST /services/reload/:name — reload a specific module
|
|
11
|
+
* DELETE /services/:name — unload a module
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { readdirSync, existsSync } from "node:fs";
|
|
15
|
+
import { join } from "node:path";
|
|
16
|
+
import { Hono } from "hono";
|
|
17
|
+
import type { ServiceModule, ServiceContext } from "../src/core/types.js";
|
|
18
|
+
|
|
19
|
+
let ctx: ServiceContext;
|
|
20
|
+
|
|
21
|
+
const routes = new Hono();
|
|
22
|
+
|
|
23
|
+
// List all loaded modules
|
|
24
|
+
routes.get("/", (c) => {
|
|
25
|
+
const modules = ctx.getModules().map((m) => ({
|
|
26
|
+
name: m.name,
|
|
27
|
+
description: m.description,
|
|
28
|
+
hasRoutes: !!m.routes,
|
|
29
|
+
hasTools: !!m.registerTools,
|
|
30
|
+
hasBehaviors: !!m.registerBehaviors,
|
|
31
|
+
hasWidget: !!m.widget,
|
|
32
|
+
mountAtRoot: !!m.mountAtRoot,
|
|
33
|
+
dependencies: m.dependencies ?? [],
|
|
34
|
+
}));
|
|
35
|
+
return c.json({ modules, count: modules.length });
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
// Reload all — re-scan directory, add new, update changed, remove deleted
|
|
39
|
+
routes.post("/reload", async (c) => {
|
|
40
|
+
const servicesDir = ctx.servicesDir;
|
|
41
|
+
|
|
42
|
+
if (!existsSync(servicesDir)) {
|
|
43
|
+
return c.json({ error: `Services directory not found: ${servicesDir}` }, 400);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const entries = readdirSync(servicesDir, { withFileTypes: true });
|
|
47
|
+
const results: Array<{ name: string; action: string }> = [];
|
|
48
|
+
const errors: Array<{ dir: string; error: string }> = [];
|
|
49
|
+
|
|
50
|
+
for (const entry of entries) {
|
|
51
|
+
if (!entry.isDirectory()) continue;
|
|
52
|
+
if (!existsSync(join(servicesDir, entry.name, "index.ts"))) continue;
|
|
53
|
+
|
|
54
|
+
try {
|
|
55
|
+
const result = await ctx.loadModule(entry.name);
|
|
56
|
+
results.push(result);
|
|
57
|
+
console.log(` [reload] /${result.name} — ${result.action}`);
|
|
58
|
+
} catch (err) {
|
|
59
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
60
|
+
errors.push({ dir: entry.name, error: msg });
|
|
61
|
+
console.error(` [reload] services/${entry.name}: ${msg}`);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Remove modules whose directories no longer exist
|
|
66
|
+
const currentDirs = new Set(
|
|
67
|
+
entries.filter((e) => e.isDirectory()).map((e) => e.name),
|
|
68
|
+
);
|
|
69
|
+
for (const mod of ctx.getModules()) {
|
|
70
|
+
// Don't remove modules that still have a directory
|
|
71
|
+
if (currentDirs.has(mod.name)) continue;
|
|
72
|
+
// Don't let the services manager remove itself
|
|
73
|
+
if (mod.name === "services") continue;
|
|
74
|
+
|
|
75
|
+
await ctx.unloadModule(mod.name);
|
|
76
|
+
results.push({ name: mod.name, action: "removed" });
|
|
77
|
+
console.log(` [reload] /${mod.name} — removed`);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return c.json({ results, errors });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// Reload a specific module by name
|
|
84
|
+
routes.post("/reload/:name", async (c) => {
|
|
85
|
+
const name = c.req.param("name");
|
|
86
|
+
|
|
87
|
+
// Check if it exists as a directory
|
|
88
|
+
const dirPath = join(ctx.servicesDir, name);
|
|
89
|
+
if (!existsSync(join(dirPath, "index.ts"))) {
|
|
90
|
+
return c.json({ error: `No service directory "${name}" with index.ts found` }, 404);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try {
|
|
94
|
+
const result = await ctx.loadModule(name);
|
|
95
|
+
console.log(` [reload] /${result.name} — ${result.action}`);
|
|
96
|
+
return c.json(result);
|
|
97
|
+
} catch (err) {
|
|
98
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
99
|
+
return c.json({ error: msg }, 400);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
// Export a module as a tarball
|
|
104
|
+
routes.get("/export/:name", async (c) => {
|
|
105
|
+
const name = c.req.param("name");
|
|
106
|
+
const mod = ctx.getModule(name);
|
|
107
|
+
|
|
108
|
+
if (!mod) {
|
|
109
|
+
return c.json({ error: `Module "${name}" not found` }, 404);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const dirPath = join(ctx.servicesDir, name);
|
|
113
|
+
if (!existsSync(dirPath)) {
|
|
114
|
+
return c.json({ error: `Service directory "${name}" not found on disk` }, 404);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
try {
|
|
118
|
+
const { execSync } = await import("node:child_process");
|
|
119
|
+
const tarball = execSync(`tar -czf - -C "${ctx.servicesDir}" "${name}"`, {
|
|
120
|
+
maxBuffer: 50 * 1024 * 1024, // 50MB
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
return new Response(tarball, {
|
|
124
|
+
headers: {
|
|
125
|
+
"Content-Type": "application/gzip",
|
|
126
|
+
"Content-Disposition": `attachment; filename="${name}.tar.gz"`,
|
|
127
|
+
"X-Service-Name": mod.name,
|
|
128
|
+
"X-Service-Description": mod.description || "",
|
|
129
|
+
},
|
|
130
|
+
});
|
|
131
|
+
} catch (err) {
|
|
132
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
133
|
+
return c.json({ error: `Failed to export: ${msg}` }, 500);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
// Unload a module
|
|
138
|
+
routes.delete("/:name", async (c) => {
|
|
139
|
+
const name = c.req.param("name");
|
|
140
|
+
|
|
141
|
+
if (name === "services") {
|
|
142
|
+
return c.json({ error: "Cannot unload the services manager" }, 400);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const mod = ctx.getModule(name);
|
|
146
|
+
if (!mod) {
|
|
147
|
+
return c.json({ error: `Module "${name}" not found` }, 404);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
await ctx.unloadModule(name);
|
|
151
|
+
console.log(` [unload] /${name} — removed`);
|
|
152
|
+
return c.json({ name, action: "removed" });
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
const services: ServiceModule = {
|
|
156
|
+
name: "services",
|
|
157
|
+
description: "Service module manager",
|
|
158
|
+
routes,
|
|
159
|
+
|
|
160
|
+
init(serviceCtx: ServiceContext) {
|
|
161
|
+
ctx = serviceCtx;
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
export default services;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Magic link auth for the web UI.
|
|
3
|
+
*
|
|
4
|
+
* Flow: agent generates a magic link via POST /auth/magic-link (bearer auth),
|
|
5
|
+
* user opens it in browser, gets a session cookie, UI proxies API calls.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { ulid } from "ulid";
|
|
9
|
+
|
|
10
|
+
interface MagicLink {
|
|
11
|
+
token: string;
|
|
12
|
+
expiresAt: string;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface Session {
|
|
16
|
+
id: string;
|
|
17
|
+
createdAt: string;
|
|
18
|
+
expiresAt: string;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const magicLinks = new Map<string, MagicLink>();
|
|
22
|
+
const sessions = new Map<string, Session>();
|
|
23
|
+
|
|
24
|
+
const LINK_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
25
|
+
const SESSION_TTL_MS = 24 * 60 * 60 * 1000; // 24 hours
|
|
26
|
+
|
|
27
|
+
export function createMagicLink(): MagicLink {
|
|
28
|
+
const token = ulid();
|
|
29
|
+
const expiresAt = new Date(Date.now() + LINK_TTL_MS).toISOString();
|
|
30
|
+
const link: MagicLink = { token, expiresAt };
|
|
31
|
+
magicLinks.set(token, link);
|
|
32
|
+
return link;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function consumeMagicLink(token: string): boolean {
|
|
36
|
+
const link = magicLinks.get(token);
|
|
37
|
+
if (!link) return false;
|
|
38
|
+
magicLinks.delete(token);
|
|
39
|
+
return new Date(link.expiresAt).getTime() > Date.now();
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function createSession(): Session {
|
|
43
|
+
const session: Session = {
|
|
44
|
+
id: ulid(),
|
|
45
|
+
createdAt: new Date().toISOString(),
|
|
46
|
+
expiresAt: new Date(Date.now() + SESSION_TTL_MS).toISOString(),
|
|
47
|
+
};
|
|
48
|
+
sessions.set(session.id, session);
|
|
49
|
+
return session;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function validateSession(sessionId: string | undefined): boolean {
|
|
53
|
+
if (!sessionId) return false;
|
|
54
|
+
const session = sessions.get(sessionId);
|
|
55
|
+
if (!session) return false;
|
|
56
|
+
if (new Date(session.expiresAt).getTime() < Date.now()) {
|
|
57
|
+
sessions.delete(sessionId);
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI service module — web dashboard with magic link auth and API proxy.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { ServiceModule } from "../src/core/types.js";
|
|
6
|
+
import { createRoutes } from "./routes.js";
|
|
7
|
+
|
|
8
|
+
const ui: ServiceModule = {
|
|
9
|
+
name: "ui",
|
|
10
|
+
description: "Web dashboard",
|
|
11
|
+
routes: createRoutes(),
|
|
12
|
+
mountAtRoot: true, // Serves at /ui/*, /auth/* — handles its own session auth
|
|
13
|
+
requiresAuth: false,
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
export default ui;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* UI routes — serves the dashboard, handles magic link auth, proxies API calls.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import { Hono } from "hono";
|
|
6
|
+
import { createMagicLink, consumeMagicLink, createSession, validateSession } from "./auth.js";
|
|
7
|
+
import { readFileSync } from "node:fs";
|
|
8
|
+
import { join } from "node:path";
|
|
9
|
+
|
|
10
|
+
const AUTH_TOKEN = process.env.VERS_AUTH_TOKEN || "test-token";
|
|
11
|
+
|
|
12
|
+
function getStaticDir(): string {
|
|
13
|
+
return join(import.meta.dir, "static");
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function getSessionId(c: any): string | undefined {
|
|
17
|
+
const cookie = c.req.header("cookie") || "";
|
|
18
|
+
const match = cookie.match(/(?:^|;\s*)session=([^;]+)/);
|
|
19
|
+
return match?.[1];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function hasBearerAuth(c: any): boolean {
|
|
23
|
+
const auth = c.req.header("authorization") || "";
|
|
24
|
+
return auth === `Bearer ${AUTH_TOKEN}`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export function createRoutes(): Hono {
|
|
28
|
+
const routes = new Hono();
|
|
29
|
+
|
|
30
|
+
// --- Auth ---
|
|
31
|
+
|
|
32
|
+
// Generate magic link (requires bearer auth)
|
|
33
|
+
routes.post("/auth/magic-link", (c) => {
|
|
34
|
+
if (!hasBearerAuth(c)) return c.json({ error: "Unauthorized" }, 401);
|
|
35
|
+
|
|
36
|
+
const link = createMagicLink();
|
|
37
|
+
const host = c.req.header("host") || "localhost:3000";
|
|
38
|
+
const proto = c.req.header("x-forwarded-proto") || "https";
|
|
39
|
+
const url = `${proto}://${host}/ui/login?token=${link.token}`;
|
|
40
|
+
return c.json({ url, expiresAt: link.expiresAt });
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// Login page / magic link consumer
|
|
44
|
+
routes.get("/ui/login", (c) => {
|
|
45
|
+
const token = c.req.query("token");
|
|
46
|
+
|
|
47
|
+
if (token) {
|
|
48
|
+
const valid = consumeMagicLink(token);
|
|
49
|
+
if (valid) {
|
|
50
|
+
const session = createSession();
|
|
51
|
+
return c.html(
|
|
52
|
+
`<html><head><meta http-equiv="refresh" content="0;url=/ui/"></head></html>`,
|
|
53
|
+
200,
|
|
54
|
+
{ "Set-Cookie": `session=${session.id}; Path=/; HttpOnly; SameSite=Lax; Max-Age=86400` },
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
return c.html(`
|
|
58
|
+
<html><body style="background:#0a0a0a;color:#f55;font-family:monospace;padding:2em">
|
|
59
|
+
<h2>Invalid or expired link</h2>
|
|
60
|
+
<p>Request a new magic link from the API.</p>
|
|
61
|
+
</body></html>
|
|
62
|
+
`, 401);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return c.html(`
|
|
66
|
+
<html><body style="background:#0a0a0a;color:#888;font-family:monospace;padding:2em">
|
|
67
|
+
<h2>Fleet Services Dashboard</h2>
|
|
68
|
+
<p>Access requires a magic link. Generate one via:</p>
|
|
69
|
+
<pre style="color:#4f9">POST /auth/magic-link</pre>
|
|
70
|
+
</body></html>
|
|
71
|
+
`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// --- Session-protected UI routes ---
|
|
75
|
+
|
|
76
|
+
routes.use("/ui/*", async (c, next) => {
|
|
77
|
+
const path = new URL(c.req.url).pathname;
|
|
78
|
+
if (path === "/ui/login" || path.startsWith("/ui/static/")) return next();
|
|
79
|
+
|
|
80
|
+
// In dev mode (no auth token set), skip session check
|
|
81
|
+
if (!process.env.VERS_AUTH_TOKEN) return next();
|
|
82
|
+
|
|
83
|
+
const sessionId = getSessionId(c);
|
|
84
|
+
if (!validateSession(sessionId)) return c.redirect("/ui/login");
|
|
85
|
+
return next();
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// Dashboard
|
|
89
|
+
routes.get("/ui/", (c) => {
|
|
90
|
+
try {
|
|
91
|
+
const html = readFileSync(join(getStaticDir(), "index.html"), "utf-8");
|
|
92
|
+
return c.html(html);
|
|
93
|
+
} catch {
|
|
94
|
+
return c.text("Dashboard files not found", 500);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
// Static files
|
|
99
|
+
routes.get("/ui/static/:file", (c) => {
|
|
100
|
+
const file = c.req.param("file");
|
|
101
|
+
if (file.includes("..") || file.includes("/")) return c.text("Not found", 404);
|
|
102
|
+
|
|
103
|
+
try {
|
|
104
|
+
const content = readFileSync(join(getStaticDir(), file), "utf-8");
|
|
105
|
+
const ext = file.split(".").pop();
|
|
106
|
+
const contentType =
|
|
107
|
+
ext === "css" ? "text/css" :
|
|
108
|
+
ext === "js" ? "application/javascript" :
|
|
109
|
+
"text/plain";
|
|
110
|
+
return c.body(content, 200, { "Content-Type": contentType });
|
|
111
|
+
} catch {
|
|
112
|
+
return c.text("Not found", 404);
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
// --- API proxy (injects bearer token so browser never needs it) ---
|
|
117
|
+
|
|
118
|
+
routes.all("/ui/api/*", async (c) => {
|
|
119
|
+
const url = new URL(c.req.url);
|
|
120
|
+
const apiPath = url.pathname.replace(/^\/ui\/api/, "");
|
|
121
|
+
const queryString = url.search;
|
|
122
|
+
|
|
123
|
+
const port = process.env.PORT || "3000";
|
|
124
|
+
const internalUrl = `http://127.0.0.1:${port}${apiPath}${queryString}`;
|
|
125
|
+
|
|
126
|
+
const headers: Record<string, string> = {
|
|
127
|
+
Authorization: `Bearer ${AUTH_TOKEN}`,
|
|
128
|
+
};
|
|
129
|
+
const contentType = c.req.header("content-type");
|
|
130
|
+
if (contentType) headers["Content-Type"] = contentType;
|
|
131
|
+
|
|
132
|
+
const method = c.req.method;
|
|
133
|
+
const body = method !== "GET" && method !== "HEAD" ? await c.req.text() : undefined;
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
const resp = await fetch(internalUrl, { method, headers, body });
|
|
137
|
+
|
|
138
|
+
// SSE passthrough
|
|
139
|
+
if (resp.headers.get("content-type")?.includes("text/event-stream")) {
|
|
140
|
+
return new Response(resp.body, {
|
|
141
|
+
status: resp.status,
|
|
142
|
+
headers: {
|
|
143
|
+
"Content-Type": "text/event-stream",
|
|
144
|
+
"Cache-Control": "no-cache",
|
|
145
|
+
Connection: "keep-alive",
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const text = await resp.text();
|
|
151
|
+
return c.body(text, resp.status as any, {
|
|
152
|
+
"Content-Type": resp.headers.get("content-type") || "application/json",
|
|
153
|
+
});
|
|
154
|
+
} catch (e) {
|
|
155
|
+
return c.json({ error: "Proxy error", details: String(e) }, 502);
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
return routes;
|
|
160
|
+
}
|