@abide/abide 0.124.1 → 0.125.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/ABIDE.md +4 -0
- package/CHANGELOG.md +36 -0
- package/bin/abide.ts +3 -2
- package/docs/REGISTRY.md +66 -40
- package/docs/RULEBOOK.md +202 -18
- package/docs/abide.md +391 -67
- package/package.json +1 -1
- package/src/compiler/appMap.ts +14 -1
- package/src/compiler/declaredExports.ts +41 -0
- package/src/compiler/handlersUnder.ts +1 -0
- package/src/compiler/hookCalls.ts +1 -0
- package/src/compiler/projectShadow.ts +52 -2
- package/src/compiler/readsIn.ts +39 -34
- package/src/server/PageRender.ts +7 -8
- package/src/server/abideModules.ts +6 -0
- package/src/server/answerRequest.ts +115 -52
- package/src/server/answerSocket.ts +6 -5
- package/src/server/answerWorker.ts +34 -23
- package/src/server/assistant.ts +141 -35
- package/src/server/banner.ts +9 -5
- package/src/server/bridgeRelay.ts +3 -3
- package/src/server/callerOf.ts +5 -0
- package/src/server/claudeCode.ts +20 -11
- package/src/server/commands.ts +14 -7
- package/src/server/createApp.ts +65 -12
- package/src/server/devWorker.ts +18 -16
- package/src/server/generatedSurfaces.ts +6 -5
- package/src/server/handlerFileMount.ts +2 -0
- package/src/server/hooks.ts +54 -8
- package/src/server/index.ts +18 -4
- package/src/server/invokeHandler.ts +1 -1
- package/src/server/mountHandlers.ts +6 -0
- package/src/server/principal.ts +3 -2
- package/src/server/render.ts +11 -2
- package/src/server/requestScope.ts +61 -1
- package/src/server/responseRelay.ts +9 -3
- package/src/server/serveDev.ts +18 -3
- package/src/server/sqliteStore.ts +65 -0
- package/src/server/testMount.ts +1 -0
- package/src/shared/AssistantEvent.ts +40 -0
- package/src/shared/BrowserHooks.ts +73 -0
- package/src/shared/CURRENT_ROUTER.ts +3 -1
- package/src/shared/Issues.ts +11 -3
- package/src/shared/Middleware.ts +9 -9
- package/src/shared/UNEXPECTED.ts +1 -1
- package/src/shared/answerOf.ts +9 -4
- package/src/shared/imagePiece.ts +85 -0
- package/src/shared/index.ts +17 -0
- package/src/shared/isServer.ts +15 -0
- package/src/shared/navigate.ts +10 -3
- package/src/shared/onError.ts +51 -0
- package/src/shared/onRefusal.ts +47 -0
- package/src/shared/online.ts +5 -1
- package/src/shared/reactive/Keyed.ts +12 -1
- package/src/shared/reactive/Reactive.ts +53 -25
- package/src/shared/reactive/cellShape.ts +4 -3
- package/src/shared/reactive/poll.ts +49 -0
- package/src/shared/reactive/scoped.ts +10 -1
- package/src/shared/reactive/scoping.ts +14 -0
- package/src/shared/reactive/watch.ts +19 -8
- package/src/shared/resultsOf.ts +21 -0
- package/src/shared/route.ts +18 -4
- package/src/shared/rpc.ts +112 -26
- package/src/shared/seededDocument.ts +5 -6
- package/src/shared/storedValue.ts +32 -0
- package/src/shared/webStore.ts +54 -0
- package/src/ui/Router.ts +42 -9
- package/src/ui/outboxQueue.ts +1 -1
- package/src/ui/wire.ts +4 -4
- package/src/shared/KEEP_OPENED.ts +0 -7
- /package/src/{server → shared}/IMAGE_TYPES.ts +0 -0
package/src/server/createApp.ts
CHANGED
|
@@ -17,7 +17,7 @@ import { PRINCIPAL_HEADER } from '#shared/PRINCIPAL_HEADER.ts'
|
|
|
17
17
|
import { RPC_SERVER } from '#shared/RPC_SERVER.ts'
|
|
18
18
|
|
|
19
19
|
import { laneFailure } from './answerFailure.ts'
|
|
20
|
-
import { answerRequest, type AppSources } from './answerRequest.ts'
|
|
20
|
+
import { answerRequest, type AppSources, laneOf } from './answerRequest.ts'
|
|
21
21
|
import { Mux, type MuxConnection } from './answerSocket.ts'
|
|
22
22
|
import {
|
|
23
23
|
type Builder,
|
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
spawnedBuild,
|
|
27
27
|
} from './appBuild.ts'
|
|
28
28
|
import { holdsApp } from './appTree.ts'
|
|
29
|
+
import { presumeCaller } from './callerOf.ts'
|
|
29
30
|
import {
|
|
30
31
|
APP_PACKAGE,
|
|
31
32
|
type AppPackage,
|
|
@@ -35,7 +36,14 @@ import {
|
|
|
35
36
|
} from './config.ts'
|
|
36
37
|
import { Followers } from './Followers.ts'
|
|
37
38
|
import { McpSurface } from './generatedSurfaces.ts'
|
|
38
|
-
import {
|
|
39
|
+
import {
|
|
40
|
+
type AppContext,
|
|
41
|
+
HOOKS,
|
|
42
|
+
inOrder,
|
|
43
|
+
type Lane,
|
|
44
|
+
reportUnexpected,
|
|
45
|
+
rootRungs,
|
|
46
|
+
} from './hooks.ts'
|
|
39
47
|
import { IDLE_TIMEOUT_SECONDS } from './IDLE_TIMEOUT_SECONDS.ts'
|
|
40
48
|
import { installServer } from './installServer.ts'
|
|
41
49
|
import { followedIn, type Mount } from './mountHandlers.ts'
|
|
@@ -51,6 +59,7 @@ import {
|
|
|
51
59
|
holdScope,
|
|
52
60
|
inReleasedScope,
|
|
53
61
|
LISTENING,
|
|
62
|
+
RECEIVED_FROM,
|
|
54
63
|
rungsTook,
|
|
55
64
|
serverTiming,
|
|
56
65
|
} from './requestScope.ts'
|
|
@@ -65,12 +74,15 @@ const REQUEST_LOG = namedLogger('abide:request')
|
|
|
65
74
|
// by it.
|
|
66
75
|
const STOP_TIMEOUT = 5000
|
|
67
76
|
|
|
77
|
+
/** Who a scope `App.run` or `App.fetch` opens is answered as, over what its request's cookies say. */
|
|
78
|
+
type Presumed = { principal?: Record<string, unknown>; caller?: string }
|
|
79
|
+
|
|
68
80
|
/** The app object: what a host, a test or a script holds the app as. */
|
|
69
81
|
export interface App {
|
|
70
|
-
fetch(request: Request): Promise<Response>
|
|
82
|
+
fetch(request: Request, options?: Presumed): Promise<Response>
|
|
71
83
|
run<Result>(
|
|
72
84
|
fn: () => Result | Promise<Result>,
|
|
73
|
-
options?: { request?: Request
|
|
85
|
+
options?: { request?: Request } & Presumed,
|
|
74
86
|
): Promise<Result>
|
|
75
87
|
listen(options?: {
|
|
76
88
|
port?: number
|
|
@@ -247,8 +259,15 @@ export async function bootEntered(
|
|
|
247
259
|
// route's rungs, the ones below it running after routing (37.2, `answerRequest`). 21.20 — the
|
|
248
260
|
// center takes its own time back out of what `fetch` counts for the whole onion.
|
|
249
261
|
// 37.14 — before routing, the lane is the request's to say (15.22).
|
|
250
|
-
const rungs = rootRungs(
|
|
251
|
-
|
|
262
|
+
const rungs = rootRungs(
|
|
263
|
+
(failure, request) =>
|
|
264
|
+
laneFailure(
|
|
265
|
+
booted,
|
|
266
|
+
new URL(request.url).pathname,
|
|
267
|
+
failure,
|
|
268
|
+
request,
|
|
269
|
+
),
|
|
270
|
+
(request) => laneOf(booted, request),
|
|
252
271
|
)
|
|
253
272
|
const answer = compose(rungs, async ({ request }) => {
|
|
254
273
|
const began = performance.now()
|
|
@@ -265,23 +284,32 @@ export async function bootEntered(
|
|
|
265
284
|
let stopping = false
|
|
266
285
|
let bound: Bun.Server<unknown> | undefined
|
|
267
286
|
|
|
287
|
+
// 42.19, 42.20, 42.21 — inside the scope, ahead of anything that reads either.
|
|
288
|
+
const presume = (options: Presumed | undefined): void => {
|
|
289
|
+
if (options?.principal !== undefined)
|
|
290
|
+
presumePrincipal(options.principal)
|
|
291
|
+
if (options?.caller !== undefined) presumeCaller(options.caller)
|
|
292
|
+
}
|
|
293
|
+
|
|
268
294
|
const run: App['run'] = async (fn, options) => {
|
|
269
|
-
|
|
270
|
-
//
|
|
295
|
+
// 42.19, 42.21 — a principal or a caller is a request's, so one given alone is given a request
|
|
296
|
+
// to be.
|
|
271
297
|
const request =
|
|
272
298
|
options?.request ??
|
|
273
|
-
(
|
|
299
|
+
(options?.principal === undefined && options?.caller === undefined
|
|
300
|
+
? undefined
|
|
301
|
+
: new Request(IN_PROCESS_ORIGIN))
|
|
274
302
|
return inReleasedScope(
|
|
275
303
|
request,
|
|
276
304
|
async () => {
|
|
277
|
-
|
|
305
|
+
presume(options)
|
|
278
306
|
return await fn()
|
|
279
307
|
},
|
|
280
308
|
answering,
|
|
281
309
|
)
|
|
282
310
|
}
|
|
283
311
|
|
|
284
|
-
const fetch = async (given
|
|
312
|
+
const fetch: App['fetch'] = async (given, options) => {
|
|
285
313
|
if (stopping) return plainAnswer('the app is stopping', 503)
|
|
286
314
|
const request = belowMount(given)
|
|
287
315
|
if (request instanceof Response) return request
|
|
@@ -294,9 +322,12 @@ export async function bootEntered(
|
|
|
294
322
|
return await inReleasedScope(
|
|
295
323
|
request,
|
|
296
324
|
async () => {
|
|
325
|
+
presume(options)
|
|
297
326
|
let answered: Response
|
|
298
327
|
try {
|
|
299
|
-
answered = await answer(
|
|
328
|
+
answered = await answer(
|
|
329
|
+
new RootContext(booted, request),
|
|
330
|
+
)
|
|
300
331
|
if (rungs.length > 0)
|
|
301
332
|
rungsTook(performance.now() - started)
|
|
302
333
|
} catch (thrown) {
|
|
@@ -431,6 +462,8 @@ export async function bootEntered(
|
|
|
431
462
|
maxRequestBodySize: Number.MAX_SAFE_INTEGER,
|
|
432
463
|
// 18.1, 37.3 — THE ONE MUX, upgraded where the app lane admitted it.
|
|
433
464
|
fetch: async (request, server) => {
|
|
465
|
+
// 22.12 — where it came from, for `requestIP` to answer.
|
|
466
|
+
RECEIVED_FROM.set(request, server.requestIP(request))
|
|
434
467
|
const answered = await fetch(request)
|
|
435
468
|
return admitsMux(request, answered, booted)
|
|
436
469
|
? upgraded(server, request, {
|
|
@@ -465,6 +498,26 @@ export async function bootEntered(
|
|
|
465
498
|
}
|
|
466
499
|
}
|
|
467
500
|
|
|
501
|
+
/**
|
|
502
|
+
* What a root rung is handed: the request, and what answers it, asked on the first read.
|
|
503
|
+
*
|
|
504
|
+
* 37.19 — LAZILY, because routing a request twice is a cost only an app whose root rungs read the
|
|
505
|
+
* lane pays: the dispatch routes it again after them, and an app with none reads nothing here.
|
|
506
|
+
*/
|
|
507
|
+
class RootContext implements AppContext {
|
|
508
|
+
readonly request: Request
|
|
509
|
+
#sources: AppSources
|
|
510
|
+
#lane: Lane | undefined = undefined
|
|
511
|
+
constructor(sources: AppSources, request: Request) {
|
|
512
|
+
this.#sources = sources
|
|
513
|
+
this.request = request
|
|
514
|
+
}
|
|
515
|
+
get lane(): Lane {
|
|
516
|
+
this.#lane ??= laneOf(this.#sources, this.request)
|
|
517
|
+
return this.#lane
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
|
|
468
521
|
/**
|
|
469
522
|
* The answer with the headers every answer carries, and what was written through `response`.
|
|
470
523
|
*
|
package/src/server/devWorker.ts
CHANGED
|
@@ -35,7 +35,7 @@ import { bootApp, type BootedApp } from './createApp.ts'
|
|
|
35
35
|
import { keepServing } from './faultPolicy.ts'
|
|
36
36
|
import { type Manifest, NO_MANIFEST } from './manifestOf.ts'
|
|
37
37
|
import { admitsMux, muxSocketOptions } from './muxUpgrade.ts'
|
|
38
|
-
import { LISTENING } from './requestScope.ts'
|
|
38
|
+
import { LISTENING, RECEIVED_FROM, type ReceivedFrom } from './requestScope.ts'
|
|
39
39
|
import {
|
|
40
40
|
describe,
|
|
41
41
|
type FromRelay,
|
|
@@ -70,6 +70,7 @@ export type ToWorker =
|
|
|
70
70
|
connection: number
|
|
71
71
|
url: string
|
|
72
72
|
headers: [string, string][]
|
|
73
|
+
from: ReceivedFrom
|
|
73
74
|
}
|
|
74
75
|
| { kind: 'socket-message'; connection: number; text: string }
|
|
75
76
|
| { kind: 'socket-close'; connection: number }
|
|
@@ -113,7 +114,9 @@ let failure = ''
|
|
|
113
114
|
const relay = serveOver(
|
|
114
115
|
{ postMessage: toListener },
|
|
115
116
|
{
|
|
116
|
-
async fetch(request) {
|
|
117
|
+
async fetch(request, from) {
|
|
118
|
+
// 22.12 — where the listener received it from, which is not this process.
|
|
119
|
+
RECEIVED_FROM.set(request, from)
|
|
117
120
|
await opening
|
|
118
121
|
return app === undefined
|
|
119
122
|
? failedDocument(failure)
|
|
@@ -161,21 +164,20 @@ async function receive(message: ToWorker): Promise<void> {
|
|
|
161
164
|
await opening
|
|
162
165
|
if (message.kind === 'socket-open') {
|
|
163
166
|
const numbered = message.connection
|
|
164
|
-
const
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
},
|
|
174
|
-
// NOTHING TO CLOSE: the listener holds the browser's socket, and it outlives this
|
|
175
|
-
// worker (38.14). Nothing here stops the mux, either.
|
|
176
|
-
close() {},
|
|
167
|
+
const upgrade = new Request(message.url, { headers: message.headers })
|
|
168
|
+
RECEIVED_FROM.set(upgrade, message.from)
|
|
169
|
+
const connection = mux?.connect(upgrade, {
|
|
170
|
+
send(text: string) {
|
|
171
|
+
toListener({
|
|
172
|
+
kind: 'socket-send',
|
|
173
|
+
connection: numbered,
|
|
174
|
+
text,
|
|
175
|
+
})
|
|
177
176
|
},
|
|
178
|
-
|
|
177
|
+
// NOTHING TO CLOSE: the listener holds the browser's socket, and it outlives this
|
|
178
|
+
// worker (38.14). Nothing here stops the mux, either.
|
|
179
|
+
close() {},
|
|
180
|
+
})
|
|
179
181
|
if (connection !== undefined) connections.set(numbered, connection)
|
|
180
182
|
return
|
|
181
183
|
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
|
|
16
16
|
import { isFailed } from '#shared/Failed.ts'
|
|
17
17
|
import { essenceOf } from '#shared/framing.ts'
|
|
18
|
+
import { IMAGE_TYPES } from '#shared/IMAGE_TYPES.ts'
|
|
18
19
|
import { namedLogger } from '#shared/log.ts'
|
|
19
20
|
import { mounted } from '#shared/mount.ts'
|
|
20
21
|
import { type Reactive, reactive } from '#shared/reactive/index.ts'
|
|
@@ -26,7 +27,6 @@ import { bodyWithin } from './answerRpc.ts'
|
|
|
26
27
|
import type { ToolAnswer, ToolDefinition } from './assistant.ts'
|
|
27
28
|
import { appIdentity } from './config.ts'
|
|
28
29
|
import { crossingOrigin, crossingRefusal } from './crossingOrigin.ts'
|
|
29
|
-
import { IMAGE_TYPES } from './IMAGE_TYPES.ts'
|
|
30
30
|
import { MCP_PROTOCOL_VERSION } from './MCP_PROTOCOL_VERSION.ts'
|
|
31
31
|
import type { Mounted } from './mountHandlers.ts'
|
|
32
32
|
import { type Listed, surfaceOf } from './surfaceOf.ts'
|
|
@@ -326,7 +326,7 @@ export class McpSurface {
|
|
|
326
326
|
|
|
327
327
|
/** What calling the tool `name` as `asker` answered, a refusal or a missing tool a failed one. */
|
|
328
328
|
call(name: string, args: unknown, asker: Request): Promise<ToolAnswer> {
|
|
329
|
-
const tool = this
|
|
329
|
+
const tool = this.tool(name)
|
|
330
330
|
if (tool === undefined) {
|
|
331
331
|
logCall(String(name), 404, undefined)
|
|
332
332
|
return Promise.resolve({
|
|
@@ -337,12 +337,13 @@ export class McpSurface {
|
|
|
337
337
|
return this.#called(tool, args as Record<string, unknown>, asker)
|
|
338
338
|
}
|
|
339
339
|
|
|
340
|
-
|
|
340
|
+
/** The tool it lists as `name`, or `undefined` where it lists none. */
|
|
341
|
+
tool(name: unknown): Listed | undefined {
|
|
341
342
|
return this.#published().find((one) => one.name === name)
|
|
342
343
|
}
|
|
343
344
|
|
|
344
345
|
async #called(
|
|
345
|
-
one:
|
|
346
|
+
one: Listed,
|
|
346
347
|
args: Record<string, unknown> | undefined,
|
|
347
348
|
asker: Request,
|
|
348
349
|
): Promise<ToolAnswer> {
|
|
@@ -416,7 +417,7 @@ export class McpSurface {
|
|
|
416
417
|
case 'tools/list':
|
|
417
418
|
return { result: { tools: this.list() } }
|
|
418
419
|
case 'tools/call': {
|
|
419
|
-
const tool = this
|
|
420
|
+
const tool = this.tool(params?.name)
|
|
420
421
|
if (tool === undefined) {
|
|
421
422
|
logCall(String(params?.name), 404, undefined)
|
|
422
423
|
return mcpFailure(
|
|
@@ -13,6 +13,7 @@ export function handlerFileMount(
|
|
|
13
13
|
| 'cli'
|
|
14
14
|
| 'mcp'
|
|
15
15
|
| 'description'
|
|
16
|
+
| 'middleware'
|
|
16
17
|
| 'copy'
|
|
17
18
|
| 'defaults'
|
|
18
19
|
| 'args'
|
|
@@ -32,6 +33,7 @@ export function handlerFileMount(
|
|
|
32
33
|
cli: declared.cli,
|
|
33
34
|
mcp: declared.mcp,
|
|
34
35
|
description: declared.description,
|
|
36
|
+
middleware: declared.middleware,
|
|
35
37
|
copy: declared.copy,
|
|
36
38
|
defaults: declared.defaults,
|
|
37
39
|
args: declared.args,
|
package/src/server/hooks.ts
CHANGED
|
@@ -26,11 +26,29 @@ import { config } from './config.ts'
|
|
|
26
26
|
import type { LauncherOptions } from './launcherOptions.ts'
|
|
27
27
|
import { currentRequestScope } from './requestScope.ts'
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
/**
|
|
30
|
+
* What answers a request, by the lane the app sends it down. 37.19.
|
|
31
|
+
*
|
|
32
|
+
* One per arm of `answerRequest`'s dispatch: a page (and every address no route answers), a
|
|
33
|
+
* handler, a public file, the three surfaces of abide's own a gate decides about one by one, the
|
|
34
|
+
* socket mux's upgrade, and the rest of `/__abide`.
|
|
35
|
+
*/
|
|
36
|
+
export type Lane =
|
|
37
|
+
| 'page'
|
|
38
|
+
| 'handler'
|
|
39
|
+
| 'file'
|
|
40
|
+
| 'openapi'
|
|
41
|
+
| 'mcp'
|
|
42
|
+
| 'bridge'
|
|
43
|
+
| 'socket'
|
|
44
|
+
| 'abide'
|
|
45
|
+
|
|
46
|
+
/** What an app rung is handed: the request, and what answers it (37.19). */
|
|
47
|
+
export type AppContext = { request: Request; lane: Lane }
|
|
30
48
|
|
|
31
49
|
/** A rung `middleware` files: what `next` answered passed on, or a refusal in its place (37.14). */
|
|
32
50
|
export type AppRung = (
|
|
33
|
-
next: (ctx?:
|
|
51
|
+
next: (ctx?: { request: Request }) => Promise<Response>,
|
|
34
52
|
ctx: AppContext,
|
|
35
53
|
) =>
|
|
36
54
|
| Response
|
|
@@ -130,18 +148,46 @@ export function inOrder<T>(list: readonly Filed<T>[]): T[] {
|
|
|
130
148
|
* 37.14, 37.15 — PER LINK, not at the onion's edge: a rung outside this one is handed the answer
|
|
131
149
|
* from `next`, so one that reads a `Response` (29.2's) reads one whichever rung inside it refused.
|
|
132
150
|
*/
|
|
133
|
-
function answering(
|
|
151
|
+
function answering(
|
|
152
|
+
rung: AppRung,
|
|
153
|
+
answer: RefusalAnswer,
|
|
154
|
+
laneOf: ((request: Request) => Lane) | undefined,
|
|
155
|
+
): LaneRung {
|
|
134
156
|
return async (next, ctx) => {
|
|
135
|
-
|
|
157
|
+
// 37.19 — A REQUEST A RUNG HANDS `next` IS ANSWERED BY WHAT ITS OWN ADDRESS ROUTES TO, asked
|
|
158
|
+
// again before routing (`laneOf`) and kept after it, where routing has already chosen.
|
|
159
|
+
const result = await rung(
|
|
160
|
+
(given) =>
|
|
161
|
+
next(
|
|
162
|
+
given === undefined || given.request === ctx.request
|
|
163
|
+
? ctx
|
|
164
|
+
: {
|
|
165
|
+
request: given.request,
|
|
166
|
+
lane:
|
|
167
|
+
laneOf === undefined
|
|
168
|
+
? ctx.lane
|
|
169
|
+
: laneOf(given.request),
|
|
170
|
+
},
|
|
171
|
+
),
|
|
172
|
+
ctx,
|
|
173
|
+
)
|
|
136
174
|
return isFailed(result) ? answer(result, ctx.request) : result
|
|
137
175
|
}
|
|
138
176
|
}
|
|
139
177
|
|
|
140
|
-
/**
|
|
141
|
-
|
|
178
|
+
/**
|
|
179
|
+
* The rungs filed at the root route, which run before routing. 37.3.
|
|
180
|
+
*
|
|
181
|
+
* `laneOf` is what the dispatch would answer a request with (37.19), asked of one a rung hands on.
|
|
182
|
+
*/
|
|
183
|
+
export function rootRungs(
|
|
184
|
+
answer: RefusalAnswer,
|
|
185
|
+
laneOf: (request: Request) => Lane,
|
|
186
|
+
): LaneRung[] {
|
|
142
187
|
const rungs: LaneRung[] = []
|
|
143
188
|
for (const entry of HOOKS.rungs)
|
|
144
|
-
if (entry.route === ROOT)
|
|
189
|
+
if (entry.route === ROOT)
|
|
190
|
+
rungs.push(answering(entry.item, answer, laneOf))
|
|
145
191
|
return rungs
|
|
146
192
|
}
|
|
147
193
|
|
|
@@ -156,7 +202,7 @@ export function rungsOver(route: string, answer: RefusalAnswer): LaneRung[] {
|
|
|
156
202
|
const rungs: LaneRung[] = []
|
|
157
203
|
for (const entry of HOOKS.rungs)
|
|
158
204
|
if (entry.route !== ROOT && coversRoute(entry.segments, segments))
|
|
159
|
-
rungs.push(answering(entry.item, answer))
|
|
205
|
+
rungs.push(answering(entry.item, answer, undefined))
|
|
160
206
|
return rungs
|
|
161
207
|
}
|
|
162
208
|
|
package/src/server/index.ts
CHANGED
|
@@ -26,13 +26,17 @@ export {
|
|
|
26
26
|
type RpcOptions,
|
|
27
27
|
} from '#shared/rpc.ts'
|
|
28
28
|
// 45 — a conversation with a model, piped through a handler, and the two providers abide ships.
|
|
29
|
+
// 44 — what a page sends and is handed back, which the isomorphic entry carries too.
|
|
30
|
+
export type {
|
|
31
|
+
AssistantContent,
|
|
32
|
+
AssistantEvent,
|
|
33
|
+
AssistantMessage,
|
|
34
|
+
} from '#shared/AssistantEvent.ts'
|
|
29
35
|
export {
|
|
30
|
-
type AssistantContent,
|
|
31
|
-
type AssistantEvent,
|
|
32
|
-
type AssistantMessage,
|
|
33
36
|
type AssistantOptions,
|
|
34
37
|
assistant,
|
|
35
38
|
type Provider,
|
|
39
|
+
type ProviderEvent,
|
|
36
40
|
type ToolAnswer,
|
|
37
41
|
type Toolbox,
|
|
38
42
|
type ToolDefinition,
|
|
@@ -62,6 +66,7 @@ export {
|
|
|
62
66
|
type ConfigOptions,
|
|
63
67
|
type Env,
|
|
64
68
|
fileRoute,
|
|
69
|
+
type Lane,
|
|
65
70
|
middleware,
|
|
66
71
|
onConfig,
|
|
67
72
|
onError,
|
|
@@ -80,7 +85,14 @@ export { type Component, render } from './render.ts'
|
|
|
80
85
|
// child's markup one thing for the client to walk to.
|
|
81
86
|
export { renderComponent } from './renderComponent.ts'
|
|
82
87
|
// 22.1 — the request being served, the response being built for it, and the listening server.
|
|
83
|
-
|
|
88
|
+
// 22.12 — and where it came from.
|
|
89
|
+
export {
|
|
90
|
+
type ReceivedFrom,
|
|
91
|
+
request,
|
|
92
|
+
requestIP,
|
|
93
|
+
response,
|
|
94
|
+
server,
|
|
95
|
+
} from './requestScope.ts'
|
|
84
96
|
// 17 — the response helpers, each a write through `response`.
|
|
85
97
|
export {
|
|
86
98
|
bytes,
|
|
@@ -92,6 +104,8 @@ export {
|
|
|
92
104
|
sse,
|
|
93
105
|
type Values,
|
|
94
106
|
} from './responseHelpers.ts'
|
|
107
|
+
// 8.25 — a `Store` in a `bun:sqlite` table.
|
|
108
|
+
export { sqliteStore } from './sqliteStore.ts'
|
|
95
109
|
// 24 — the trace this work belongs to.
|
|
96
110
|
export { trace } from './trace.ts'
|
|
97
111
|
|
|
@@ -222,7 +222,7 @@ async function routeAdmits(
|
|
|
222
222
|
const answered = await compose(mounted.rungs, async () => {
|
|
223
223
|
reached = true
|
|
224
224
|
return new Response(null, { status: 204 })
|
|
225
|
-
})({ request })
|
|
225
|
+
})({ request, lane: 'handler' })
|
|
226
226
|
return reached && answered.ok
|
|
227
227
|
} catch {
|
|
228
228
|
return false
|
|
@@ -44,6 +44,11 @@ export type Mount = {
|
|
|
44
44
|
mcp: boolean
|
|
45
45
|
/** Its `description`, where the declaration writes one as a literal the build can read. */
|
|
46
46
|
description: string | undefined
|
|
47
|
+
/**
|
|
48
|
+
* The rung each entry of its declaration's `middleware` names, as written; `null` where the
|
|
49
|
+
* options are not written where the build can read them. 38.135.
|
|
50
|
+
*/
|
|
51
|
+
middleware: string[] | null
|
|
47
52
|
/**
|
|
48
53
|
* What a `GET`'s value declares that the browser's copy behaves by, or `undefined`.
|
|
49
54
|
*
|
|
@@ -134,6 +139,7 @@ export function mountedOwn(
|
|
|
134
139
|
cli: false,
|
|
135
140
|
mcp: false,
|
|
136
141
|
description: undefined,
|
|
142
|
+
middleware: [],
|
|
137
143
|
copy: undefined,
|
|
138
144
|
defaults: undefined,
|
|
139
145
|
args: undefined,
|
package/src/server/principal.ts
CHANGED
|
@@ -198,13 +198,14 @@ function resolvePrincipal(): Promise<Principal> {
|
|
|
198
198
|
/**
|
|
199
199
|
* This request's principal, resolved from `claims` as a sealed cookie's are, with no cookie written.
|
|
200
200
|
*
|
|
201
|
-
* 42.19 — `App.run`'s `principal`, which is why it throws where
|
|
201
|
+
* 42.19, 42.20 — `App.run`'s and `App.fetch`'s `principal`, which is why it throws where
|
|
202
|
+
* `principal.set` does.
|
|
202
203
|
*/
|
|
203
204
|
export function presumePrincipal(claims: Record<string, unknown>): void {
|
|
204
205
|
const scope = within('App.run principal')
|
|
205
206
|
if (!isRecord(claims))
|
|
206
207
|
throw new Error(
|
|
207
|
-
'abide: App.run
|
|
208
|
+
'abide: App.run and App.fetch take a principal of claims, as principal.set does. RULEBOOK 26.11.',
|
|
208
209
|
)
|
|
209
210
|
const expires = Date.now() + ttl()
|
|
210
211
|
scope.principal = documentOf(claims, expires)
|
package/src/server/render.ts
CHANGED
|
@@ -11,7 +11,7 @@ import type { ShareScope } from '#shared/reactive/share.ts'
|
|
|
11
11
|
import { setUpLevels } from '#shared/setUpLevels.ts'
|
|
12
12
|
|
|
13
13
|
import { buildDeferred } from './deferredBuilds.ts'
|
|
14
|
-
import { currentRequestScope } from './requestScope.ts'
|
|
14
|
+
import { currentRequestScope, inReleasedRender } from './requestScope.ts'
|
|
15
15
|
|
|
16
16
|
/**
|
|
17
17
|
* A compiled component, opaque to an app.
|
|
@@ -314,7 +314,16 @@ export function render(component: Component): Promise<Uint8Array<ArrayBuffer>> {
|
|
|
314
314
|
* `render` into `given`: abide's own document with what the server adds to it — a page's client
|
|
315
315
|
* tags, the launcher page's stylesheet. NOT PUBLIC SURFACE — `server/index.ts` exports `render`.
|
|
316
316
|
*/
|
|
317
|
-
export
|
|
317
|
+
export function renderDocument(
|
|
318
|
+
component: Component,
|
|
319
|
+
given: string,
|
|
320
|
+
): Promise<Uint8Array<ArrayBuffer>> {
|
|
321
|
+
// 22.11 — a server's render whether or not a request is being served; 12.5 — and torn down once
|
|
322
|
+
// written where no request is.
|
|
323
|
+
return inReleasedRender(() => documentOf(component, given))
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
async function documentOf(
|
|
318
327
|
component: Component,
|
|
319
328
|
given: string,
|
|
320
329
|
): Promise<Uint8Array<ArrayBuffer>> {
|
|
@@ -125,8 +125,20 @@ const STORAGE = new AsyncLocalStorage<RequestScope | undefined>()
|
|
|
125
125
|
// value is filed per request (11.32), and a server module builds values as it loads — the
|
|
126
126
|
// principal's document here, an app's handlers ahead of `bootApp` in a compiled entry. Anything
|
|
127
127
|
// that can enter a scope imports this module, so none is built before the provider is in.
|
|
128
|
+
// 22.11 — A RENDER OUTSIDE EVERY REQUEST IS STILL A SERVER'S: `render` called bare, and `mount`'s
|
|
129
|
+
// first half, which a test process runs beside a DOM and the browser half that hydrates it. Marked
|
|
130
|
+
// apart from STORAGE so the render's values stay the ones 11.32 files outside a request. What it
|
|
131
|
+
// holds is the render's stops (12.5): a request's are its scope's.
|
|
132
|
+
const RENDERING = new AsyncLocalStorage<(() => void)[]>()
|
|
133
|
+
|
|
134
|
+
/** The stops of the request or the render current, or `undefined` where neither is. */
|
|
135
|
+
function currentStops(): (() => void)[] | undefined {
|
|
136
|
+
return STORAGE.getStore()?.stops ?? RENDERING.getStore()
|
|
137
|
+
}
|
|
138
|
+
|
|
128
139
|
SCOPING.provider = {
|
|
129
140
|
current: (): Scope | undefined => STORAGE.getStore(),
|
|
141
|
+
serverStops: currentStops,
|
|
130
142
|
run: (scope, body, given) =>
|
|
131
143
|
STORAGE.run(scope as RequestScope | undefined, body, given),
|
|
132
144
|
}
|
|
@@ -179,6 +191,21 @@ export function inScope<Result>(
|
|
|
179
191
|
return STORAGE.run(scopeFor(given, given, undefined), body)
|
|
180
192
|
}
|
|
181
193
|
|
|
194
|
+
/**
|
|
195
|
+
* Runs `body` as a server's render, stopping every watch it opened once it has settled.
|
|
196
|
+
*
|
|
197
|
+
* 12.5 — A RENDER OUTSIDE EVERY REQUEST IS TORN DOWN as a request's scope is, once its document is
|
|
198
|
+
* written. Inside a request or a render already open, the watches are that one's to stop.
|
|
199
|
+
*/
|
|
200
|
+
export function inReleasedRender<Result>(
|
|
201
|
+
body: () => Promise<Result>,
|
|
202
|
+
): Promise<Result> {
|
|
203
|
+
// A REQUEST'S PAGE PAYS NOTHING HERE — no wrapping promise, no tick — as its scope stops them.
|
|
204
|
+
if (currentStops() !== undefined) return body()
|
|
205
|
+
const stops: (() => void)[] = []
|
|
206
|
+
return RENDERING.run(stops, body).finally(() => stopAll(stops))
|
|
207
|
+
}
|
|
208
|
+
|
|
182
209
|
/** Runs `body` in `scope`, which `scopeFor` opened, and returns what it returned. */
|
|
183
210
|
export function inOpenScope<Result>(
|
|
184
211
|
scope: RequestScope,
|
|
@@ -261,7 +288,11 @@ export function releaseScope(scope: RequestScope): void {
|
|
|
261
288
|
scope.unreaped = false
|
|
262
289
|
LISTENING.timeout(scope.received as Request, IDLE_TIMEOUT_SECONDS)
|
|
263
290
|
}
|
|
264
|
-
|
|
291
|
+
stopAll(scope.stops)
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/** Runs and drops every stop in `stops`. 12.5. */
|
|
295
|
+
function stopAll(stops: (() => void)[]): void {
|
|
265
296
|
for (let at = 0; at < stops.length; at += 1) (stops[at] as () => void)()
|
|
266
297
|
stops.length = 0
|
|
267
298
|
}
|
|
@@ -433,6 +464,35 @@ export function response(): { status: number; headers: Headers } {
|
|
|
433
464
|
return scope.response
|
|
434
465
|
}
|
|
435
466
|
|
|
467
|
+
/** Where a request came from: a peer's address, `null` where the listener names none. */
|
|
468
|
+
export type ReceivedFrom = {
|
|
469
|
+
address: string
|
|
470
|
+
family: 'IPv4' | 'IPv6'
|
|
471
|
+
port: number
|
|
472
|
+
} | null
|
|
473
|
+
|
|
474
|
+
/**
|
|
475
|
+
* The peer each request a listener received came from, keyed by the request it handed the app.
|
|
476
|
+
*
|
|
477
|
+
* 22.12 — FILED BY THE LISTENER: `App.listen` files what bun names, and `abide dev`'s worker what its
|
|
478
|
+
* listener sent beside the request (38.14). A request nobody filed was made in process.
|
|
479
|
+
*/
|
|
480
|
+
export const RECEIVED_FROM = new WeakMap<Request, ReceivedFrom>()
|
|
481
|
+
|
|
482
|
+
/**
|
|
483
|
+
* The address the request being served came from, or `'in-process'` for one no listener received.
|
|
484
|
+
*
|
|
485
|
+
* 22.12, 22.13 — the same under `abide start`, `abide dev` and `App.listen`; `null` where the
|
|
486
|
+
* listener names no peer, as a unix socket does. A socket mux frame answers its upgrade's.
|
|
487
|
+
*/
|
|
488
|
+
export function requestIP(): ReceivedFrom | 'in-process' {
|
|
489
|
+
const scope = within('requestIP')
|
|
490
|
+
const asked = (scope.received ?? scope.request) as Request
|
|
491
|
+
return RECEIVED_FROM.has(asked)
|
|
492
|
+
? (RECEIVED_FROM.get(asked) as ReceivedFrom)
|
|
493
|
+
: 'in-process'
|
|
494
|
+
}
|
|
495
|
+
|
|
436
496
|
/**
|
|
437
497
|
* The listening server.
|
|
438
498
|
*
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
// What `serveDev` and `devWorker` keep is the processes and the sockets.
|
|
7
7
|
|
|
8
8
|
import { plainAnswer } from './plainAnswer.ts'
|
|
9
|
+
import type { ReceivedFrom } from './requestScope.ts'
|
|
9
10
|
|
|
10
11
|
/** One direction of the line between the halves: bun's IPC in `abide dev`, a pair of queues in a test. */
|
|
11
12
|
export type Channel<Message> = { postMessage(message: Message): void }
|
|
@@ -17,6 +18,8 @@ export type Ask = {
|
|
|
17
18
|
url: string
|
|
18
19
|
headers: [string, string][]
|
|
19
20
|
body: ArrayBuffer | null
|
|
21
|
+
/** 22.12 — the peer the listener received it from. */
|
|
22
|
+
from: ReceivedFrom
|
|
20
23
|
}
|
|
21
24
|
|
|
22
25
|
/** What the listener's half sends: a request, and a pull or cancel of its body. */
|
|
@@ -62,7 +65,7 @@ export function describe(thrown: unknown): string {
|
|
|
62
65
|
export function serveOver(
|
|
63
66
|
channel: Channel<FromRelay>,
|
|
64
67
|
answering: {
|
|
65
|
-
fetch(request: Request): Promise<Response>
|
|
68
|
+
fetch(request: Request, from: ReceivedFrom): Promise<Response>
|
|
66
69
|
/** 37.3 — whether the answer admits the request to the mux. */
|
|
67
70
|
admits(request: Request, response: Response): boolean
|
|
68
71
|
/** The answer to a throw out of `fetch`, which is an answer and not a dropped message. */
|
|
@@ -107,7 +110,7 @@ export function serveOver(
|
|
|
107
110
|
asks.set(request, ask.id)
|
|
108
111
|
let response: Response
|
|
109
112
|
try {
|
|
110
|
-
response = await answering.fetch(request)
|
|
113
|
+
response = await answering.fetch(request, ask.from)
|
|
111
114
|
} catch (thrown) {
|
|
112
115
|
response = answering.failed(describe(thrown))
|
|
113
116
|
}
|
|
@@ -207,6 +210,7 @@ type InFlight = {
|
|
|
207
210
|
request: Request
|
|
208
211
|
headers: [string, string][]
|
|
209
212
|
body: ArrayBuffer | null
|
|
213
|
+
from: ReceivedFrom
|
|
210
214
|
settle: (response: Response) => void
|
|
211
215
|
asked: Channel<ToRelay> | undefined
|
|
212
216
|
stream: ReadableStreamDefaultController<Uint8Array> | null
|
|
@@ -252,6 +256,7 @@ export function askOver(
|
|
|
252
256
|
url: waiting.request.url,
|
|
253
257
|
headers: waiting.headers,
|
|
254
258
|
body: waiting.body,
|
|
259
|
+
from: waiting.from,
|
|
255
260
|
})
|
|
256
261
|
}
|
|
257
262
|
|
|
@@ -310,7 +315,7 @@ export function askOver(
|
|
|
310
315
|
* what the worker rebuilds one from — and the body is collected ONCE, HERE, which is what lets
|
|
311
316
|
* it be asked again of a replacement (38.24): a body can only be read off a `Request` once.
|
|
312
317
|
*/
|
|
313
|
-
async ask(request: Request): Promise<Response> {
|
|
318
|
+
async ask(request: Request, from: ReceivedFrom): Promise<Response> {
|
|
314
319
|
const body =
|
|
315
320
|
request.body === null ? null : await request.arrayBuffer()
|
|
316
321
|
return await new Promise<Response>((settle) => {
|
|
@@ -318,6 +323,7 @@ export function askOver(
|
|
|
318
323
|
request,
|
|
319
324
|
headers: identityOnly(request.headers),
|
|
320
325
|
body,
|
|
326
|
+
from,
|
|
321
327
|
settle,
|
|
322
328
|
asked: undefined,
|
|
323
329
|
stream: null,
|