@fugood/buttress-server 2.26.0-beta.1 → 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.
- package/README.md +52 -4
- package/config/function-samples/README.md +2 -0
- package/config/function-samples/bank-note.ts +47 -0
- package/config/function-samples/bank-watch-daemon.ts +63 -0
- package/lib/functions/bank-subscribe.d.ts +46 -0
- package/lib/functions/bank.d.ts +21 -0
- package/lib/functions/daemons.d.ts +45 -0
- package/lib/functions/executor.d.ts +25 -4
- package/lib/functions/index.d.ts +14 -6
- package/lib/functions/registry.d.ts +7 -1
- package/lib/functions/status.d.ts +48 -0
- package/lib/functions/templates.d.ts +3 -1
- package/lib/functions/types.d.ts +121 -0
- package/lib/index.mjs +254 -42
- package/lib/routes/anthropic-messages.d.ts +2 -2
- package/lib/routes/openai-compat.d.ts +2 -2
- package/lib/utils/cors.check.d.ts +1 -0
- package/lib/utils/cors.d.ts +72 -0
- package/lib/utils/workspaceState.d.ts +9 -0
- package/package.json +2 -2
- package/public/status.html +77 -1
|
@@ -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
|
|
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(
|
|
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
|
|
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(
|
|
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.
|
|
3
|
+
"version": "2.26.0-beta.2",
|
|
4
4
|
"main": "lib/index.mjs",
|
|
5
5
|
"types": "lib/index.d.ts",
|
|
6
6
|
"type": "module",
|
|
@@ -69,5 +69,5 @@
|
|
|
69
69
|
"tsdown": "^0.22.4",
|
|
70
70
|
"typescript": "^7.0.2"
|
|
71
71
|
},
|
|
72
|
-
"gitHead": "
|
|
72
|
+
"gitHead": "e216ee220c5fe1a29c033badb06fe90ba7252cb9"
|
|
73
73
|
}
|
package/public/status.html
CHANGED
|
@@ -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
|
|