@fugood/buttress-server 2.26.0-beta.0 → 2.26.0-beta.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.
@@ -11,7 +11,7 @@
11
11
  *
12
12
  * Note: This feature is experimental and may change in future versions.
13
13
  */
14
- import type { EventStream, Config } from '../types';
14
+ import type { EventStream } from '../types';
15
15
  /**
16
16
  * Stream an Anthropic Messages SSE response from the backend stream.
17
17
  *
@@ -52,4 +52,4 @@ export declare function streamAnthropicMessage(completionStream: ReadableStream<
52
52
  readonly event: "message_stop";
53
53
  readonly data: string;
54
54
  }, void, unknown>;
55
- export default function factory({ global: globalConfig }: Config): import("../types").ButtressApp;
55
+ export default function factory(): import("../types").ButtressApp;
@@ -6,7 +6,7 @@
6
6
  *
7
7
  * Note: This feature is experimental and may change in future versions.
8
8
  */
9
- import type { EventStream, Config } from '../types';
9
+ import type { EventStream } from '../types';
10
10
  /**
11
11
  * Stream an OpenAI-compatible chat completion (SSE) from the backend stream.
12
12
  * Mirrors collectChatCompletion (the non-streaming path).
@@ -14,4 +14,4 @@ import type { EventStream, Config } from '../types';
14
14
  export declare function streamChatCompletion(completionStream: ReadableStream<EventStream>, completionId: string, created: number, modelId: string, includeUsage: boolean): AsyncGenerator<{
15
15
  readonly data: string;
16
16
  }, void, unknown>;
17
- export default function factory({ global: globalConfig }: Config): import("../types").ButtressApp;
17
+ export default function factory(): import("../types").ButtressApp;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,72 @@
1
+ import type { Config } from '../types';
2
+ /**
3
+ * The server's whole CORS policy, decided in one place, by path.
4
+ *
5
+ * Every surface used to install its own `@elysiajs/cors` plugin on its own
6
+ * router. That does not do what it looks like it does: the plugin works through
7
+ * an `onRequest` hook, which Elysia runs *before* routing and therefore applies
8
+ * app-wide regardless of the router it was mounted on, and the plugins' default
9
+ * response headers all merge into one map on the root app. The four policies
10
+ * ended up fighting over a single set of `Access-Control-*` headers, last
11
+ * writer winning:
12
+ *
13
+ * - `/buttress/info` ran without an explicit `allowedHeaders`, which makes
14
+ * @elysiajs/cors echo the request's own header names back. That answer
15
+ * overwrote every other surface's allow-list, so `Content-Type` was missing
16
+ * from it and any browser `POST /functions/<name>` with a JSON body failed
17
+ * preflight — the symptom that surfaced this.
18
+ * - With that route disabled the *last registered* plugin's header list won
19
+ * instead, so `/anthropic-messages` was served `/functions`'s list and lost
20
+ * `x-api-key` / `anthropic-version`.
21
+ * - `handleOrigin` only ever sets `Access-Control-Allow-Origin` on a match and
22
+ * never clears it on a miss, so an origin allow-listed for one surface stayed
23
+ * reflected on the others — per-surface `cors_allowed_origins` meant nothing.
24
+ *
25
+ * Hence: one hook, one policy per request, chosen by path. A request that no
26
+ * policy owns gets no `Access-Control-*` headers at all.
27
+ *
28
+ * This is CORS only — it decides what a *browser* is willing to hand to script.
29
+ * It is not the access gate. `functionsAuthGuard` (cross-site `Origin` /
30
+ * `Sec-Fetch-Site` check) and `buttressAuthGuard` (workspace JWT) still run on
31
+ * every call and are what actually keeps callers out.
32
+ */
33
+ export type CorsPolicy = {
34
+ /** Path the policy owns: this exact path, or anything below it. */
35
+ path: string;
36
+ /** `true` reflects whatever `Origin` asked. Only for surfaces with no secrets. */
37
+ origin: true | string | string[];
38
+ methods: string[];
39
+ allowedHeaders: string[];
40
+ /** Preflight cache lifetime, in seconds. */
41
+ maxAge: number;
42
+ /**
43
+ * Emit `Access-Control-Allow-Credentials`. Never combined with `origin: true`:
44
+ * reflecting an arbitrary origin *and* allowing credentials is the pairing
45
+ * that turns a public endpoint into a cross-site read primitive.
46
+ */
47
+ credentials: boolean;
48
+ /** Answer Chrome's Private Network Access opt-in when the client asks for it. */
49
+ privateNetwork?: boolean;
50
+ };
51
+ /**
52
+ * Build the policy table from config. The `enable*` flags mirror the conditions
53
+ * `startServer` mounts each router under, so a surface that isn't served never
54
+ * gets a CORS answer either.
55
+ */
56
+ export declare const buildCorsPolicies: (config: Config, enabled?: {
57
+ openaiCompat?: boolean;
58
+ anthropicMessages?: boolean;
59
+ functions?: boolean;
60
+ }) => CorsPolicy[];
61
+ /**
62
+ * Install the policy table as the single writer of `Access-Control-*` headers.
63
+ *
64
+ * Preflights are answered from the hook rather than from `OPTIONS` routes: the
65
+ * hook runs before routing, so there is nothing to collide with a real route,
66
+ * and a preflight for an unmounted path simply falls through to a 404. A bare
67
+ * `OPTIONS` with no `Access-Control-Request-Method` is not a preflight and is
68
+ * left to routing.
69
+ */
70
+ export declare const installCors: <T extends {
71
+ onRequest: (handler: any) => unknown;
72
+ }>(app: T, policies: CorsPolicy[]) => T;
@@ -11,9 +11,18 @@ export interface ServerKeyPair {
11
11
  privateKeyPkcs8: string;
12
12
  kid: string;
13
13
  }
14
+ export interface BankBinding {
15
+ /** Public Data Bank API base URL, e.g. https://bank.bricks.tools */
16
+ endpoint: string;
17
+ spacename: string;
18
+ spacekey: string;
19
+ keyName?: string;
20
+ issuedAt?: string;
21
+ }
14
22
  export interface WorkspaceState {
15
23
  workspace: WorkspaceBinding | null;
16
24
  serverKeyPair: ServerKeyPair | null;
25
+ bank: BankBinding | null;
17
26
  }
18
27
  export declare const resolveStateDir: () => string;
19
28
  export declare const resolveStatePath: () => string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fugood/buttress-server",
3
- "version": "2.26.0-beta.0",
3
+ "version": "2.26.0-beta.2",
4
4
  "main": "lib/index.mjs",
5
5
  "types": "lib/index.d.ts",
6
6
  "type": "module",
@@ -43,7 +43,7 @@
43
43
  "@fugood/whisper.node": "^1.1.2",
44
44
  "@huggingface/gguf": "^0.3.2",
45
45
  "@iarna/toml": "^3.0.0",
46
- "@modelcontextprotocol/sdk": "^1.15.0",
46
+ "@modelcontextprotocol/sdk": "^1.30.0",
47
47
  "bytes": "^3.1.0",
48
48
  "chroma-js": "^2.1.2",
49
49
  "elysia": "^1.4.19",
@@ -69,5 +69,5 @@
69
69
  "tsdown": "^0.22.4",
70
70
  "typescript": "^7.0.2"
71
71
  },
72
- "gitHead": "782515bb3d092465f87e28ee6801a11b0e1f53f2"
72
+ "gitHead": "e216ee220c5fe1a29c033badb06fe90ba7252cb9"
73
73
  }
@@ -610,6 +610,16 @@
610
610
  <span class="badge badge-info" id="functionsCount">0 functions</span>
611
611
  </div>
612
612
  <div id="functionsSummary"></div>
613
+ <div class="section" id="functionsDaemonsSection" style="display:none">
614
+ <div class="section-title collapsible" onclick="toggleSection(this)">Daemons</div>
615
+ <div class="collapsible-content" id="functionsDaemons"></div>
616
+ </div>
617
+ <div class="section" id="functionsDaemonHistorySection" style="display:none">
618
+ <div class="section-title collapsible" onclick="toggleSection(this)">Daemon Activity</div>
619
+ <div class="collapsible-content" id="functionsDaemonHistory">
620
+ <div class="empty-state">No daemon activity</div>
621
+ </div>
622
+ </div>
613
623
  <div class="section">
614
624
  <div class="section-title collapsible" onclick="toggleSection(this)">Call History</div>
615
625
  <div class="collapsible-content" id="functionsCallHistory">
@@ -1287,8 +1297,10 @@
1287
1297
  if (!fns.enabled) return
1288
1298
 
1289
1299
  const count = fns.count ?? 0
1300
+ const daemonCount = fns.daemonCount ?? 0
1290
1301
  document.getElementById('functionsCount').textContent =
1291
- `${count} function${count !== 1 ? 's' : ''}`
1302
+ `${count} function${count !== 1 ? 's' : ''}` +
1303
+ (daemonCount > 0 ? ` · ${daemonCount} daemon${daemonCount !== 1 ? 's' : ''}` : '')
1292
1304
 
1293
1305
  const c = fns.counters || {}
1294
1306
  const summary = document.getElementById('functionsSummary')
@@ -1309,6 +1321,7 @@
1309
1321
  </thead>
1310
1322
  <tbody>
1311
1323
  ${stat('Calls', c.calls?.total ?? 0, c.calls?.failed ? `${c.calls.failed} failed` : '')}
1324
+ ${stat('Daemon runs', c.daemons?.invocations ?? 0, c.daemons?.failed ? `${c.daemons.failed} failed` : '')}
1312
1325
  ${stat('Uploads', `${c.uploads?.total ?? 0} (${formatBytes(c.uploads?.bytes ?? 0)})`, c.uploads?.failed ? `${c.uploads.failed} failed` : '')}
1313
1326
  ${stat('Downloads', `${c.downloads?.total ?? 0} (${formatBytes(c.downloads?.bytes ?? 0)})`, c.downloads?.missed ? `${c.downloads.missed} missed` : '')}
1314
1327
  ${stat('Auth checks', c.auth?.total ?? 0, c.auth?.denied ? `${c.auth.denied} denied` : '')}
@@ -1319,6 +1332,8 @@
1319
1332
  `
1320
1333
  })
1321
1334
 
1335
+ renderFunctionsDaemons(fns.daemons || [], fns.history?.daemons || [])
1336
+
1322
1337
  const history = fns.history || {}
1323
1338
  const statusBadge = i => i.success ?
1324
1339
  '<span class="badge badge-success">Success</span>' :
@@ -1362,6 +1377,67 @@
1362
1377
  ])
1363
1378
  }
1364
1379
 
1380
+ // Live daemon state + recent daemon event invocations
1381
+ function renderFunctionsDaemons(daemons, activity) {
1382
+ const hasDaemons = daemons.length > 0
1383
+ document.getElementById('functionsDaemonsSection').style.display = hasDaemons ? '' : 'none'
1384
+ document.getElementById('functionsDaemonHistorySection').style.display =
1385
+ hasDaemons || activity.length > 0 ? '' : 'none'
1386
+
1387
+ if (hasDaemons) {
1388
+ const container = document.getElementById('functionsDaemons')
1389
+ const stateBadge = d => d.state === 'running' ?
1390
+ '<span class="badge badge-success">Running</span>' :
1391
+ `<span class="badge badge-error">Error${d.error ? `: ${escapeHtml(d.error)}` : ''}</span>`
1392
+ const bankBadge = d => {
1393
+ if (!d.bankSubscriptions) return '-'
1394
+ const cls = d.bank === 'connected' ? 'badge-success' :
1395
+ d.bank === 'connecting' ? 'badge-info' : 'badge-warning'
1396
+ return `<span class="badge ${cls}">${escapeHtml(d.bank || 'connecting')} (${d.bankSubscriptions})</span>`
1397
+ }
1398
+ withScrollPreserve(container, () => {
1399
+ container.innerHTML = `
1400
+ <div class="table-wrapper">
1401
+ <div class="table-inner">
1402
+ <table>
1403
+ <thead>
1404
+ <tr>
1405
+ <th>Daemon</th><th>State</th><th>Started</th><th>Timers</th>
1406
+ <th>Bank</th><th>Events</th><th>Runs</th>
1407
+ </tr>
1408
+ </thead>
1409
+ <tbody>
1410
+ ${daemons.map(d => `
1411
+ <tr>
1412
+ <td title="${escapeHtml(d.description || '')}">${escapeHtml(d.name)}</td>
1413
+ <td>${stateBadge(d)}</td>
1414
+ <td><span class="timestamp">${d.startedAt ? formatRelativeTime(d.startedAt) : '-'}</span></td>
1415
+ <td>${d.timers ?? 0}</td>
1416
+ <td>${bankBadge(d)}</td>
1417
+ <td>${d.listening ? '<span class="badge badge-info">listening</span>' : '-'}</td>
1418
+ <td>${d.counts?.runs ?? 0}${d.counts?.failed ? ` <span class="badge badge-error">${d.counts.failed} failed</span>` : ''}</td>
1419
+ </tr>
1420
+ `).join('')}
1421
+ </tbody>
1422
+ </table>
1423
+ </div>
1424
+ </div>
1425
+ `
1426
+ })
1427
+ }
1428
+
1429
+ renderHistory('functionsDaemonHistory', activity, [
1430
+ { label: 'Time', render: i => `<span class="timestamp">${formatRelativeTime(i.timestamp)}</span>` },
1431
+ { label: 'Daemon', render: i => escapeHtml(i.name) },
1432
+ { label: 'Event', render: i => `<span class="badge badge-info">${escapeHtml(i.event || '-')}</span>` },
1433
+ { label: 'Duration', render: i => `${(i.durationMs / 1000).toFixed(2)}s` },
1434
+ { label: 'Status', render: i => i.success ?
1435
+ '<span class="badge badge-success">Success</span>' :
1436
+ `<span class="badge badge-error">Failed: ${escapeHtml(i.error || 'Unknown')}</span>`
1437
+ },
1438
+ ])
1439
+ }
1440
+
1365
1441
  // Fallback: Fetch status via HTTP polling
1366
1442
  let pollingInterval = null
1367
1443