@abide/abide 0.124.1 → 0.125.1

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.
Files changed (74) hide show
  1. package/ABIDE.md +4 -0
  2. package/CHANGELOG.md +50 -0
  3. package/bin/abide.ts +3 -2
  4. package/docs/REGISTRY.md +67 -40
  5. package/docs/RULEBOOK.md +206 -18
  6. package/docs/abide.md +393 -67
  7. package/package.json +1 -1
  8. package/src/compiler/appMap.ts +14 -1
  9. package/src/compiler/compileApp.ts +1 -0
  10. package/src/compiler/declaredExports.ts +41 -0
  11. package/src/compiler/handlersUnder.ts +1 -0
  12. package/src/compiler/hookCalls.ts +1 -0
  13. package/src/compiler/projectShadow.ts +52 -2
  14. package/src/compiler/readsIn.ts +39 -34
  15. package/src/server/PageRender.ts +7 -8
  16. package/src/server/abideModules.ts +22 -9
  17. package/src/server/addressed.ts +14 -0
  18. package/src/server/answerRequest.ts +115 -52
  19. package/src/server/answerSocket.ts +6 -5
  20. package/src/server/answerWorker.ts +34 -23
  21. package/src/server/assistant.ts +141 -35
  22. package/src/server/banner.ts +9 -5
  23. package/src/server/bridgeRelay.ts +3 -3
  24. package/src/server/callerOf.ts +5 -0
  25. package/src/server/claudeCode.ts +20 -11
  26. package/src/server/commands.ts +14 -7
  27. package/src/server/createApp.ts +65 -12
  28. package/src/server/devWorker.ts +18 -16
  29. package/src/server/faceOf.ts +3 -1
  30. package/src/server/generatedSurfaces.ts +6 -5
  31. package/src/server/handlerFileMount.ts +2 -0
  32. package/src/server/hooks.ts +54 -8
  33. package/src/server/index.ts +21 -4
  34. package/src/server/invokeHandler.ts +1 -1
  35. package/src/server/mountHandlers.ts +9 -7
  36. package/src/server/principal.ts +3 -2
  37. package/src/server/render.ts +11 -2
  38. package/src/server/requestScope.ts +61 -1
  39. package/src/server/responseRelay.ts +9 -3
  40. package/src/server/serveDev.ts +18 -3
  41. package/src/server/sqliteStore.ts +65 -0
  42. package/src/server/testMount.ts +1 -0
  43. package/src/shared/AssistantEvent.ts +40 -0
  44. package/src/shared/BrowserHooks.ts +73 -0
  45. package/src/shared/CURRENT_ROUTER.ts +3 -1
  46. package/src/shared/Issues.ts +11 -3
  47. package/src/shared/Middleware.ts +14 -7
  48. package/src/shared/UNEXPECTED.ts +1 -1
  49. package/src/shared/answerOf.ts +9 -4
  50. package/src/shared/imagePiece.ts +85 -0
  51. package/src/shared/index.ts +17 -0
  52. package/src/shared/isServer.ts +15 -0
  53. package/src/shared/navigate.ts +10 -3
  54. package/src/shared/onError.ts +51 -0
  55. package/src/shared/onRefusal.ts +47 -0
  56. package/src/shared/online.ts +5 -1
  57. package/src/shared/reactive/Keyed.ts +12 -1
  58. package/src/shared/reactive/Reactive.ts +53 -25
  59. package/src/shared/reactive/cellShape.ts +4 -3
  60. package/src/shared/reactive/poll.ts +49 -0
  61. package/src/shared/reactive/scoped.ts +10 -1
  62. package/src/shared/reactive/scoping.ts +14 -0
  63. package/src/shared/reactive/watch.ts +19 -8
  64. package/src/shared/resultsOf.ts +23 -0
  65. package/src/shared/route.ts +18 -4
  66. package/src/shared/rpc.ts +125 -30
  67. package/src/shared/seededDocument.ts +5 -6
  68. package/src/shared/storedValue.ts +32 -0
  69. package/src/shared/webStore.ts +54 -0
  70. package/src/ui/Router.ts +42 -9
  71. package/src/ui/outboxQueue.ts +1 -1
  72. package/src/ui/wire.ts +4 -4
  73. package/src/shared/KEEP_OPENED.ts +0 -7
  74. /package/src/{server → shared}/IMAGE_TYPES.ts +0 -0
@@ -8,17 +8,15 @@
8
8
  // directory, settings — is not an option here at all, and its built-in `tools` are a request the visitor
9
9
  // grants or does not: the bridge decides (44.15, 44.16, 44.29).
10
10
 
11
+ import type {
12
+ AssistantContent,
13
+ AssistantMessage,
14
+ } from '#shared/AssistantEvent.ts'
11
15
  import { BRIDGE_SCHEME } from '#shared/BRIDGE_SCHEME.ts'
12
16
  import { mounted } from '#shared/mount.ts'
13
17
  import { refuse } from '#shared/refuse.ts'
14
18
 
15
- import {
16
- type AssistantContent,
17
- type AssistantMessage,
18
- blocksOf,
19
- forwardOf,
20
- type Provider,
21
- } from './assistant.ts'
19
+ import { blocksOf, forwardOf, type Provider } from './assistant.ts'
22
20
  import { BRIDGE_VERSION } from './BRIDGE_VERSION.ts'
23
21
  import { mintCode, pairingStatus, runTurn } from './bridgeRelay.ts'
24
22
  import { callerOf } from './callerOf.ts'
@@ -45,12 +43,13 @@ type PairingOptions = Pick<ClaudeCodeConfig, 'tools'>
45
43
 
46
44
  /**
47
45
  * Whether this browser's bridge is connected, and the command and the link that pair one where it is
48
- * not.
46
+ * not, with the command that installs the app the link opens.
49
47
  */
50
48
  export type BridgeStatus = {
51
49
  paired: boolean
52
50
  command: string | undefined
53
51
  link: string | undefined
52
+ install: string | undefined
54
53
  }
55
54
 
56
55
  /** The visitor's own Claude Code, reached through the bridge paired to their browser. */
@@ -123,17 +122,21 @@ function askingCaller(): string {
123
122
  function bridgeCommand(options: PairingOptions = {}): {
124
123
  command: string
125
124
  link: string
125
+ install: string
126
126
  expires: number
127
127
  } {
128
128
  const { code, expires } = mintCode(askingCaller(), signedInAs())
129
129
  return { ...commandFor(code, options), expires }
130
130
  }
131
131
 
132
- /** The command and the link that pair a bridge by `code`, granting `tools`. */
132
+ /**
133
+ * The command and the link that pair a bridge by `code`, granting `tools`, and the command that
134
+ * installs the app the link opens — of the release the command runs, 44.56.
135
+ */
133
136
  function commandFor(
134
137
  code: string,
135
138
  { tools }: PairingOptions,
136
- ): { command: string; link: string } {
139
+ ): { command: string; link: string; install: string } {
137
140
  const site = `${appOrigin() ?? new URL(request().url).origin}${mounted('')}`
138
141
  const granted = tools?.length ? tools.join(',') : undefined
139
142
  const query = new URLSearchParams({ site, code, version: BRIDGE_VERSION })
@@ -142,6 +145,7 @@ function commandFor(
142
145
  return {
143
146
  command: `bunx @abide/bridge@${BRIDGE_VERSION} ${site} ${code}${grant}`,
144
147
  link: `${BRIDGE_SCHEME}://pair?${query}`,
148
+ install: `bunx @abide/bridge@${BRIDGE_VERSION} install`,
145
149
  }
146
150
  }
147
151
 
@@ -159,7 +163,12 @@ async function* bridge(
159
163
  request().signal,
160
164
  ))
161
165
  yield code === undefined
162
- ? { paired: true, command: undefined, link: undefined }
166
+ ? {
167
+ paired: true,
168
+ command: undefined,
169
+ link: undefined,
170
+ install: undefined,
171
+ }
163
172
  : { paired: false, ...commandFor(code, options) }
164
173
  }
165
174
 
@@ -245,10 +245,11 @@ function usage(host: Host, commands: Row[]): string {
245
245
  text += ` ${command.padEnd(width)} ${meaning}\n`
246
246
  if (!sourced)
247
247
  return `${text}
248
- --port the listen port the app is served on
249
- --url the running app the shell, call, watch, openapi and mcp talk to
250
- --method the method call sends, where the handler answers more than one
251
- --version the version of this executable's app
248
+ --port the listen port the app is served on
249
+ --hostname the interface it listens on — every one where none is named
250
+ --url the running app the shell, call, watch, openapi and mcp talk to
251
+ --method the method call sends, where the handler answers more than one
252
+ --version the version of this executable's app
252
253
  `
253
254
  text += `
254
255
  --root the app's source directory, where its pages sit —
@@ -256,6 +257,7 @@ function usage(host: Host, commands: Row[]): string {
256
257
  --out where the build goes, beside the app by default; the file compile and
257
258
  bundle write — named after the app, where you stand
258
259
  --port the listen port for dev, start and the shell's app
260
+ --hostname the interface they listen on — every one where none is named
259
261
  --url the running app the shell, call, watch, openapi and mcp talk to
260
262
  --target the platform compile builds for — linux-x64, darwin-arm64, windows-x64…
261
263
  --platforms several of them, comma-separated, one executable each
@@ -291,6 +293,8 @@ function parseFlags(argv: string[]) {
291
293
  // `PORT` and then falls back to 3000 — `abide dev` scanning on from there, 38.63 — and
292
294
  // a default written here would take that away from every host that sets it.
293
295
  port: { type: 'string' },
296
+ // NO DEFAULT EITHER: `Bun.serve` given none binds every interface (38.136).
297
+ hostname: { type: 'string' },
294
298
  url: { type: 'string' },
295
299
  method: { type: 'string' },
296
300
  target: { type: 'string' },
@@ -321,14 +325,17 @@ async function served(
321
325
  process.env.NODE_ENV ??= 'production'
322
326
  const booted = await booting(host, app, given)
323
327
  if (typeof booted === 'number') return booted
324
- const port = given.values.port
325
- await booted.listen(port === undefined ? {} : { port: Number(port) })
328
+ const { port, hostname } = given.values
329
+ await booted.listen({
330
+ ...(port === undefined ? {} : { port: Number(port) }),
331
+ ...(hostname === undefined ? {} : { hostname }),
332
+ })
326
333
  stopOnSignalOrFault(booted)
327
334
  const server = LISTENING.server as Bun.Server<unknown>
328
335
  return {
329
336
  app: booted,
330
337
  url: withoutTrailingSlash(server.url.href),
331
- heading: banner(host.name, 'start', app.package, server),
338
+ heading: banner(host.name, 'start', app.package, server, hostname),
332
339
  }
333
340
  }
334
341
 
@@ -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 { HOOKS, inOrder, reportUnexpected, rootRungs } from './hooks.ts'
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; principal?: Record<string, unknown> },
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((failure, request) =>
251
- laneFailure(booted, new URL(request.url).pathname, failure, request),
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
- const claims = options?.principal
270
- // 42.19 — a principal is a request's, so one given alone is given a request to be.
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
- (claims === undefined ? undefined : new Request(IN_PROCESS_ORIGIN))
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
- if (claims !== undefined) presumePrincipal(claims)
305
+ presume(options)
278
306
  return await fn()
279
307
  },
280
308
  answering,
281
309
  )
282
310
  }
283
311
 
284
- const fetch = async (given: Request): Promise<Response> => {
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({ request })
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
  *
@@ -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 connection = mux?.connect(
165
- new Request(message.url, { headers: message.headers }),
166
- {
167
- send(text: string) {
168
- toListener({
169
- kind: 'socket-send',
170
- connection: numbered,
171
- text,
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
  }
@@ -43,10 +43,12 @@ export type Asked =
43
43
 
44
44
  /**
45
45
  * One handler file, as `Compilation.handlers` holds it — and 19.9's schema its default's annotation
46
- * derives, which the module at the file's path holds it to (`derivedArgs`).
46
+ * derives, which the module at the file's path holds it to (`derivedArgs`), and the address it is
47
+ * stamped with on load (16.2).
47
48
  */
48
49
  export type HandlerFace = {
49
50
  scope: string
51
+ address: string
50
52
  client: string
51
53
  args: JsonValue | undefined
52
54
  }
@@ -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.#tool(name)
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
- #tool(name: unknown): Published | undefined {
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: Published,
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.#tool(params?.name)
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,
@@ -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
- type AppContext = { request: Request }
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?: AppContext) => Promise<Response>,
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(rung: AppRung, answer: RefusalAnswer): LaneRung {
151
+ function answering(
152
+ rung: AppRung,
153
+ answer: RefusalAnswer,
154
+ laneOf: ((request: Request) => Lane) | undefined,
155
+ ): LaneRung {
134
156
  return async (next, ctx) => {
135
- const result = await rung(next, ctx)
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
- /** The rungs filed at the root route, which run before routing. 37.3. */
141
- export function rootRungs(answer: RefusalAnswer): LaneRung[] {
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) rungs.push(answering(entry.item, answer))
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
 
@@ -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,
@@ -52,6 +56,9 @@ export { type App, createApp } from './createApp.ts'
52
56
  // 41.28's run-time half: an emitted component reads it on either side of its setup, and one whose
53
57
  // setup made a call answers as a page does. Not an app's name.
54
58
  export { callsInRender } from './PageRender.ts'
59
+ // 16.88's run-time half: what the module a handler file is loaded through calls to give it its
60
+ // address. Not an app's name.
61
+ export { addressed } from './addressed.ts'
55
62
  // 19.9's run-time half: what the module a handler is loaded through calls to hold it to the schema
56
63
  // its annotation derives (15.8). Not an app's name.
57
64
  export { derivedArgs } from './argsContract.ts'
@@ -62,6 +69,7 @@ export {
62
69
  type ConfigOptions,
63
70
  type Env,
64
71
  fileRoute,
72
+ type Lane,
65
73
  middleware,
66
74
  onConfig,
67
75
  onError,
@@ -80,7 +88,14 @@ export { type Component, render } from './render.ts'
80
88
  // child's markup one thing for the client to walk to.
81
89
  export { renderComponent } from './renderComponent.ts'
82
90
  // 22.1 — the request being served, the response being built for it, and the listening server.
83
- export { request, response, server } from './requestScope.ts'
91
+ // 22.12 — and where it came from.
92
+ export {
93
+ type ReceivedFrom,
94
+ request,
95
+ requestIP,
96
+ response,
97
+ server,
98
+ } from './requestScope.ts'
84
99
  // 17 — the response helpers, each a write through `response`.
85
100
  export {
86
101
  bytes,
@@ -92,6 +107,8 @@ export {
92
107
  sse,
93
108
  type Values,
94
109
  } from './responseHelpers.ts'
110
+ // 8.25 — a `Store` in a `bun:sqlite` table.
111
+ export { sqliteStore } from './sqliteStore.ts'
95
112
  // 24 — the trace this work belongs to.
96
113
  export { trace } from './trace.ts'
97
114
 
@@ -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
@@ -3,11 +3,11 @@
3
3
 
4
4
  import type { CopyOptions } from '#shared/CopyOptions.ts'
5
5
  import type { JsonValue } from '#shared/JsonValue.ts'
6
- import { mounted as underMount } from '#shared/mount.ts'
7
6
  import type { Gated, HeldToAnnotation } from '#shared/reactive/argsContract.ts'
8
7
  import type { Defaults } from '#shared/reactive/parameterDefaults.ts'
9
8
  import type { Method, Rpc } from '#shared/rpc.ts'
10
9
 
10
+ import { addressed } from './addressed.ts'
11
11
  import { answered } from './answerFailure.ts'
12
12
  import { derivedArgs } from './argsContract.ts'
13
13
  import { type LaneRung, rungsOver } from './hooks.ts'
@@ -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
  *
@@ -111,11 +116,7 @@ export function mountedOwn(
111
116
  name: string,
112
117
  handler: object,
113
118
  ): [string, Mounted] {
114
- // 16.10 — READ WHEN IT IS REPORTED, where a mounted handler's is stamped at boot: the mount is
115
- // the configuration's, and nothing boots this handler.
116
- Object.defineProperty(handler, 'address', {
117
- get: () => underMount(address),
118
- })
119
+ addressed(handler, address)
119
120
  return [
120
121
  address,
121
122
  {
@@ -134,6 +135,7 @@ export function mountedOwn(
134
135
  cli: false,
135
136
  mcp: false,
136
137
  description: undefined,
138
+ middleware: [],
137
139
  copy: undefined,
138
140
  defaults: undefined,
139
141
  args: undefined,
@@ -162,7 +164,7 @@ export async function mountHandlers(
162
164
  const handler = exported as Mounted['handler']
163
165
  // 16.9, 16.10 — the address `rpc.url` and `rpc.raw` build on, under the mount — and what a
164
166
  // page's render turns a call it made into the browser's request by (41.20).
165
- handler.address = underMount(mount.address)
167
+ addressed(handler, mount.address)
166
168
  // 19.9 — the schema the build derived off its annotation, for a handler whose module did not
167
169
  // file it on load: a bundled executable's faces carry no `derivedArgs` call.
168
170
  if (mount.args !== undefined) derivedArgs(handler, mount.args)