@nifrajs/core 5.10.0 → 5.12.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/CHANGELOG.md +24 -0
- package/dist/edge-kit.d.ts +3 -2
- package/dist/edge-kit.d.ts.map +1 -1
- package/dist/edge-kit.js +3 -2
- package/dist/edge-kit.js.map +1 -1
- package/dist/guard-deny.d.ts +38 -0
- package/dist/guard-deny.d.ts.map +1 -0
- package/dist/guard-deny.js +43 -0
- package/dist/guard-deny.js.map +1 -0
- package/dist/index.d.ts +3 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/internal/capability-runtime.d.ts.map +1 -1
- package/dist/internal/capability-runtime.js +3 -2
- package/dist/internal/capability-runtime.js.map +1 -1
- package/dist/internal/route-execution.d.ts +6 -4
- package/dist/internal/route-execution.d.ts.map +1 -1
- package/dist/internal/route-execution.js.map +1 -1
- package/dist/internal/route-lanes.d.ts +4 -4
- package/dist/internal/route-lanes.d.ts.map +1 -1
- package/dist/internal/route-lanes.js +37 -37
- package/dist/internal/route-lanes.js.map +1 -1
- package/dist/manifest.d.ts.map +1 -1
- package/dist/manifest.js +2 -1
- package/dist/manifest.js.map +1 -1
- package/dist/server/body.d.ts +2 -0
- package/dist/server/body.d.ts.map +1 -1
- package/dist/server/body.js +2 -0
- package/dist/server/body.js.map +1 -1
- package/dist/server/core-copies.js +1 -1
- package/dist/server/idle-timeout.d.ts +1 -1
- package/dist/server/idle-timeout.d.ts.map +1 -1
- package/dist/server/idle-timeout.js.map +1 -1
- package/dist/server/json-response.d.ts +14 -0
- package/dist/server/json-response.d.ts.map +1 -1
- package/dist/server/json-response.js +205 -0
- package/dist/server/json-response.js.map +1 -1
- package/dist/server/server-types.d.ts +7 -1
- package/dist/server/server-types.d.ts.map +1 -1
- package/dist/server/server.d.ts +16 -24
- package/dist/server/server.d.ts.map +1 -1
- package/dist/server/server.js +285 -140
- package/dist/server/server.js.map +1 -1
- package/dist/server/static-response.d.ts +13 -0
- package/dist/server/static-response.d.ts.map +1 -0
- package/dist/server/static-response.js +28 -0
- package/dist/server/static-response.js.map +1 -0
- package/dist/server/websocket.d.ts +23 -7
- package/dist/server/websocket.d.ts.map +1 -1
- package/dist/server/websocket.js +8 -3
- package/dist/server/websocket.js.map +1 -1
- package/dist/server/ws-adapter-view.d.ts +3 -3
- package/dist/server/ws-adapter-view.d.ts.map +1 -1
- package/dist/server/ws-bun.d.ts +4 -4
- package/dist/server/ws-bun.d.ts.map +1 -1
- package/dist/server/ws-bun.js +12 -5
- package/dist/server/ws-bun.js.map +1 -1
- package/dist/server/ws-hook.d.ts +4 -4
- package/dist/server/ws-hook.d.ts.map +1 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.d.ts.map +1 -1
- package/dist/server.js +1 -0
- package/dist/server.js.map +1 -1
- package/dist/single-copy-register.js +5 -3
- package/dist/single-copy-register.js.map +1 -1
- package/dist/single-copy.d.ts +1 -1
- package/dist/single-copy.d.ts.map +1 -1
- package/dist/single-copy.js +49 -2
- package/dist/single-copy.js.map +1 -1
- package/dist/tool-contract.d.ts +5 -0
- package/dist/tool-contract.d.ts.map +1 -1
- package/dist/tool-contract.js +6 -3
- package/dist/tool-contract.js.map +1 -1
- package/dist/ws.d.ts +1 -1
- package/dist/ws.d.ts.map +1 -1
- package/dist/ws.js.map +1 -1
- package/package.json +6 -1
- package/src/edge-kit.ts +3 -2
- package/src/guard-deny.ts +61 -0
- package/src/index.ts +3 -1
- package/src/internal/capability-runtime.ts +3 -2
- package/src/internal/route-execution.ts +6 -4
- package/src/internal/route-lanes.ts +44 -44
- package/src/manifest.ts +2 -1
- package/src/server/body.ts +3 -0
- package/src/server/core-copies.ts +1 -1
- package/src/server/idle-timeout.ts +2 -4
- package/src/server/json-response.ts +209 -0
- package/src/server/server-types.ts +7 -1
- package/src/server/server.ts +414 -189
- package/src/server/static-response.ts +36 -0
- package/src/server/websocket.ts +29 -8
- package/src/server/ws-adapter-view.ts +3 -3
- package/src/server/ws-bun.ts +15 -9
- package/src/server/ws-hook.ts +4 -3
- package/src/server.ts +2 -0
- package/src/single-copy-register.ts +5 -3
- package/src/single-copy.ts +51 -3
- package/src/tool-contract.ts +13 -3
- package/src/ws.ts +1 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export interface StaticResponseDefinition {
|
|
2
|
+
readonly status: number
|
|
3
|
+
readonly statusText: string
|
|
4
|
+
readonly headers: Headers
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
const definitions = new WeakMap<object, StaticResponseDefinition>()
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Declare a bodyless response with fixed status and headers.
|
|
11
|
+
* Caller-supplied Content-Length and Transfer-Encoding are rejected.
|
|
12
|
+
*/
|
|
13
|
+
export function staticResponse(init?: ResponseInit): () => Response {
|
|
14
|
+
const normalized = new Response(null, init)
|
|
15
|
+
for (const name of ["content-length", "transfer-encoding"]) {
|
|
16
|
+
if (normalized.headers.has(name)) {
|
|
17
|
+
throw new TypeError(`staticResponse cannot set the framing header "${name}"`)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
const definition: StaticResponseDefinition = Object.freeze({
|
|
21
|
+
status: normalized.status,
|
|
22
|
+
statusText: normalized.statusText,
|
|
23
|
+
headers: new Headers(normalized.headers),
|
|
24
|
+
})
|
|
25
|
+
const handler = (): Response => createStaticResponse(definition)
|
|
26
|
+
definitions.set(handler, definition)
|
|
27
|
+
return Object.freeze(handler)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function staticResponseDefinition(handler: unknown): StaticResponseDefinition | undefined {
|
|
31
|
+
return typeof handler === "function" ? definitions.get(handler) : undefined
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function createStaticResponse(definition: StaticResponseDefinition): Response {
|
|
35
|
+
return new Response(null, definition)
|
|
36
|
+
}
|
package/src/server/websocket.ts
CHANGED
|
@@ -51,13 +51,30 @@ export interface NifraWebSocket<Data = unknown> {
|
|
|
51
51
|
readonly raw: unknown
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Per-server topic membership and broadcast adapter. The default {@link TopicRegistry} keeps sockets
|
|
56
|
+
* in this process; a custom adapter can bridge those operations to a shared fan-out service. The
|
|
57
|
+
* membership methods are synchronous because socket callbacks are synchronous; `publish` may be
|
|
58
|
+
* asynchronous, in which case callers can await `app.publish()`.
|
|
59
|
+
*/
|
|
60
|
+
export interface TopicAdapter {
|
|
61
|
+
/** Add an open socket to a topic; ignore `readyState >= 2` and make repeats idempotent. */
|
|
62
|
+
subscribe(topic: string, ws: NifraWebSocket): void
|
|
63
|
+
/** Remove a socket from one topic; repeated unsubscriptions must be idempotent. */
|
|
64
|
+
unsubscribe(topic: string, ws: NifraWebSocket): void
|
|
65
|
+
/** Drop every topic membership for a socket when it closes. */
|
|
66
|
+
unsubscribeAll(ws: NifraWebSocket): void
|
|
67
|
+
/** Broadcast a frame to the topic; asynchronous fan-out is returned to `app.publish()`. */
|
|
68
|
+
publish(topic: string, data: string | ArrayBufferView | ArrayBuffer): void | Promise<void>
|
|
69
|
+
}
|
|
70
|
+
|
|
54
71
|
/**
|
|
55
72
|
* In-process pub/sub for `ws.subscribe(topic)` + `app.publish(topic, data)`. **Single-instance only** -
|
|
56
73
|
* topics live in this process's memory, so a multi-instance deploy (multiple servers behind a load
|
|
57
|
-
* balancer) needs
|
|
58
|
-
*
|
|
74
|
+
* balancer) needs a {@link TopicAdapter} backed by a shared fan-out. The same registry instance is
|
|
75
|
+
* shared by an app's connections and its `publish`.
|
|
59
76
|
*/
|
|
60
|
-
export class TopicRegistry {
|
|
77
|
+
export class TopicRegistry implements TopicAdapter {
|
|
61
78
|
private readonly topics = new Map<string, Set<NifraWebSocket>>()
|
|
62
79
|
private readonly memberships = new WeakMap<NifraWebSocket, Set<string>>()
|
|
63
80
|
|
|
@@ -234,8 +251,8 @@ export type WebSocketUpgradeOutcome =
|
|
|
234
251
|
readonly kind: "upgrade"
|
|
235
252
|
readonly handler: WebSocketHandler
|
|
236
253
|
readonly data: unknown
|
|
237
|
-
/** The app's
|
|
238
|
-
readonly pubsub:
|
|
254
|
+
/** The app's topic adapter - the adapter wires `ws.subscribe` + close-cleanup to it. */
|
|
255
|
+
readonly pubsub: TopicAdapter
|
|
239
256
|
/** The installed runtime's {@link attachWebSocket}, carried on the outcome so an adapter can wire
|
|
240
257
|
* a standard socket without a static import of the WS implementation (which would defeat the
|
|
241
258
|
* `.use(websocket())` tree-shaking). `@nifrajs/workers` may import `attachWebSocket` directly instead. */
|
|
@@ -259,7 +276,7 @@ export type WsAttach = (
|
|
|
259
276
|
socket: StandardWebSocket,
|
|
260
277
|
handler: WebSocketHandler,
|
|
261
278
|
data: unknown,
|
|
262
|
-
options: { openNow: boolean; pubsub:
|
|
279
|
+
options: { openNow: boolean; pubsub: TopicAdapter; maxPayloadBytes?: number | undefined },
|
|
263
280
|
) => NifraWebSocket
|
|
264
281
|
|
|
265
282
|
/**
|
|
@@ -299,7 +316,7 @@ export function attachWebSocket<
|
|
|
299
316
|
socket: StandardWebSocket,
|
|
300
317
|
handler: WebSocketHandler<Data, Env, Schema, Send>,
|
|
301
318
|
data: unknown,
|
|
302
|
-
options: { openNow: boolean; pubsub:
|
|
319
|
+
options: { openNow: boolean; pubsub: TopicAdapter; maxPayloadBytes?: number | undefined },
|
|
303
320
|
): NifraWebSocket<Data> {
|
|
304
321
|
const { pubsub } = options
|
|
305
322
|
let ws!: NifraWebSocket<Data>
|
|
@@ -372,7 +389,11 @@ export function attachWebSocket<
|
|
|
372
389
|
socket.addEventListener(
|
|
373
390
|
"close",
|
|
374
391
|
(event: { readonly code?: number; readonly reason?: string }) => {
|
|
375
|
-
|
|
392
|
+
try {
|
|
393
|
+
pubsub.unsubscribeAll(ws)
|
|
394
|
+
} catch (error) {
|
|
395
|
+
reportError(error)
|
|
396
|
+
}
|
|
376
397
|
const [code, reason] = sentClose ?? [event.code ?? 1005, event.reason ?? ""]
|
|
377
398
|
safe(() => handler.close?.(ws, code, reason))
|
|
378
399
|
},
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* and outcome types carry the schemas and codecs the adapters never touch.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import type {
|
|
7
|
+
import type { TopicAdapter, WebSocketHandler, WebSocketUpgradeOutcome } from "./websocket.ts"
|
|
8
8
|
|
|
9
9
|
/** A received frame, normalized: text → `string`, binary → `Uint8Array`. */
|
|
10
10
|
export type NifraWsData = string | Uint8Array
|
|
@@ -20,7 +20,7 @@ export interface NifraWs {
|
|
|
20
20
|
readonly raw: unknown
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
/** The
|
|
23
|
+
/** The socket-membership surface the adapter wires `ws.subscribe` to. */
|
|
24
24
|
export interface WsPubSub {
|
|
25
25
|
subscribe(topic: string, ws: NifraWs): void
|
|
26
26
|
unsubscribe(topic: string, ws: NifraWs): void
|
|
@@ -59,5 +59,5 @@ type Fits<View, Actual extends View> = Actual
|
|
|
59
59
|
export type CoreFitsAdapterView = [
|
|
60
60
|
Fits<WsUpgradeOutcome, WebSocketUpgradeOutcome>,
|
|
61
61
|
Fits<NifraWsHandler, WebSocketHandler>,
|
|
62
|
-
Fits<WsPubSub,
|
|
62
|
+
Fits<WsPubSub, TopicAdapter>,
|
|
63
63
|
]
|
package/src/server/ws-bun.ts
CHANGED
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
createWebSocketSender,
|
|
11
11
|
exceedsPayloadCap,
|
|
12
12
|
type NifraWebSocket,
|
|
13
|
-
type
|
|
13
|
+
type TopicAdapter,
|
|
14
14
|
type WebSocketData,
|
|
15
15
|
type WebSocketHandler,
|
|
16
16
|
} from "./websocket.ts"
|
|
@@ -20,8 +20,8 @@ import type { BunWsHandlers } from "./ws-hook.ts"
|
|
|
20
20
|
* `upgrade()`-seeded user data, and the memoized portable {@link NifraWebSocket} wrapper. */
|
|
21
21
|
export interface BunWsData {
|
|
22
22
|
readonly handler: WebSocketHandler
|
|
23
|
-
/** Mounted/modern upgrades carry
|
|
24
|
-
readonly pubsub?:
|
|
23
|
+
/** Mounted/modern upgrades carry their topic adapter; old/manual fixtures may omit it. */
|
|
24
|
+
readonly pubsub?: TopicAdapter
|
|
25
25
|
/** The inbound cap of the app this socket belongs to (the upgrade outcome's). Absent, the
|
|
26
26
|
* listening app's applies. */
|
|
27
27
|
readonly maxPayloadBytes?: number | undefined
|
|
@@ -79,18 +79,18 @@ function toBinaryData(message: ArrayBuffer | Uint8Array): Uint8Array {
|
|
|
79
79
|
*
|
|
80
80
|
* `nativePubsub` selects the topic backend: in native mode `subscribe`/`unsubscribe` go straight to
|
|
81
81
|
* Bun's own (uWebSockets) pub/sub so a broadcast via the Bun server's `publish` reaches this socket
|
|
82
|
-
* with no per-connection JS loop; otherwise they use the
|
|
82
|
+
* with no per-connection JS loop; otherwise they use the configured topic adapter. Native
|
|
83
83
|
* mode is used only when no route validates outbound frames, so raw and validated sends are identical.
|
|
84
84
|
*/
|
|
85
85
|
function wrapBunSocket(
|
|
86
86
|
raw: BunSocket,
|
|
87
|
-
topics:
|
|
87
|
+
topics: TopicAdapter,
|
|
88
88
|
handler: WebSocketHandler,
|
|
89
89
|
nativePubsub: boolean,
|
|
90
90
|
): NifraWebSocket {
|
|
91
91
|
let ws!: NifraWebSocket
|
|
92
|
-
// Mounted child apps carry their own
|
|
93
|
-
// fallback for old/manual Bun upgrades, but use the per-connection
|
|
92
|
+
// Mounted child apps carry their own topic adapter in the upgrade outcome. Keep the argument as a
|
|
93
|
+
// fallback for old/manual Bun upgrades, but use the per-connection adapter for composed mounts.
|
|
94
94
|
const pubsub = raw.data.pubsub ?? topics
|
|
95
95
|
const reportError = (error: unknown): void => reportWsError(error, ws, handler)
|
|
96
96
|
const send = createWebSocketSender(
|
|
@@ -167,7 +167,7 @@ const BUN_TOO_BIG_REASON = "Received too big message"
|
|
|
167
167
|
* `maxPayloadLength`; a socket whose app declares less is closed with `1009` here.
|
|
168
168
|
*/
|
|
169
169
|
export function createBunWsHandlers(
|
|
170
|
-
topics:
|
|
170
|
+
topics: TopicAdapter,
|
|
171
171
|
nativePubsub = false,
|
|
172
172
|
app?: object,
|
|
173
173
|
): BunWsHandlers {
|
|
@@ -209,7 +209,13 @@ export function createBunWsHandlers(
|
|
|
209
209
|
const nifra = ws.data.nifra ?? wrapBunSocket(ws, topics, ws.data.handler, nativePubsub)
|
|
210
210
|
// Registry mode: drop topic subscriptions so it never holds a dead socket. Native mode: Bun
|
|
211
211
|
// unsubscribes the socket from every topic on close, so there is nothing to sweep here.
|
|
212
|
-
if (!nativePubsub)
|
|
212
|
+
if (!nativePubsub) {
|
|
213
|
+
try {
|
|
214
|
+
;(ws.data.pubsub ?? topics).unsubscribeAll(nifra)
|
|
215
|
+
} catch (error) {
|
|
216
|
+
reportWsError(error, nifra, ws.data.handler)
|
|
217
|
+
}
|
|
218
|
+
}
|
|
213
219
|
// Bun's own maxPayloadLength reports 1006 here, whether it dropped the connection (up to 1.4.3)
|
|
214
220
|
// or sent the peer a 1009 (from 1.4.4); the handler gets the 1009 every other payload cap reports.
|
|
215
221
|
const oversize = code === 1006 && reason === BUN_TOO_BIG_REASON
|
package/src/server/ws-hook.ts
CHANGED
|
@@ -12,13 +12,14 @@ import type { ProtoPoisoning } from "./proto-guard.ts"
|
|
|
12
12
|
import type {
|
|
13
13
|
NifraWebSocket,
|
|
14
14
|
StandardWebSocket,
|
|
15
|
+
TopicAdapter,
|
|
15
16
|
TopicRegistry,
|
|
16
17
|
WebSocketContext,
|
|
17
18
|
WebSocketHandler,
|
|
18
19
|
WsSender,
|
|
19
20
|
} from "./websocket.ts"
|
|
20
21
|
|
|
21
|
-
/** The Bun serve `websocket` callback trio, closed over one app's
|
|
22
|
+
/** The Bun serve `websocket` callback trio, closed over one app's topic adapter. The `ws`
|
|
22
23
|
* params are `unknown` so `Bun.*` types never leak into the public `.d.ts`; the implementation
|
|
23
24
|
* narrows to its structural `BunSocket` view. */
|
|
24
25
|
export interface BunWsHandlers {
|
|
@@ -43,13 +44,13 @@ export interface WsRuntime {
|
|
|
43
44
|
/** The Bun `websocket` config for `listen()` when the app has WS routes. `nativePubsub` routes
|
|
44
45
|
* `ws.subscribe` through Bun's own pub/sub (set when no route validates outbound frames). `app` is
|
|
45
46
|
* the listening server, whose mount table names the payload caps the listener has to admit. */
|
|
46
|
-
bunHandlers(topics:
|
|
47
|
+
bunHandlers(topics: TopicAdapter, nativePubsub?: boolean, app?: object): BunWsHandlers
|
|
47
48
|
/** `attachWebSocket` - wires a standard server socket (Workers `WebSocketPair`) to a handler. */
|
|
48
49
|
attach(
|
|
49
50
|
socket: StandardWebSocket,
|
|
50
51
|
handler: WebSocketHandler,
|
|
51
52
|
data: unknown,
|
|
52
|
-
options: { openNow: boolean; pubsub:
|
|
53
|
+
options: { openNow: boolean; pubsub: TopicAdapter },
|
|
53
54
|
): NifraWebSocket
|
|
54
55
|
/** `webSocketSender` - `ws.send` for an adapter that wires its own sockets (Node, Deno). */
|
|
55
56
|
sender: WsSender
|
package/src/server.ts
CHANGED
|
@@ -177,9 +177,11 @@ export type {
|
|
|
177
177
|
SSEStream,
|
|
178
178
|
TypedSSEStream,
|
|
179
179
|
} from "./server/sse.ts"
|
|
180
|
+
export { staticResponse } from "./server/static-response.ts"
|
|
180
181
|
export type {
|
|
181
182
|
NifraWebSocket,
|
|
182
183
|
StandardWebSocket,
|
|
184
|
+
TopicAdapter,
|
|
183
185
|
WebSocketContext,
|
|
184
186
|
WebSocketData,
|
|
185
187
|
WebSocketHandler,
|
|
@@ -14,10 +14,12 @@
|
|
|
14
14
|
* A resolver only affects imports that have not resolved yet, and a module's own imports resolve
|
|
15
15
|
* before its body runs. Registering from inside application code would therefore miss exactly the
|
|
16
16
|
* imports it exists to redirect. `preload` runs before the entry point, which is the only moment that
|
|
17
|
-
* covers the whole graph.
|
|
17
|
+
* covers the whole graph. When Bun starts a filtered workspace script below the app root, the
|
|
18
|
+
* registrar finds the nearest ancestor package declaring `nifra.singleCopy` and passes that root to
|
|
19
|
+
* the resolver instead of planning against the workspace package's manifest.
|
|
18
20
|
*
|
|
19
|
-
* `nifra check`
|
|
20
|
-
*
|
|
21
|
+
* `nifra check` recognizes this public specifier and verifies checkout-local relative preloads before
|
|
22
|
+
* treating them as proof that the runtime arm is armed.
|
|
21
23
|
*
|
|
22
24
|
* A declared package that cannot be collapsed (two versions, or a file with no counterpart in the
|
|
23
25
|
* app's copy) prints one warning per package naming both copies. Set
|
package/src/single-copy.ts
CHANGED
|
@@ -130,7 +130,7 @@ export interface SingleCopyPlan {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
export interface SingleCopyOptions {
|
|
133
|
-
/** The app root whose copy wins. Defaults to `process.cwd()
|
|
133
|
+
/** The app root whose copy wins. Defaults to its nearest declaration, or `process.cwd()` if none. */
|
|
134
134
|
readonly cwd?: string
|
|
135
135
|
/** Package names or `@scope/*` patterns. Defaults to the `nifra.singleCopy` declaration. */
|
|
136
136
|
readonly packages?: readonly string[]
|
|
@@ -179,6 +179,21 @@ const singleCopyField = (cwd: string): unknown => {
|
|
|
179
179
|
return isRecord(nifra) ? nifra.singleCopy : undefined
|
|
180
180
|
}
|
|
181
181
|
|
|
182
|
+
/** Find the declaring app when a Bun workspace script runs from a package directory. */
|
|
183
|
+
const singleCopyProjectRoot = (cwd: string): string => {
|
|
184
|
+
const start = resolve(cwd)
|
|
185
|
+
let current = start
|
|
186
|
+
for (let depth = 0; depth < MAX_ANCESTOR_DIRS; depth++) {
|
|
187
|
+
const pkg = readJsonSync(join(current, "package.json"))
|
|
188
|
+
const nifra = pkg?.nifra
|
|
189
|
+
if (isRecord(nifra) && Object.hasOwn(nifra, "singleCopy")) return realpathOrSelf(current)
|
|
190
|
+
const parent = dirname(current)
|
|
191
|
+
if (parent === current) break
|
|
192
|
+
current = parent
|
|
193
|
+
}
|
|
194
|
+
return realpathOrSelf(start)
|
|
195
|
+
}
|
|
196
|
+
|
|
182
197
|
/**
|
|
183
198
|
* The declaration, read from `package.json` - deliberately NOT from `nifra.config.ts`.
|
|
184
199
|
*
|
|
@@ -266,6 +281,34 @@ function preloadEntries(value: string): readonly string[] {
|
|
|
266
281
|
return entries
|
|
267
282
|
}
|
|
268
283
|
|
|
284
|
+
/** Accept the public entry, or a relative source preload that is itself the registrar. */
|
|
285
|
+
function isSingleCopyRegistrar(entry: string, cwd: string): boolean {
|
|
286
|
+
if (entry === SINGLE_COPY_REGISTER_SPECIFIER) return true
|
|
287
|
+
if (!entry.startsWith(".")) return false
|
|
288
|
+
let source: string
|
|
289
|
+
try {
|
|
290
|
+
source = readFileSync(resolve(cwd, entry), "utf8")
|
|
291
|
+
} catch {
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
const lines = source
|
|
295
|
+
.replaceAll("\r\n", "\n")
|
|
296
|
+
.split("\n")
|
|
297
|
+
.map((line) => line.trim())
|
|
298
|
+
const importsRegistrar = lines.some((line) => {
|
|
299
|
+
const statement = line.endsWith(";") ? line.slice(0, -1) : line
|
|
300
|
+
return (
|
|
301
|
+
statement === 'import { registerSingleCopy } from "./single-copy"' ||
|
|
302
|
+
statement === "import { registerSingleCopy } from './single-copy'"
|
|
303
|
+
)
|
|
304
|
+
})
|
|
305
|
+
const callsRegistrar = lines.some((line) => {
|
|
306
|
+
const statement = line.endsWith(";") ? line.slice(0, -1) : line
|
|
307
|
+
return statement === "registerSingleCopy()"
|
|
308
|
+
})
|
|
309
|
+
return importsRegistrar && callsRegistrar
|
|
310
|
+
}
|
|
311
|
+
|
|
269
312
|
/**
|
|
270
313
|
* Read the runtime proof out of `bunfig.toml`.
|
|
271
314
|
*
|
|
@@ -294,7 +337,10 @@ export function readSingleCopyRegistration(cwd: string): SingleCopyRegistration
|
|
|
294
337
|
section = heading.trim()
|
|
295
338
|
continue
|
|
296
339
|
}
|
|
297
|
-
if (
|
|
340
|
+
if (
|
|
341
|
+
value === undefined ||
|
|
342
|
+
!preloadEntries(value).some((entry) => isSingleCopyRegistrar(entry, cwd))
|
|
343
|
+
)
|
|
298
344
|
continue
|
|
299
345
|
if (section === "test") test = true
|
|
300
346
|
else if (section === "") run = true
|
|
@@ -726,7 +772,9 @@ const strictFromEnv = (): boolean => {
|
|
|
726
772
|
* anything loads, and a `no-counterpart` file fails its own import.
|
|
727
773
|
*/
|
|
728
774
|
export function registerSingleCopy(options: SingleCopyOptions = {}): SingleCopyPlan {
|
|
729
|
-
|
|
775
|
+
// Bun runs `--filter` scripts from the workspace package directory. Resolve the declaration root
|
|
776
|
+
// first so the preload still pins packages against the application tree that owns bunfig.toml.
|
|
777
|
+
const root = realpathOrSelf(resolve(options.cwd ?? singleCopyProjectRoot(process.cwd())))
|
|
730
778
|
const strict = options.strict ?? (readSingleCopyStrict(root) || strictFromEnv())
|
|
731
779
|
const warned = processSet(WARNED)
|
|
732
780
|
const emit = (key: string, message: string): void => {
|
package/src/tool-contract.ts
CHANGED
|
@@ -84,6 +84,11 @@ export interface ToolContractOptions<
|
|
|
84
84
|
readonly description: string
|
|
85
85
|
readonly input: InputSchema
|
|
86
86
|
readonly output: OutputSchema
|
|
87
|
+
/**
|
|
88
|
+
* The capability a caller must hold to run the tool. Defaults to `tool.execute.<name>`, valid for
|
|
89
|
+
* any name made of lowercase segments joined by `.`, `-` or `_` (`list_orders`); a name with an
|
|
90
|
+
* empty segment (`a__b`, a trailing `-`) derives none and must pass one.
|
|
91
|
+
*/
|
|
87
92
|
readonly capability?: string
|
|
88
93
|
readonly approval?: ToolApprovalPolicy
|
|
89
94
|
readonly idempotency?: ToolIdempotencyPolicy<Input>
|
|
@@ -398,8 +403,13 @@ export function defineTool<
|
|
|
398
403
|
if (!isStandardSchema(options.input) || !isStandardSchema(options.output)) {
|
|
399
404
|
throw new TypeError("tool contract: input and output must implement Standard Schema")
|
|
400
405
|
}
|
|
401
|
-
|
|
402
|
-
|
|
406
|
+
const capability = options.capability ?? `${DEFAULT_CAPABILITY}.${options.name}`
|
|
407
|
+
if (typeof capability !== "string" || !validCapabilityId(capability)) {
|
|
408
|
+
throw new TypeError(
|
|
409
|
+
options.capability === undefined
|
|
410
|
+
? `tool contract: name ${JSON.stringify(options.name)} derives no valid capability; pass \`capability\``
|
|
411
|
+
: `tool contract: invalid capability ${JSON.stringify(options.capability)}`,
|
|
412
|
+
)
|
|
403
413
|
}
|
|
404
414
|
const approval = validateApprovalPolicy(options.approval ?? { kind: "none" })
|
|
405
415
|
const cost =
|
|
@@ -415,7 +425,7 @@ export function defineTool<
|
|
|
415
425
|
description: options.description.trim(),
|
|
416
426
|
input: options.input,
|
|
417
427
|
output: options.output,
|
|
418
|
-
capability
|
|
428
|
+
capability,
|
|
419
429
|
approval,
|
|
420
430
|
...(options.idempotency === undefined ? {} : { idempotency: options.idempotency }),
|
|
421
431
|
...(cost === undefined ? {} : { cost }),
|