@hachej/boring-core 0.1.13 → 0.1.14
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/README.md +280 -43
- package/dist/app/server/index.js +66 -11
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -1,83 +1,320 @@
|
|
|
1
|
-
# @boring
|
|
1
|
+
# @hachej/boring-core
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
<div align="center">
|
|
4
|
+
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://www.npmjs.com/package/@hachej/boring-core)
|
|
7
|
+
|
|
8
|
+
</div>
|
|
9
|
+
|
|
10
|
+
The foundation package for boring-ui apps: Postgres/Drizzle database schema, email/password auth (better-auth), config loader, Fastify HTTP app factory, and React frontend shell. Every child app imports core first.
|
|
4
11
|
|
|
5
12
|
```bash
|
|
6
|
-
pnpm add @boring
|
|
13
|
+
pnpm add @hachej/boring-core
|
|
7
14
|
```
|
|
8
15
|
|
|
9
16
|
---
|
|
10
17
|
|
|
11
|
-
##
|
|
18
|
+
## TL;DR
|
|
12
19
|
|
|
13
|
-
|
|
14
|
-
- **Auth** — better-auth with workspace support, invite flows, email verification
|
|
15
|
-
- **App factory** — Fastify app with auth routes, middleware, and CORS wired in
|
|
16
|
-
- **Frontend shell** — `<BoringApp>` React provider with auth pages and workspace switcher
|
|
20
|
+
**The Problem**: Building a multi-user agent-powered app means re-implementing auth, sessions, workspaces, invites, email flows, and an app shell every single time. These are the same across every deployment.
|
|
17
21
|
|
|
18
|
-
|
|
22
|
+
**The Solution**: `@hachej/boring-core` provides a complete app skeleton — Postgres DB, better-auth with email verification + password reset + magic links, workspace membership with roles, email transport (Resend/SMTP/console), and a `<BoringApp>` React shell with auth pages. You bring the domain logic.
|
|
23
|
+
|
|
24
|
+
### Why Use @hachej/boring-core?
|
|
19
25
|
|
|
20
|
-
|
|
26
|
+
| Feature | What It Does |
|
|
27
|
+
|---------|--------------|
|
|
28
|
+
| **Full auth suite** | Email/password + email verification + password reset + magic links (better-auth) |
|
|
29
|
+
| **Workspace management** | Create, update, delete workspaces; member roles (owner/editor/viewer); invites |
|
|
30
|
+
| **Fastify app factory** | Pre-wired with helmet, CORS, rate limiting, secret redaction, graceful shutdown |
|
|
31
|
+
| **Drizzle + Postgres** | Ready-to-run schema for users, workspaces, members, invites, settings |
|
|
32
|
+
| **Email transport** | Resend (default), SMTP, or console — pluggable via URL scheme |
|
|
33
|
+
| **<BoringApp> shell** | Client-rendered React shell with auth gate, theme toggle, workspace switcher |
|
|
34
|
+
| **Config loader** | TOML + env vars merged, Zod-validated, redacted for frontend |
|
|
21
35
|
|
|
22
|
-
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## Quick Example
|
|
23
39
|
|
|
24
40
|
```ts
|
|
25
|
-
|
|
41
|
+
// Server — 4 lines to a full app
|
|
42
|
+
import { createCoreApp, loadConfig } from "@hachej/boring-core/server"
|
|
26
43
|
|
|
27
44
|
const config = await loadConfig()
|
|
28
|
-
const app = await createCoreApp(config)
|
|
29
|
-
|
|
45
|
+
const app = await createCoreApp(config) // Fastify + DB + auth + routes
|
|
46
|
+
|
|
47
|
+
await app.listen({ port: 3000 })
|
|
30
48
|
```
|
|
31
49
|
|
|
32
|
-
|
|
50
|
+
```tsx
|
|
51
|
+
// Frontend — mount auth gate + workspace routing
|
|
52
|
+
<BoringApp>
|
|
53
|
+
<Route path="/workspace/:id" element={<WorkspaceRoute />} />
|
|
54
|
+
<Route path="/settings" element={<Settings />} />
|
|
55
|
+
</BoringApp>
|
|
56
|
+
```
|
|
33
57
|
|
|
34
58
|
```tsx
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return (
|
|
40
|
-
<BoringApp>
|
|
41
|
-
<Route path="/" element={<WorkspaceProvider><IdeLayout /></WorkspaceProvider>} />
|
|
42
|
-
</BoringApp>
|
|
43
|
-
)
|
|
44
|
-
}
|
|
59
|
+
// In your components — typed auth + workspace access
|
|
60
|
+
const user = useUser()
|
|
61
|
+
const workspace = useCurrentWorkspace()
|
|
62
|
+
const role = useWorkspaceRole() // 'owner' | 'editor' | 'viewer'
|
|
45
63
|
```
|
|
46
64
|
|
|
47
65
|
---
|
|
48
66
|
|
|
49
|
-
##
|
|
67
|
+
## Design Philosophy
|
|
68
|
+
|
|
69
|
+
1. **Core owns persistence and identity** — DB tables, auth, sessions, workspaces, invites. Everything else injects stores via interfaces.
|
|
70
|
+
2. **One config source** — `boring.app.toml` + environment variables merged, Zod-validated at boot. No scattered config.
|
|
71
|
+
3. **Email flows are real, not stubs** — password reset, email verification, magic links, workspace invites — all shipped with React Email templates.
|
|
72
|
+
4. **Swap seams, not rewrites** — `AuthProvider`, `UserStore`, `WorkspaceStore` are interfaces. The default impl is Postgres; swap via `createCoreApp({ authProvider })`.
|
|
73
|
+
5. **Fail closed on auth** — config fetch failure throws a `ConfigFetchError` with retries. Users see "Cannot reach server" not a blank page.
|
|
74
|
+
|
|
75
|
+
---
|
|
50
76
|
|
|
51
|
-
|
|
77
|
+
## Installation
|
|
52
78
|
|
|
53
79
|
```bash
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
80
|
+
# pnpm
|
|
81
|
+
pnpm add @hachej/boring-core @hachej/boring-workspace
|
|
82
|
+
|
|
83
|
+
# npm
|
|
84
|
+
npm install @hachej/boring-core @hachej/boring-workspace
|
|
85
|
+
|
|
86
|
+
# from source
|
|
87
|
+
git clone https://github.com/hachej/boring-ui.git
|
|
88
|
+
cd boring-ui && pnpm install
|
|
89
|
+
pnpm --filter @hachej/boring-core build
|
|
57
90
|
```
|
|
58
91
|
|
|
59
|
-
|
|
92
|
+
### Dependencies
|
|
93
|
+
|
|
94
|
+
Postgres is required for production. For dev, set `CORE_STORES=local` and core runs in-memory (state resets on restart).
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Quick Start
|
|
99
|
+
|
|
100
|
+
### 1. Set Environment
|
|
60
101
|
|
|
61
102
|
```bash
|
|
62
|
-
|
|
103
|
+
# .env
|
|
104
|
+
DATABASE_URL=postgres://user:pass@localhost:5432/myapp
|
|
105
|
+
BETTER_AUTH_SECRET=<32-byte random hex>
|
|
106
|
+
BETTER_AUTH_URL=http://localhost:3000
|
|
107
|
+
WORKSPACE_SETTINGS_ENCRYPTION_KEY=<32-byte hex>
|
|
108
|
+
MAIL_FROM=noreply@myapp.dev
|
|
109
|
+
MAIL_TRANSPORT_URL=resend://re_xxxxxxxxxxxxxxxx
|
|
63
110
|
```
|
|
64
111
|
|
|
65
|
-
|
|
112
|
+
### 2. Create Config File
|
|
113
|
+
|
|
114
|
+
```toml
|
|
115
|
+
# boring.app.toml
|
|
116
|
+
[app]
|
|
117
|
+
id = "my-app"
|
|
118
|
+
|
|
119
|
+
[frontend.branding]
|
|
120
|
+
name = "My App"
|
|
121
|
+
logo = "/logo.svg"
|
|
66
122
|
|
|
67
|
-
|
|
123
|
+
[features]
|
|
124
|
+
invites_enabled = true
|
|
125
|
+
invite_ttl_days = 7
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
### 3. Run Migrations
|
|
129
|
+
|
|
130
|
+
```bash
|
|
131
|
+
pnpm drizzle-kit generate --config node_modules/@hachej/boring-core/drizzle.config.ts
|
|
132
|
+
pnpm drizzle-kit migrate --config node_modules/@hachej/boring-core/drizzle.config.ts
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### 4. Server Entry
|
|
68
136
|
|
|
69
137
|
```ts
|
|
70
|
-
import {
|
|
71
|
-
|
|
72
|
-
|
|
138
|
+
import { createCoreApp, loadConfig } from "@hachej/boring-core/server"
|
|
139
|
+
|
|
140
|
+
const config = await loadConfig()
|
|
141
|
+
const app = await createCoreApp(config)
|
|
142
|
+
|
|
143
|
+
// add child-app routes
|
|
144
|
+
app.get("/api/v1/my-thing", async () => ({ ok: true }))
|
|
145
|
+
|
|
146
|
+
await app.listen({ port: config.port })
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### 5. Frontend Entry
|
|
150
|
+
|
|
151
|
+
```tsx
|
|
152
|
+
import { createRoot } from "react-dom/client"
|
|
153
|
+
import { BoringApp } from "@hachej/boring-core/front"
|
|
154
|
+
import { Route } from "react-router-dom"
|
|
155
|
+
import "@hachej/boring-core/theme.css"
|
|
156
|
+
|
|
157
|
+
createRoot(document.getElementById("root")!).render(
|
|
158
|
+
<BoringApp>
|
|
159
|
+
<Route path="/" element={<Dashboard />} />
|
|
160
|
+
</BoringApp>,
|
|
161
|
+
)
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## Package Surfaces
|
|
167
|
+
|
|
168
|
+
| Import | Environment | What You Get |
|
|
169
|
+
|--------|-------------|--------------|
|
|
170
|
+
| `@hachej/boring-core/server` | Node | `createCoreApp`, `loadConfig`, auth, stores, routes |
|
|
171
|
+
| `@hachej/boring-core/server/db` | Node | Drizzle schema, migrations, store interfaces |
|
|
172
|
+
| `@hachej/boring-core/front` | Browser | `<BoringApp>`, hooks, auth pages, components |
|
|
173
|
+
| `@hachej/boring-core/shared` | Any | `User`, `Workspace`, `HttpError`, `ErrorCode` types |
|
|
174
|
+
| `@hachej/boring-core/theme.css` | Browser | CSS theme tokens for the frontend shell |
|
|
175
|
+
| `@hachej/boring-core/app/front` | Browser | App composition helpers (`WorkspaceAgentFront`, etc.) |
|
|
176
|
+
| `@hachej/boring-core/app/server` | Node | App composition helpers (`createWorkspaceAgentApp`) |
|
|
177
|
+
|
|
178
|
+
---
|
|
179
|
+
|
|
180
|
+
## Configuration
|
|
181
|
+
|
|
182
|
+
### Environment Variables
|
|
183
|
+
|
|
184
|
+
| Variable | Required | Description |
|
|
185
|
+
|----------|----------|-------------|
|
|
186
|
+
| `DATABASE_URL` | Yes (prod) | Postgres connection string |
|
|
187
|
+
| `BETTER_AUTH_SECRET` | Yes | 32-byte hex — signs session cookies |
|
|
188
|
+
| `BETTER_AUTH_URL` | Yes | Public URL for OAuth callbacks |
|
|
189
|
+
| `WORKSPACE_SETTINGS_ENCRYPTION_KEY` | Yes (prod) | 32-byte hex — encrypts workspace settings |
|
|
190
|
+
| `MAIL_FROM` | Yes (prod) | Sender address for auth emails |
|
|
191
|
+
| `MAIL_TRANSPORT_URL` | Yes (prod) | `resend://key`, `smtp://host`, or `console://` |
|
|
192
|
+
| `CORE_STORES` | No | `postgres` (default) or `local` (in-memory dev) |
|
|
193
|
+
| `CORS_ORIGINS` | Yes (prod) | Comma-separated allowlist |
|
|
194
|
+
| `SEND_WELCOME_EMAIL` | No | Default `true` — suppress with `false` |
|
|
195
|
+
| `SESSION_TTL_SECONDS` | No | Default 2,592,000 (30 days) |
|
|
196
|
+
|
|
197
|
+
---
|
|
198
|
+
|
|
199
|
+
## Architecture
|
|
200
|
+
|
|
201
|
+
```
|
|
202
|
+
┌──────────────────────┐
|
|
203
|
+
│ Browser Client │
|
|
204
|
+
│ /auth/* + /me + │
|
|
205
|
+
│ /workspaces/* │
|
|
206
|
+
└──────────┬───────────┘
|
|
207
|
+
│ HTTP (typed, cookie auth)
|
|
208
|
+
┌──────────▼───────────┐
|
|
209
|
+
│ Fastify App │
|
|
210
|
+
│ ├── authHook (req.user)
|
|
211
|
+
│ ├── helmet + CORS │
|
|
212
|
+
│ ├── rate limits │
|
|
213
|
+
│ ├── secret redaction│
|
|
214
|
+
│ └── graceful shutdown
|
|
215
|
+
└──────────┬───────────┘
|
|
216
|
+
│
|
|
217
|
+
┌──────────▼───────────┐
|
|
218
|
+
│ better-auth │
|
|
219
|
+
│ (sessions, email, │
|
|
220
|
+
│ password reset) │
|
|
221
|
+
└──────────┬───────────┘
|
|
222
|
+
│
|
|
223
|
+
┌──────────▼───────────┐
|
|
224
|
+
│ Drizzle + Postgres │
|
|
225
|
+
│ users, sessions, │
|
|
226
|
+
│ workspaces, members, │
|
|
227
|
+
│ invites, settings │
|
|
228
|
+
└──────────────────────┘
|
|
73
229
|
```
|
|
74
230
|
|
|
231
|
+
### Error Handling Contract
|
|
232
|
+
|
|
233
|
+
All errors flow through a single `setErrorHandler`:
|
|
234
|
+
|
|
235
|
+
| Condition | Status | Code |
|
|
236
|
+
|-----------|--------|------|
|
|
237
|
+
| No/expired session | 401 | `unauthorized` |
|
|
238
|
+
| Insufficient role | 403 | `forbidden` / `not_member` |
|
|
239
|
+
| Zod validation fail | 400 | `validation_failed` |
|
|
240
|
+
| Rate limited | 429 | `rate_limited` + `Retry-After` |
|
|
241
|
+
| DB ping fails | 503 | `db_unavailable` |
|
|
242
|
+
| Everything else | 500 | `internal_error` |
|
|
243
|
+
|
|
244
|
+
Every response includes `{ error, code, message, requestId }`. Client-side `apiFetch` parses this into `HttpError` instances.
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## How @hachej/boring-core Compares
|
|
249
|
+
|
|
250
|
+
| Feature | @hachej/boring-core | Supabase + custom | Firebase | Roll your own |
|
|
251
|
+
|---------|---------------------|-------------------|----------|---------------|
|
|
252
|
+
| Auth flows | ✅ email + reset + magic link | ✅ OAuth only | ✅ OAuth/phone | Weeks to build |
|
|
253
|
+
| Workspaces + invites | ✅ owner/editor/viewer roles | ❌ Custom tables | ❌ Custom rules | ~1 week |
|
|
254
|
+
| Email templates | ✅ 5 React Email templates | ❌ You write them | ❌ SendGrid setup | ~3 days |
|
|
255
|
+
| App shell | ✅ `<BoringApp>` + hooks | ❌ DIY | ❌ DIY | ~1 week |
|
|
256
|
+
| Rate limiting | ✅ pre-wired routes | ❌ Edge functions | ⚠️ Cloud rules | ~2 days |
|
|
257
|
+
| Config validation | ✅ TOML + env + Zod | ❌ dotenv only | ⚠️ Remote config | Custom |
|
|
258
|
+
|
|
259
|
+
**When to use @hachej/boring-core:**
|
|
260
|
+
- Building a multi-user app around an AI agent
|
|
261
|
+
- You need auth + workspaces + invites in days, not weeks
|
|
262
|
+
- You're deploying to Fly.io, Render, Railway, or any Postgres-capable host
|
|
263
|
+
|
|
264
|
+
**When it might not fit:**
|
|
265
|
+
- You need server-side rendering (client-rendered only)
|
|
266
|
+
- You want SQLite (Postgres-only with Drizzle)
|
|
267
|
+
- You need Google/Apple/Discord OAuth (planned for v1.x)
|
|
268
|
+
- You need billing/Stripe integration (future `@boring/cloud` package)
|
|
269
|
+
|
|
270
|
+
---
|
|
271
|
+
|
|
272
|
+
## Troubleshooting
|
|
273
|
+
|
|
274
|
+
| Error | Cause | Fix |
|
|
275
|
+
|-------|-------|-----|
|
|
276
|
+
| `ConfigValidationError` at boot | Missing required env var | Check `.env` has all required vars |
|
|
277
|
+
| `config_fetch_failed` in browser | API server not reachable | Verify `BETTER_AUTH_URL` matches |
|
|
278
|
+
| `mail_disabled` warning at boot | `MAIL_FROM` not set | Set `MAIL_TRANSPORT_URL=console://` for dev |
|
|
279
|
+
| `unauthorized` on `/api/v1/me` | No session cookie | Check `BETTER_AUTH_URL` and `CORS_ORIGINS` |
|
|
280
|
+
| `db_unavailable` on `/health` | Postgres can't connect | Verify `DATABASE_URL` and network access |
|
|
281
|
+
|
|
282
|
+
---
|
|
283
|
+
|
|
284
|
+
## Limitations
|
|
285
|
+
|
|
286
|
+
- **Postgres only** — No SQLite/libsql support in v1.
|
|
287
|
+
- **Client-rendered only** — `<BoringApp>` mounts client-side. No SSR.
|
|
288
|
+
- **GitHub OAuth deferred** — Planned for v1.x, bundled with agent's GitHub App install.
|
|
289
|
+
- **No billing** — Stripe integration planned for `@boring/cloud` package.
|
|
290
|
+
- **In-memory stores are dev-only** — `CORE_STORES=local` resets on restart. Not for production.
|
|
291
|
+
- **Partial swap seams** — `AuthProvider` is swappable, but the React auth surfaces (`useSession`, sign-in pages) are better-auth-shaped.
|
|
292
|
+
|
|
293
|
+
---
|
|
294
|
+
|
|
295
|
+
## FAQ
|
|
296
|
+
|
|
297
|
+
**Q: Can I use this without Postgres?**
|
|
298
|
+
A: In dev, yes — set `CORE_STORES=local`. State is in-memory and resets on restart. For production, Postgres is required.
|
|
299
|
+
|
|
300
|
+
**Q: How do I add Google/Discord OAuth?**
|
|
301
|
+
A: better-auth supports these out of the box. Add the provider config to `createAuth()` in the core source. Official v1.x support planned.
|
|
302
|
+
|
|
303
|
+
**Q: Can I swap better-auth for Clerk/Neon?**
|
|
304
|
+
A: The `AuthProvider` interface is designed as a swap seam. You'll need to re-implement the React auth surfaces (`SignInPage`, `useSession`, etc.) and preserve the `users.id` continuity invariant.
|
|
305
|
+
|
|
306
|
+
**Q: How do email templates work?**
|
|
307
|
+
A: Five React Email components (`VerifyEmail`, `ResetPassword`, `MagicLink`, `WorkspaceInvite`, `Welcome`) rendered via `@react-email/render`. CSS is inlined. Swap them by providing your own mail transport.
|
|
308
|
+
|
|
309
|
+
**Q: What's the difference between `@hachej/boring-core/server` and `@hachej/boring-core/server/db`?**
|
|
310
|
+
A: `server` includes the full Fastify app, routes, auth, and stores. `server/db` is the Drizzle schema + connection + store interfaces only — useful for migration tooling and type-only imports.
|
|
311
|
+
|
|
312
|
+
---
|
|
313
|
+
|
|
314
|
+
*About Contributions:* Please don't take this the wrong way, but I do not accept outside contributions for any of my projects. I simply don't have the mental bandwidth to review anything, and it's my name on the thing, so I'm responsible for any problems it causes; thus, the risk-reward is highly asymmetric from my perspective. I'd also have to worry about other "stakeholders," which seems unwise for tools I mostly make for myself for free. Feel free to submit issues, and even PRs if you want to illustrate a proposed fix, but know I won't merge them directly. Instead, I'll have Claude or Codex review submissions via `gh` and independently decide whether and how to address them. Bug reports in particular are welcome. Sorry if this offends, but I want to avoid wasted time and hurt feelings. I understand this isn't in sync with the prevailing open-source ethos that seeks community contributions, but it's the only way I can move at this velocity and keep my sanity.
|
|
315
|
+
|
|
75
316
|
---
|
|
76
317
|
|
|
77
|
-
##
|
|
318
|
+
## License
|
|
78
319
|
|
|
79
|
-
|
|
80
|
-
|---|---|
|
|
81
|
-
| `@boring/core` | DB, auth, app factory |
|
|
82
|
-
| `@boring/workspace` | Plugin system, layouts |
|
|
83
|
-
| `@boring/agent` | Agent runtime + tools |
|
|
320
|
+
MIT
|
package/dist/app/server/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { access, mkdir, readFile, stat } from "fs/promises";
|
|
|
23
23
|
import { createReadStream } from "fs";
|
|
24
24
|
import path from "path";
|
|
25
25
|
import {
|
|
26
|
+
compactPiPackages,
|
|
26
27
|
registerAgentRoutes
|
|
27
28
|
} from "@hachej/boring-agent/server";
|
|
28
29
|
import {
|
|
@@ -63,6 +64,24 @@ var FRONTEND_AUTH_PAGES = /* @__PURE__ */ new Set([
|
|
|
63
64
|
var FRONTEND_AUTH_PAGES_SPA_ONLY = /* @__PURE__ */ new Set([
|
|
64
65
|
"/auth/verify-email"
|
|
65
66
|
]);
|
|
67
|
+
function dedupeStrings(values) {
|
|
68
|
+
return Array.from(new Set(values));
|
|
69
|
+
}
|
|
70
|
+
function mergeResourceLoaderOptions(base, override) {
|
|
71
|
+
if (!base && !override) return void 0;
|
|
72
|
+
return {
|
|
73
|
+
...base,
|
|
74
|
+
...override,
|
|
75
|
+
additionalSkillPaths: dedupeStrings([
|
|
76
|
+
...base?.additionalSkillPaths ?? [],
|
|
77
|
+
...override?.additionalSkillPaths ?? []
|
|
78
|
+
]),
|
|
79
|
+
piPackages: compactPiPackages([
|
|
80
|
+
...base?.piPackages ?? [],
|
|
81
|
+
...override?.piPackages ?? []
|
|
82
|
+
])
|
|
83
|
+
};
|
|
84
|
+
}
|
|
66
85
|
function contentType(filePath) {
|
|
67
86
|
const ext = path.extname(filePath).toLowerCase();
|
|
68
87
|
return MIME_TYPES[ext] ?? "application/octet-stream";
|
|
@@ -131,6 +150,11 @@ function httpError(message, statusCode) {
|
|
|
131
150
|
error.statusCode = statusCode;
|
|
132
151
|
return error;
|
|
133
152
|
}
|
|
153
|
+
function firstString(value) {
|
|
154
|
+
if (typeof value === "string") return value;
|
|
155
|
+
if (!Array.isArray(value)) return void 0;
|
|
156
|
+
return value.find((item) => typeof item === "string");
|
|
157
|
+
}
|
|
134
158
|
function validateWorkspaceIdSegment(value) {
|
|
135
159
|
const workspaceId = value.trim();
|
|
136
160
|
if (!workspaceId) throw httpError("workspace id is required", 400);
|
|
@@ -139,12 +163,14 @@ function validateWorkspaceIdSegment(value) {
|
|
|
139
163
|
}
|
|
140
164
|
return workspaceId;
|
|
141
165
|
}
|
|
142
|
-
|
|
143
|
-
const
|
|
166
|
+
function resolveWorkspaceIdFromRequest(request) {
|
|
167
|
+
const headers = request.headers ?? {};
|
|
168
|
+
const headerValue = headers["x-boring-workspace-id"] ?? Object.entries(headers).find(([key]) => key.toLowerCase() === "x-boring-workspace-id")?.[1];
|
|
144
169
|
const query = request.query;
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
170
|
+
return validateWorkspaceIdSegment(firstString(headerValue) ?? firstString(query?.workspaceId) ?? "");
|
|
171
|
+
}
|
|
172
|
+
async function resolveAuthorizedWorkspaceId(request, workspaceStore) {
|
|
173
|
+
const normalizedWorkspaceId = resolveWorkspaceIdFromRequest(request);
|
|
148
174
|
const user = request.user;
|
|
149
175
|
if (!user?.id) throw httpError("authentication required", 401);
|
|
150
176
|
let member = false;
|
|
@@ -331,26 +357,28 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
331
357
|
const provisionedWorkspaceRoots = /* @__PURE__ */ new Map();
|
|
332
358
|
const ensureWorkspaceProvisioned = (root) => {
|
|
333
359
|
if (pluginCollection.provisioningContributions.length === 0) return Promise.resolve();
|
|
334
|
-
const
|
|
360
|
+
const resolvedRoot = path.resolve(root);
|
|
361
|
+
const existing = provisionedWorkspaceRoots.get(resolvedRoot);
|
|
335
362
|
if (existing) return existing;
|
|
336
363
|
const pending = provisionWorkspaceAgentServer({
|
|
337
|
-
workspaceRoot:
|
|
364
|
+
workspaceRoot: resolvedRoot,
|
|
338
365
|
provisioningContributions: pluginCollection.provisioningContributions,
|
|
339
366
|
force: options.forceProvisioning
|
|
340
367
|
}).catch((error) => {
|
|
341
|
-
provisionedWorkspaceRoots.delete(
|
|
368
|
+
provisionedWorkspaceRoots.delete(resolvedRoot);
|
|
342
369
|
throw error;
|
|
343
370
|
});
|
|
344
|
-
provisionedWorkspaceRoots.set(
|
|
371
|
+
provisionedWorkspaceRoots.set(resolvedRoot, pending);
|
|
345
372
|
return pending;
|
|
346
373
|
};
|
|
347
374
|
await ensureWorkspaceProvisioned(workspaceRoot);
|
|
348
375
|
const bridges = /* @__PURE__ */ new Map();
|
|
349
376
|
const getUiBridge = (workspaceId) => {
|
|
350
|
-
|
|
377
|
+
const safeWorkspaceId = validateWorkspaceIdSegment(workspaceId);
|
|
378
|
+
let bridge = bridges.get(safeWorkspaceId);
|
|
351
379
|
if (!bridge) {
|
|
352
380
|
bridge = createInMemoryBridge();
|
|
353
|
-
bridges.set(
|
|
381
|
+
bridges.set(safeWorkspaceId, bridge);
|
|
354
382
|
}
|
|
355
383
|
return bridge;
|
|
356
384
|
};
|
|
@@ -360,6 +388,30 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
360
388
|
await ensureWorkspaceProvisioned(root);
|
|
361
389
|
return root;
|
|
362
390
|
};
|
|
391
|
+
const resourceLoaderOptionsByRoot = /* @__PURE__ */ new Map();
|
|
392
|
+
const getPluginResourceLoaderOptions = (root) => {
|
|
393
|
+
const resolvedRoot = path.resolve(root);
|
|
394
|
+
if (resourceLoaderOptionsByRoot.has(resolvedRoot)) {
|
|
395
|
+
return resourceLoaderOptionsByRoot.get(resolvedRoot);
|
|
396
|
+
}
|
|
397
|
+
const scopedPluginCollection = collectWorkspaceAgentServerPlugins({
|
|
398
|
+
workspaceRoot: resolvedRoot,
|
|
399
|
+
systemPromptAppend: options.systemPromptAppend,
|
|
400
|
+
resourceLoaderOptions: options.resourceLoaderOptions,
|
|
401
|
+
plugins: options.plugins,
|
|
402
|
+
excludeDefaults: options.excludeDefaults
|
|
403
|
+
});
|
|
404
|
+
resourceLoaderOptionsByRoot.set(
|
|
405
|
+
resolvedRoot,
|
|
406
|
+
scopedPluginCollection.agentOptions.resourceLoaderOptions
|
|
407
|
+
);
|
|
408
|
+
return scopedPluginCollection.agentOptions.resourceLoaderOptions;
|
|
409
|
+
};
|
|
410
|
+
const resolveResourceLoaderOptions = async (ctx) => {
|
|
411
|
+
const pluginOptions = getPluginResourceLoaderOptions(ctx.workspaceRoot);
|
|
412
|
+
const callerOptions = options.getResourceLoaderOptions ? await options.getResourceLoaderOptions(ctx) : void 0;
|
|
413
|
+
return mergeResourceLoaderOptions(pluginOptions, callerOptions);
|
|
414
|
+
};
|
|
363
415
|
await app.register(registerAgentRoutes, {
|
|
364
416
|
workspaceRoot,
|
|
365
417
|
sessionId: options.sessionId,
|
|
@@ -373,6 +425,9 @@ async function createCoreWorkspaceAgentServer(options = {}) {
|
|
|
373
425
|
],
|
|
374
426
|
systemPromptAppend: pluginCollection.agentOptions.systemPromptAppend,
|
|
375
427
|
resourceLoaderOptions: pluginCollection.agentOptions.resourceLoaderOptions,
|
|
428
|
+
getResourceLoaderOptions: resolveResourceLoaderOptions,
|
|
429
|
+
sessionNamespace: options.sessionNamespace,
|
|
430
|
+
getSessionNamespace: options.getSessionNamespace,
|
|
376
431
|
getExtraTools: async (ctx) => {
|
|
377
432
|
const callerTools = options.getExtraTools ? await options.getExtraTools(ctx) : [];
|
|
378
433
|
return [
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hachej/boring-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.14",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"description": "Foundation package for boring-ui-v2 apps: DB, auth, config, HTTP app factory, and frontend app shell.",
|
|
@@ -78,9 +78,9 @@
|
|
|
78
78
|
"react-router-dom": "^7.14.2",
|
|
79
79
|
"smol-toml": "^1.6.1",
|
|
80
80
|
"zod": "^3.25.76",
|
|
81
|
-
"@hachej/boring-agent": "0.1.
|
|
82
|
-
"@hachej/boring-ui-kit": "0.1.
|
|
83
|
-
"@hachej/boring-workspace": "0.1.
|
|
81
|
+
"@hachej/boring-agent": "0.1.14",
|
|
82
|
+
"@hachej/boring-ui-kit": "0.1.14",
|
|
83
|
+
"@hachej/boring-workspace": "0.1.14"
|
|
84
84
|
},
|
|
85
85
|
"devDependencies": {
|
|
86
86
|
"@testing-library/jest-dom": "^6.9.1",
|