@abide/abide 0.121.0 → 0.122.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 CHANGED
@@ -57,6 +57,10 @@ affected code saying what it did, and tell the user.
57
57
  scripts, the CLI and MCP alike.
58
58
  - A `.abide` cannot export. A `<script server>` in one holds that file's private handlers
59
59
  (`const save = POST(…)`), callable by that file alone, and its hooks (`onStart`, `middleware`, …).
60
+ - A rung shared between handlers or layouts is typed `Middleware` and nothing else:
61
+ `export const signedIn: Middleware = async (next) => (await principal.authenticated) ? next() : refuse(401)`
62
+ fits a handler's `middleware: […]` and a layout's `middleware(…)` alike (RULEBOOK 37.14). No
63
+ `<Ctx, Result>` of its own, and no `as` on a `refuse`.
60
64
  - A directory holds addresses, tests, benches, `.d.ts` declarations and two fenced directories
61
65
  alone: `lib/` for components and modules, and `public/` for files served as they are. Anything
62
66
  else beside the pages is a build error.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,21 @@
1
1
  # @abide/abide
2
2
 
3
+ ## 0.122.0
4
+
5
+ ### Minor Changes
6
+
7
+ - a2144de: `Middleware` is one type for a rung shared between handlers and layouts: `const signedIn: Middleware = async (next) => …` fits a handler's `middleware: […]` and a layout's `middleware(…)` alike, with no `<Ctx, Result>` of its own. `Middleware<Response>` is a rung that answers with a `Response` of its own, and `Middleware<never, { args: () => unknown }>` one that reads a handler's args. A `refuse(…)` a layout's rung returns is now answered in its lane — the nearest error page over a page, Problem Details over a handler, and by what the request accepts before routing — and the rung outside it is handed that `Response` (RULEBOOK 37.14, 37.15). BREAKING: `Middleware<Ctx, Result>` written with a context type first no longer reads that way; annotate `next` instead, or use the forms above.
8
+
9
+ ### Patch Changes
10
+
11
+ - 44e9f1d: ABIDE.md names the one-word Middleware form for a shared rung (RULEBOOK 37.14) ([`0699004`](https://github.com/briancray/abide/commit/0699004266bf8174263d1820cee2e9946eeee8a3))
12
+
13
+ ## 0.121.1
14
+
15
+ ### Patch Changes
16
+
17
+ - 7d6571d: a pairing link written into an href survives the URL scheme check — abide-bridge: joins the safe set, and the render warning names the set it checked against (RULEBOOK 32.20, 44.40) ([`aaba479`](https://github.com/briancray/abide/commit/aaba479a86b318d2441e920b6f7cfd124cb6c75e))
18
+
3
19
  ## 0.121.0
4
20
 
5
21
  ### Minor Changes
package/docs/REGISTRY.md CHANGED
@@ -218,7 +218,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
218
218
  | `Value` | `unknown` | What the addressed `Reactive` holds. | 16.3, 16.30 |
219
219
  | `Produced` | `unknown` | What it yields, per the unit rule. | 1.2, 16.3 |
220
220
  | `Args` | `Record<string, JsonValue> \| undefined` | The arguments, in the body on a mutation and in the URL on a `GET` or `DELETE`. | 11.24, 16.18, 19.15 |
221
- | `Middleware` | `<Ctx, Result>(next: (ctx?: Ctx) => Promise<Result>, ctx: Ctx) => Result \| Promise<Result>` | One rung of `middleware`. | 16.14, 16.15, 16.16, 16.21 |
221
+ | `Middleware` | `Middleware<Answer = never, Extra extends object = object> = <Given extends { request: Request } & Extra, Result>(next: (ctx?: Given) => Promise<Result>, ctx: Given) => Result \| Answer \| Failed<string, unknown> \| Promise<Result \| Answer \| Failed<string, unknown>>` | One rung, written once for every lane whose context carries `Extra`. | 16.14, 16.15, 16.16, 16.21, 37.14, 37.15 |
222
222
  | `GET` | `<Value, Args, Failures, RungFailures, Produced = Value>(handler: Reactive<Value, Value, Failures, Produced> \| Keyed<Value, Args, Value, Failures, Produced>, options?: RpcOptions<Args, RungFailures>) => Rpc<Value, Args, Failures \| RungFailures, Produced>` | Declares a read any surface may call. | 16.1, 16.2, 16.5, 16.6, 16.30, 16.31, 16.32, 16.68, 16.76, 16.34, 16.35, 16.36, 16.37, 16.39, 16.44, 16.45, 16.46, 16.47, 16.75, 16.50, 16.51, 16.52, 16.64, 18.1, 18.3, 18.6, 18.8, 18.10, 18.12, 18.21, 18.24, 18.27 |
223
223
  | `GET` | `<Value, Args, Failures, RungFailures, Produced = Value>(handler: (args?: Args) => Value, options?: RpcOptions<Args, RungFailures> & ReactiveOptions<Value, Value, Failures, Args>) => Rpc<Value, Args, Failures \| RungFailures, Produced>` | The plain-function overload, whose `ReactiveOptions` (`ttl`, `global`, `debounce` and the rest) build the value it becomes beside its address. | 16.1, 16.6, 16.7 |
224
224
  | `GET` | `<Value, Failures, RungFailures>(handler: Value \| PromiseLike<Value>, options?: RpcOptions<undefined, RungFailures> & ReactiveOptions<Value, Value, Failures>) => Rpc<Value, undefined, Failures \| RungFailures>` | The value overload, for what the handler owns outright, whose `ReactiveOptions` build the value it becomes. | 16.1, 16.6, 16.7 |
@@ -240,7 +240,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
240
240
  | Name | Signature | Meaning | Rules |
241
241
  | --- | --- | --- | --- |
242
242
  | `description` | `string` | The human description carried onto every generated surface. | 16.28 |
243
- | `middleware` | `Middleware<{ request: Request; args: () => Args }, Value \| Failures>[]` | The rungs run per call. | 16.11, 16.12, 16.16, 16.17, 16.18, 16.19, 16.20 |
243
+ | `middleware` | `((next: (ctx?: { request: Request; args: () => Args }) => Promise<unknown>, ctx: { request: Request; args: () => Args }) => unknown)[]` | The rungs run per call. | 16.11, 16.12, 16.16, 16.17, 16.18, 16.19, 16.20 |
244
244
  | `timeout` | `number` | How long the call may go without progress. | 16.22, 16.58, 16.59, 16.60, 16.61, 16.77 |
245
245
  | `clients` | `Clients` | Which surfaces carry this handler. | 16.37, 20.3 |
246
246
  | `crossOrigin` | `boolean \| string[]` | Whether other origins may call it, or follow it over the mux. | 16.24, 18.2, 21.19 |
@@ -351,7 +351,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
351
351
  | --- | --- | --- | --- |
352
352
  | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | 44.5, 44.13, 44.17, 44.18, 44.19, 44.24, 44.25, 44.27, 44.30, 44.33 |
353
353
  | `ClaudeCodeConfig` | `{ model?: string; fallbackModel?: string; systemPrompt?: string; appendSystemPrompt?: string; effort?: 'low' \| 'medium' \| 'high' \| 'xhigh' \| 'max'; maxBudgetUsd?: number; betas?: string[]; disallowedTools?: string[]; tools?: string[] }` | The Claude Code options a site may set for a turn, and the built-in tools it asks the visitor for. | 44.16, 44.29 |
354
- | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the built-in tools it is given. | 44.14, 44.32, 44.33, 44.40, 44.41, 44.42, 44.43 |
354
+ | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the built-in tools it is given. | 44.14, 44.32, 44.33, 44.40, 44.41, 44.42, 44.43, 44.44 |
355
355
  | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | 44.20, 44.22, 44.32, 44.33, 44.34, 44.40 |
356
356
  | `BridgeStatus` | `{ paired: boolean; command: string \| undefined; link: string \| undefined }` | Whether a bridge is paired, and the command and the link that pair one where none is. | 44.20, 44.40 |
357
357
  | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | 36.7, 44.14, 44.15, 44.19, 44.23, 44.26, 16.57, 16.78 |
@@ -499,7 +499,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
499
499
 
500
500
  | Name | Signature | Meaning | Rules |
501
501
  | --- | --- | --- | --- |
502
- | `csp` | `(sources?: Record<string, string[]>) => Middleware` | The rung that sets `content-security-policy`. | 29.1, 29.2, 29.5, 29.6, 29.7, 29.8 |
502
+ | `csp` | `(sources?: Record<string, string[]>) => (next: (ctx?: { request: Request }) => Promise<Response>, ctx: { request: Request }) => Promise<Response>` | The rung that sets `content-security-policy`. | 29.1, 29.2, 29.5, 29.6, 29.7, 29.8 |
503
503
  | `csp.nonce` | `() => string` | This response's nonce, for the app's own inline script. | 29.3, 29.4, 29.9, 29.11 |
504
504
 
505
505
  ## `url`
@@ -702,7 +702,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
702
702
 
703
703
  | Name | Signature | Meaning | Rules |
704
704
  | --- | --- | --- | --- |
705
- | `middleware` | `(...rungs: Middleware<{ request: Request }, Response>[]) => () => void` | The per-request lane at the calling file's route. | 18.26, 37.2, 37.3, 37.13 |
705
+ | `middleware` | `(...rungs: ((next: (ctx?: { request: Request }) => Promise<Response>, ctx: { request: Request }) => Response \| Failed<string, unknown> \| Promise<Response \| Failed<string, unknown>>)[]) => () => void` | The per-request lane at the calling file's route. | 18.26, 37.2, 37.3, 37.13, 37.14, 37.15 |
706
706
  | `onStart` | `(fn: (start: () => Promise<void>) => void \| Promise<void>) => () => void` | Wraps the boot. | 37.4, 37.6, 37.13 |
707
707
  | `onStop` | `(fn: (stop: () => Promise<void>) => void \| Promise<void>) => () => void` | Mirrors it for teardown. | 37.4, 37.6, 37.13 |
708
708
  | `onError` | `(fn: (error: unknown) => unknown) => () => void` | Runs on an unexpected error at the calling file's route. | 37.5, 37.8, 37.13 |
package/docs/RULEBOOK.md CHANGED
@@ -1959,8 +1959,8 @@ body.
1959
1959
  MUST NOT be built as a string.
1960
1960
 
1961
1961
  32.20 An interpolation in a URL-typed attribute MUST be scheme-checked, and a scheme outside
1962
- `http:`, `https:`, `mailto:`, `tel:`, `blob:`, `data:` or a relative reference MUST be dropped and warned on
1963
- `abide:render`, naming the attribute and not the value.
1962
+ `http:`, `https:`, `mailto:`, `tel:`, `blob:`, `data:`, `abide-bridge:` (44.40) or a relative
1963
+ reference MUST be dropped and warned on `abide:render`, naming the attribute and not the value.
1964
1964
 
1965
1965
  32.21 A control block MUST bind each reactive read and probe call in its subject once for the body.
1966
1966
 
@@ -2454,6 +2454,13 @@ through `App.stop`, and the process MUST then exit with 0.
2454
2454
  37.13 A lifecycle hook called in a module of the app's own that is neither a `.abide` nor a handler
2455
2455
  file MUST be a build error naming the module and the hook.
2456
2456
 
2457
+ 37.14 A `Failed` a `middleware` rung returns MUST be answered in the lane the rung is filed over: by
2458
+ the nearest `error.abide` per 35.2 over a page, as 15.21 states over a handler, and as 15.22 paints
2459
+ a 405 for a rung filed at the root route.
2460
+
2461
+ 37.15 A `middleware` rung outside one that returned a `Failed` MUST be handed 37.14's answer from
2462
+ `next`, and MUST NOT be handed the `Failed`.
2463
+
2457
2464
  # 38. Commands
2458
2465
 
2459
2466
  ## The commands
@@ -3409,12 +3416,17 @@ a link in the `abide-bridge` scheme naming the same site, code, bridge release a
3409
3416
  whose release is a version alone, and MUST run the release it names.
3410
3417
 
3411
3418
  44.42 A bridge opening an `abide-bridge` link MUST ask the visitor before it pairs, every time, naming
3412
- the site, the built-in tools and the bridge release the link asks for.
3419
+ the site, the built-in tools and the bridge release the link asks for, and the options and the names
3420
+ of the variables `abide-bridge install` kept (44.44).
3413
3421
 
3414
3422
  44.43 `abide-bridge install` MUST write the app that opens links under the visitor's own Application
3415
3423
  Support and register it there, and `abide-bridge uninstall` MUST unregister it and remove all
3416
3424
  install wrote.
3417
3425
 
3426
+ 44.44 `abide-bridge install` MUST keep the Claude Code options after its `--` and each `--env
3427
+ NAME=VALUE` it is given, and a bridge opening an `abide-bridge` link MUST run Claude Code with those
3428
+ options ahead of the link's built-in tools and with those variables set, and a link MUST NOT set either.
3429
+
3418
3430
  # 45. Offline
3419
3431
 
3420
3432
  45.1 Where `ABIDE_OFFLINE` is on, a page `abide build` built MUST register the worker
package/docs/abide.md CHANGED
@@ -4227,24 +4227,28 @@ async function exportAll() {
4227
4227
  ```
4228
4228
 
4229
4229
  ```ts src/account/exportContacts.handler.ts
4230
- import { principal, refuse } from '@abide/abide'
4231
4230
  import { POST } from '@abide/abide/server'
4232
4231
 
4233
4232
  import { CONTACTS } from '../contacts/lib/CONTACTS.server.ts'
4233
+ import { adminsOnly } from './lib/adminsOnly.ts'
4234
4234
 
4235
4235
  export default POST(async () => CONTACTS, {
4236
4236
  description: 'Every contact, for an admin to export.',
4237
- middleware: [
4238
- // Runs before the handler, on every call, in process or not.
4239
- async (next, ctx) => {
4240
- const { role } = await principal.resolved
4241
- if (role !== 'admin') return refuse(403, 'Admins only.')
4242
- return next(ctx)
4243
- },
4244
- ],
4237
+ // Runs before the handler, on every call, in process or not.
4238
+ middleware: [adminsOnly],
4245
4239
  })
4246
4240
  ```
4247
4241
 
4242
+ ```ts src/account/lib/adminsOnly.ts
4243
+ import { type Middleware, principal, refuse } from '@abide/abide'
4244
+
4245
+ // One rung for a handler's `middleware` and a layout's alike.
4246
+ export const adminsOnly: Middleware = async (next) => {
4247
+ const { role } = await principal.resolved
4248
+ return role === 'admin' ? next() : refuse(403, 'Admins only.')
4249
+ }
4250
+ ```
4251
+
4248
4252
  ```ts src/account/lib/ExportContacts.test.ts
4249
4253
  import { expect, test } from 'bun:test'
4250
4254
 
@@ -4278,22 +4282,18 @@ test('sales and a signed-out caller are refused 403', async () => {
4278
4282
  })
4279
4283
  ```
4280
4284
 
4281
- A rung is a `Middleware`: it is handed `next` and the call's context, and either returns `next(ctx)` or answers in the handler's place. Returning `refuse(403, …)` refuses, and neither the handler nor any rung after it runs. A throw escapes every rung rather than unwinding through them.
4285
+ A rung is a `Middleware`: it is handed `next` and the call's context, and either returns `next()` or answers in the handler's place. Returning `refuse(403, …)` refuses, and neither the handler nor any rung after it runs. A throw escapes every rung rather than unwinding through them.
4282
4286
 
4283
4287
  The rung sits on the handler, so it runs on every call: from a browser, from another handler, and from a page rendering on the server. It reads arguments through `ctx.args()`, which parses the body on its first call. So a rung that never asks refuses a signed-out caller before their body is parsed or checked.
4284
4288
 
4285
- To guard a whole section, call `middleware(…)` in its `layout.abide`, such as `src/account/admin/layout.abide`. That rung runs over every page and handler below it, and it has the request and nothing else:
4289
+ To guard a whole section, call `middleware(…)` in its `layout.abide`, such as `src/account/admin/layout.abide`. That rung runs over every page and handler below it, and it has the request and nothing else. The same `adminsOnly` fits there, and its refusal is painted by the section's `error.abide` over a page and answered as Problem Details over a handler:
4286
4290
 
4287
4291
  ```abide abide
4288
4292
  <script server>
4289
- import { principal } from '@abide/abide'
4290
4293
  import { middleware } from '@abide/abide/server'
4294
+ import { adminsOnly } from '#src/account/lib/adminsOnly.ts'
4291
4295
 
4292
- middleware(async (next) => {
4293
- const { role } = await principal.resolved
4294
- if (role !== 'admin') return new Response('Admins only.', { status: 403 })
4295
- return next()
4296
- })
4296
+ middleware(adminsOnly)
4297
4297
  </script>
4298
4298
 
4299
4299
  <slot/>
@@ -7776,7 +7776,7 @@ Reactive primitives: what is written in place rather than imported.
7776
7776
 
7777
7777
  | Name | Signature | Meaning | Rules |
7778
7778
  | --- | --- | --- | --- |
7779
- | `Middleware` | `<Ctx, Result>(next: (ctx?: Ctx) => Promise<Result>, ctx: Ctx) => Result \| Promise<Result>` | One rung of `middleware`. | [16.14](/rulebook#16.14), [16.15](/rulebook#16.15), [16.16](/rulebook#16.16), [16.21](/rulebook#16.21) |
7779
+ | `Middleware` | `Middleware<Answer = never, Extra extends object = object> = <Given extends { request: Request } & Extra, Result>(next: (ctx?: Given) => Promise<Result>, ctx: Given) => Result \| Answer \| Failed<string, unknown> \| Promise<Result \| Answer \| Failed<string, unknown>>` | One rung, written once for every lane whose context carries `Extra`. | [16.14](/rulebook#16.14), [16.15](/rulebook#16.15), [16.16](/rulebook#16.16), [16.21](/rulebook#16.21), [37.14](/rulebook#37.14), [37.15](/rulebook#37.15) |
7780
7780
 
7781
7781
  ## Taught in
7782
7782
 
@@ -7851,7 +7851,7 @@ Reactive primitives: what is written in place rather than imported.
7851
7851
  | Name | Signature | Meaning | Rules |
7852
7852
  | --- | --- | --- | --- |
7853
7853
  | `description` | `string` | The human description carried onto every generated surface. | [16.28](/rulebook#16.28) |
7854
- | `middleware` | `Middleware<{ request: Request; args: () => Args }, Value \| Failures>[]` | The rungs run per call. | [16.11](/rulebook#16.11), [16.12](/rulebook#16.12), [16.16](/rulebook#16.16), [16.17](/rulebook#16.17), [16.18](/rulebook#16.18), [16.19](/rulebook#16.19), [16.20](/rulebook#16.20) |
7854
+ | `middleware` | `((next: (ctx?: { request: Request; args: () => Args }) => Promise<unknown>, ctx: { request: Request; args: () => Args }) => unknown)[]` | The rungs run per call. | [16.11](/rulebook#16.11), [16.12](/rulebook#16.12), [16.16](/rulebook#16.16), [16.17](/rulebook#16.17), [16.18](/rulebook#16.18), [16.19](/rulebook#16.19), [16.20](/rulebook#16.20) |
7855
7855
  | `timeout` | `number` | How long the call may go without progress. | [16.22](/rulebook#16.22), [16.58](/rulebook#16.58), [16.59](/rulebook#16.59), [16.60](/rulebook#16.60), [16.61](/rulebook#16.61), [16.77](/rulebook#16.77) |
7856
7856
  | `clients` | `Clients` | Which surfaces carry this handler. | [16.37](/rulebook#16.37), [20.3](/rulebook#20.3) |
7857
7857
  | `crossOrigin` | `boolean \| string[]` | Whether other origins may call it, or follow it over the mux. | [16.24](/rulebook#16.24), [18.2](/rulebook#18.2), [21.19](/rulebook#21.19) |
@@ -8419,7 +8419,7 @@ Generated surfaces: what is written in place rather than imported.
8419
8419
  | Name | Signature | Meaning | Rules |
8420
8420
  | --- | --- | --- | --- |
8421
8421
  | `claudeCode` | `(config?: ClaudeCodeConfig) => Provider` | The visitor's own Claude Code, reached through the bridge paired with their browser, as a provider. | [44.5](/rulebook#44.5), [44.13](/rulebook#44.13), [44.17](/rulebook#44.17), [44.18](/rulebook#44.18), [44.19](/rulebook#44.19), [44.24](/rulebook#44.24), [44.25](/rulebook#44.25), [44.27](/rulebook#44.27), [44.30](/rulebook#44.30), [44.33](/rulebook#44.33) |
8422
- | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the built-in tools it is given. | [44.14](/rulebook#44.14), [44.32](/rulebook#44.32), [44.33](/rulebook#44.33), [44.40](/rulebook#44.40), [44.41](/rulebook#44.41), [44.42](/rulebook#44.42), [44.43](/rulebook#44.43) |
8422
+ | `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; link: string; expires: number }` | The command and the link that pair a bridge with this browser, granting the built-in tools it is given. | [44.14](/rulebook#44.14), [44.32](/rulebook#44.32), [44.33](/rulebook#44.33), [44.40](/rulebook#44.40), [44.41](/rulebook#44.41), [44.42](/rulebook#44.42), [44.43](/rulebook#44.43), [44.44](/rulebook#44.44) |
8423
8423
  | `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | [44.20](/rulebook#44.20), [44.22](/rulebook#44.22), [44.32](/rulebook#44.32), [44.33](/rulebook#44.33), [44.34](/rulebook#44.34), [44.40](/rulebook#44.40) |
8424
8424
  | `/__abide/bridge` | `GET`, `POST` | Where a bridge pairs, and is relayed its turns. | [36.7](/rulebook#36.7), [44.14](/rulebook#44.14), [44.15](/rulebook#44.15), [44.19](/rulebook#44.19), [44.23](/rulebook#44.23), [44.26](/rulebook#44.26), [16.57](/rulebook#16.57), [16.78](/rulebook#16.78) |
8425
8425
 
@@ -8977,7 +8977,7 @@ Generated surfaces: what is written in place rather than imported.
8977
8977
 
8978
8978
  | Name | Signature | Meaning | Rules |
8979
8979
  | --- | --- | --- | --- |
8980
- | `csp` | `(sources?: Record<string, string[]>) => Middleware` | The rung that sets `content-security-policy`. | [29.1](/rulebook#29.1), [29.2](/rulebook#29.2), [29.5](/rulebook#29.5), [29.6](/rulebook#29.6), [29.7](/rulebook#29.7), [29.8](/rulebook#29.8) |
8980
+ | `csp` | `(sources?: Record<string, string[]>) => (next: (ctx?: { request: Request }) => Promise<Response>, ctx: { request: Request }) => Promise<Response>` | The rung that sets `content-security-policy`. | [29.1](/rulebook#29.1), [29.2](/rulebook#29.2), [29.5](/rulebook#29.5), [29.6](/rulebook#29.6), [29.7](/rulebook#29.7), [29.8](/rulebook#29.8) |
8981
8981
  | `csp.nonce` | `() => string` | This response's nonce, for the app's own inline script. | [29.3](/rulebook#29.3), [29.4](/rulebook#29.4), [29.9](/rulebook#29.9), [29.11](/rulebook#29.11) |
8982
8982
 
8983
8983
  ## Taught in
@@ -9511,7 +9511,7 @@ Configuration: what is written in place rather than imported.
9511
9511
 
9512
9512
  | Name | Signature | Meaning | Rules |
9513
9513
  | --- | --- | --- | --- |
9514
- | `middleware` | `(...rungs: Middleware<{ request: Request }, Response>[]) => () => void` | The per-request lane at the calling file's route. | [18.26](/rulebook#18.26), [37.2](/rulebook#37.2), [37.3](/rulebook#37.3), [37.13](/rulebook#37.13) |
9514
+ | `middleware` | `(...rungs: ((next: (ctx?: { request: Request }) => Promise<Response>, ctx: { request: Request }) => Response \| Failed<string, unknown> \| Promise<Response \| Failed<string, unknown>>)[]) => () => void` | The per-request lane at the calling file's route. | [18.26](/rulebook#18.26), [37.2](/rulebook#37.2), [37.3](/rulebook#37.3), [37.13](/rulebook#37.13), [37.14](/rulebook#37.14), [37.15](/rulebook#37.15) |
9515
9515
 
9516
9516
  ## Taught in
9517
9517
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@abide/abide",
3
- "version": "0.121.0",
3
+ "version": "0.122.0",
4
4
  "type": "module",
5
5
  "description": "Isomorphic type-safe framework for reactive async interfaces for humans and machines",
6
6
  "license": "MIT",
@@ -1,3 +1,3 @@
1
1
  // The `abide-bridge` release a pairing command names, so a visitor's `bunx` runs the relay this
2
2
  // server speaks. `packages/abide/tests/claudeCode.test.ts` holds it to the bridge package's own.
3
- export const BRIDGE_VERSION = '0.4.0'
3
+ export const BRIDGE_VERSION = '0.5.0'
@@ -10,6 +10,7 @@ import { coversRoute, parseRoutePattern } from '#shared/routePattern.ts'
10
10
 
11
11
  import type { AppSources } from './answerRequest.ts'
12
12
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
13
+ import { prefers } from './prefers.ts'
13
14
  import { boundPage, renderDocument } from './render.ts'
14
15
  import { beginRender, currentRequestScope } from './requestScope.ts'
15
16
 
@@ -100,3 +101,18 @@ export async function answerFailure(
100
101
  headers.set('content-type', 'text/plain; charset=utf-8')
101
102
  return new Response(failure.message, { status, headers })
102
103
  }
104
+
105
+ /**
106
+ * A failure at an address whose lane only the request can say: a page's, painted, where it prefers
107
+ * a document, and a handler's otherwise. 15.22.
108
+ */
109
+ export function laneFailure(
110
+ app: ErrorPages,
111
+ pathname: string,
112
+ failure: Failed<string, unknown>,
113
+ request: Request,
114
+ ): Response | Promise<Response> {
115
+ return prefers(request, 'text/html', 'application/json')
116
+ ? answerFailure(app, pathname, failure)
117
+ : answered(failure)
118
+ }
@@ -22,7 +22,7 @@ import { DEBUG_META } from '#shared/DEBUG_META.ts'
22
22
  import { decodedOrKept } from '#shared/decodedOrKept.ts'
23
23
  import { ERROR_META } from '#shared/ERROR_META.ts'
24
24
  import { escapeAttribute } from '#shared/escapeText.ts'
25
- import { type Failed, isFailed } from '#shared/Failed.ts'
25
+ import { isFailed } from '#shared/Failed.ts'
26
26
  import { HandlerRoute } from '#shared/HandlerRoute.ts'
27
27
  import { health } from '#shared/health.ts'
28
28
  import { KEEP_HEADER } from '#shared/KEEP_HEADER.ts'
@@ -45,7 +45,7 @@ import {
45
45
  import type { Method, Rpc } from '#shared/rpc.ts'
46
46
  import { rpcRequest } from '#shared/rpcRequest.ts'
47
47
 
48
- import { answered, answerFailure } from './answerFailure.ts'
48
+ import { answerFailure, laneFailure } from './answerFailure.ts'
49
49
  import {
50
50
  answerRpc,
51
51
  type Mounted,
@@ -64,12 +64,11 @@ import { settingOn } from './config.ts'
64
64
  import { noteCallInRender } from './deferredBuilds.ts'
65
65
  import { DOCUMENT_HEADERS } from './DOCUMENT_HEADERS.ts'
66
66
  import { answerMcp, openApi } from './generatedSurfaces.ts'
67
- import { type AppRung, failureOf, rungsOver } from './hooks.ts'
67
+ import { failureOf, type LaneRung, rungsOver } from './hooks.ts'
68
68
  import { chunksOf, type Manifest } from './manifestOf.ts'
69
69
  import { muxAdmission } from './muxUpgrade.ts'
70
70
  import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
71
71
  import { plainAnswer } from './plainAnswer.ts'
72
- import { prefers } from './prefers.ts'
73
72
  import { PRINCIPAL_MOUNT } from './principal.ts'
74
73
  import {
75
74
  boundPage,
@@ -625,7 +624,18 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
625
624
  if (row.page !== undefined)
626
625
  row.answers.set(
627
626
  row.page,
628
- answerInside(sources, row.page, rungsOver(row.pattern)),
627
+ answerInside(
628
+ sources,
629
+ row.page,
630
+ // 37.14 — a page's lane paints a refusal as its error page does (35.2).
631
+ rungsOver(row.pattern, (failure, request) =>
632
+ answerFailure(
633
+ sources,
634
+ new URL(request.url).pathname,
635
+ failure,
636
+ ),
637
+ ),
638
+ ),
629
639
  )
630
640
  for (const handler of row.handlers.values())
631
641
  row.answers.set(
@@ -640,7 +650,7 @@ export async function appSources(parts: SourceParts): Promise<AppSources> {
640
650
  function answerInside(
641
651
  app: AppSources,
642
652
  found: Ranked | Mounted,
643
- rungs: AppRung[],
653
+ rungs: LaneRung[],
644
654
  ): (ctx: { request: Request }) => Promise<Response> {
645
655
  return compose(rungs, ({ request }) => answerMatched(app, request, found))
646
656
  }
@@ -774,21 +784,6 @@ async function methodRefused(
774
784
  return answer
775
785
  }
776
786
 
777
- /**
778
- * A failure at an address whose lane only the request can say: a page's, painted, where it prefers
779
- * a document, and a handler's otherwise. 15.22.
780
- */
781
- function laneFailure(
782
- app: AppSources,
783
- pathname: string,
784
- failure: Failed<string, unknown>,
785
- request: Request,
786
- ): Response | Promise<Response> {
787
- return prefers(request, 'text/html', 'application/json')
788
- ? answerFailure(app, pathname, failure)
789
- : answered(failure)
790
- }
791
-
792
787
  /**
793
788
  * A public file's answer, conditional and range-aware off the etag read at boot.
794
789
  *
@@ -44,7 +44,7 @@ import {
44
44
  crossingOrigin,
45
45
  crossingRefusal,
46
46
  } from './crossingOrigin.ts'
47
- import { type AppRung, failureOf, rungsOver } from './hooks.ts'
47
+ import { failureOf, type LaneRung, rungsOver } from './hooks.ts'
48
48
  import { OFFLINE_STALE } from './OFFLINE_STALE.ts'
49
49
  import { prefers } from './prefers.ts'
50
50
  import {
@@ -120,7 +120,7 @@ export type Mounted = Mount & {
120
120
  * The rungs filed over `scope`, read once at mount (16.14) — what an http call and a socket
121
121
  * subscription to it both pass through (37.2, 18.26).
122
122
  */
123
- rungs: AppRung[]
123
+ rungs: LaneRung[]
124
124
  handler: Rpc<unknown, unknown> & Gated<unknown> & HeldToAnnotation
125
125
  /**
126
126
  * The args' JSON Schema, probed once at mount.
@@ -203,7 +203,8 @@ export async function mountHandlers(
203
203
  ...mount,
204
204
  // 37.1, 16.72 — the route its file's hooks are filed at, which the build read off
205
205
  // where the file answers (23.19's mount for a package's), never its place on disk.
206
- rungs: rungsOver(mount.scope),
206
+ // 37.14 — a handler's lane answers a refusal as Problem Details (15.1).
207
+ rungs: rungsOver(mount.scope, answered),
207
208
  handler,
208
209
  argsDocument: declared ?? (mount.args as Document | undefined),
209
210
  })
@@ -8,6 +8,7 @@
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 { BRIDGE_SCHEME } from '#shared/BRIDGE_SCHEME.ts'
11
12
  import { mounted } from '#shared/mount.ts'
12
13
  import { refuse } from '#shared/refuse.ts'
13
14
 
@@ -19,7 +20,6 @@ import {
19
20
  forwardOf,
20
21
  type Provider,
21
22
  } from './assistant.ts'
22
- import { BRIDGE_SCHEME } from './BRIDGE_SCHEME.ts'
23
23
  import { BRIDGE_VERSION } from './BRIDGE_VERSION.ts'
24
24
  import {
25
25
  followSignIn,
@@ -17,6 +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 { servedTable } from './abideModules.ts'
20
+ import { laneFailure } from './answerFailure.ts'
20
21
  import { answerRequest, type AppSources } from './answerRequest.ts'
21
22
  import type { Mount } from './answerRpc.ts'
22
23
  import { Mux, type MuxConnection } from './answerSocket.ts'
@@ -278,7 +279,10 @@ export async function bootEntered(
278
279
  // 16.14, 37.3 — THE APP LANE'S ONION, composed once over the whole request path: the root
279
280
  // route's rungs, the ones below it running after routing (37.2, `answerRequest`). 21.20 — the
280
281
  // center takes its own time back out of what `fetch` counts for the whole onion.
281
- const rungs = rootRungs()
282
+ // 37.14 — before routing, the lane is the request's to say (15.22).
283
+ const rungs = rootRungs((failure, request) =>
284
+ laneFailure(booted, new URL(request.url).pathname, failure, request),
285
+ )
282
286
  const answer = compose(rungs, async ({ request }) => {
283
287
  const began = performance.now()
284
288
  const answered = await answerRequest(booted, request)
package/src/server/csp.ts CHANGED
@@ -5,7 +5,7 @@
5
5
  // THE NONCE IS MINTED BY WHICHEVER ASKS FIRST — a page reading `csp.nonce` while it renders, or this
6
6
  // rung writing a document's header — so the two agree, and an answer that is not a document mints none.
7
7
 
8
- import type { Middleware } from '#shared/Middleware.ts'
8
+ import type { Rung } from '#shared/Middleware.ts'
9
9
 
10
10
  import { randomToken } from './randomToken.ts'
11
11
  import { EARLY_CALL_HASH } from './render.ts'
@@ -64,7 +64,7 @@ function policy(
64
64
 
65
65
  export function cspRung(
66
66
  sources: Record<string, string[]> | undefined,
67
- ): Middleware<{ request: Request }, Response> {
67
+ ): Rung<{ request: Request }, Response> {
68
68
  const written = policy(sources)
69
69
  return async (next) => {
70
70
  const answer = await next()
@@ -13,7 +13,7 @@
13
13
 
14
14
  import { type Failed, isFailed } from '#shared/Failed.ts'
15
15
  import { namedLogger } from '#shared/log.ts'
16
- import type { Middleware } from '#shared/Middleware.ts'
16
+ import type { Rung } from '#shared/Middleware.ts'
17
17
  import { aborted, refuse } from '#shared/refuse.ts'
18
18
  import {
19
19
  coversRoute,
@@ -26,7 +26,25 @@ import { config } from './config.ts'
26
26
  import type { LauncherOptions } from './launcherOptions.ts'
27
27
  import { currentRequestScope } from './requestScope.ts'
28
28
 
29
- export type AppRung = Middleware<{ request: Request }, Response>
29
+ type AppContext = { request: Request }
30
+
31
+ /** A rung `middleware` files: what `next` answered passed on, or a refusal in its place (37.14). */
32
+ export type AppRung = (
33
+ next: (ctx?: AppContext) => Promise<Response>,
34
+ ctx: AppContext,
35
+ ) =>
36
+ | Response
37
+ | Failed<string, unknown>
38
+ | Promise<Response | Failed<string, unknown>>
39
+
40
+ /** An app rung as its lane composes it, a refusal it returned already answered (37.14). */
41
+ export type LaneRung = Rung<AppContext, Response>
42
+
43
+ /** How a lane answers a refusal one of its rungs returned, in that lane's representation (37.14). */
44
+ export type RefusalAnswer = (
45
+ failure: Failed<string, unknown>,
46
+ request: Request,
47
+ ) => Response | Promise<Response>
30
48
 
31
49
  export type Env = Record<string, string | undefined>
32
50
  export type ConfigDefaults = (env: Env) => unknown
@@ -106,11 +124,24 @@ export function inOrder<T>(list: readonly Filed<T>[]): T[] {
106
124
  return items
107
125
  }
108
126
 
127
+ /**
128
+ * `rung` with a refusal it returns answered by `answer`.
129
+ *
130
+ * 37.14, 37.15 — PER LINK, not at the onion's edge: a rung outside this one is handed the answer
131
+ * from `next`, so one that reads a `Response` (29.2's) reads one whichever rung inside it refused.
132
+ */
133
+ function answering(rung: AppRung, answer: RefusalAnswer): LaneRung {
134
+ return async (next, ctx) => {
135
+ const result = await rung(next, ctx)
136
+ return isFailed(result) ? answer(result, ctx.request) : result
137
+ }
138
+ }
139
+
109
140
  /** The rungs filed at the root route, which run before routing. 37.3. */
110
- export function rootRungs(): AppRung[] {
111
- const rungs: AppRung[] = []
141
+ export function rootRungs(answer: RefusalAnswer): LaneRung[] {
142
+ const rungs: LaneRung[] = []
112
143
  for (const entry of HOOKS.rungs)
113
- if (entry.route === ROOT) rungs.push(entry.item)
144
+ if (entry.route === ROOT) rungs.push(answering(entry.item, answer))
114
145
  return rungs
115
146
  }
116
147
 
@@ -120,12 +151,12 @@ export function rootRungs(): AppRung[] {
120
151
  * 37.2 — every rung filed at `route` or at a route that is its front, the root's aside: those ran
121
152
  * before routing (37.3).
122
153
  */
123
- export function rungsOver(route: string): AppRung[] {
154
+ export function rungsOver(route: string, answer: RefusalAnswer): LaneRung[] {
124
155
  const segments = parseRoutePattern(route)
125
- const rungs: AppRung[] = []
156
+ const rungs: LaneRung[] = []
126
157
  for (const entry of HOOKS.rungs)
127
158
  if (entry.route !== ROOT && coversRoute(entry.segments, segments))
128
- rungs.push(entry.item)
159
+ rungs.push(answering(entry.item, answer))
129
160
  return rungs
130
161
  }
131
162
 
@@ -1,3 +1,5 @@
1
1
  // 44.40 — the scheme a pairing link is written in, which the handler `abide-bridge install` registers
2
- // opens. The bridge spells it out as `PAIRING_SCHEME`, and a test holds the two equal.
2
+ // opens. The bridge spells it out as `PAIRING_SCHEME`, and a test holds the two equal. A leaf in
3
+ // #shared because both seams read it: the server mints the link, and 32.20's `safeUrl` lets a page
4
+ // write it into an `href`.
3
5
  export const BRIDGE_SCHEME = 'abide-bridge'
@@ -1,7 +1,27 @@
1
1
  // ONE RUNG OF AN ONION, and the onion. The app lane's `middleware` (37.3) and a handler's rungs
2
2
  // (16.11) are the same shape over a different `Ctx`, so they compose through the one function.
3
3
 
4
- export type Middleware<Ctx, Result> = (
4
+ import type { Failed } from './Failed.ts'
5
+
6
+ /**
7
+ * A rung written once for every lane whose context carries `Extra`: generic over the context it is
8
+ * handed and over what `next` answers, so it passes `next()`'s answer on, refuses in its place, or
9
+ * answers with an `Answer` of its own.
10
+ */
11
+ export type Middleware<Answer = never, Extra extends object = object> = <
12
+ Given extends { request: Request } & Extra,
13
+ Result,
14
+ >(
15
+ next: (ctx?: Given) => Promise<Result>,
16
+ ctx: Given,
17
+ ) =>
18
+ | Result
19
+ | Answer
20
+ | Failed<string, unknown>
21
+ | Promise<Result | Answer | Failed<string, unknown>>
22
+
23
+ /** A rung as one lane composes it, over that lane's context and what its center answers. */
24
+ export type Rung<Ctx, Result> = (
5
25
  next: (ctx?: Ctx) => Promise<Result>,
6
26
  ctx: Ctx,
7
27
  ) => Result | Promise<Result>
@@ -17,17 +37,17 @@ export type Middleware<Ctx, Result> = (
17
37
  * `next()` with no argument hands the inner link the ctx this rung was given.
18
38
  */
19
39
  export function compose<Ctx, Result>(
20
- rungs: readonly Middleware<Ctx, Result>[],
40
+ rungs: readonly Rung<Ctx, Result>[],
21
41
  terminal: (ctx: Ctx) => Promise<Result>,
22
42
  ): (ctx: Ctx) => Promise<Result> {
23
43
  let link = terminal
24
44
  for (let at = rungs.length - 1; at >= 0; at -= 1)
25
- link = linkOf(rungs[at] as Middleware<Ctx, Result>, link)
45
+ link = linkOf(rungs[at] as Rung<Ctx, Result>, link)
26
46
  return link
27
47
  }
28
48
 
29
49
  function linkOf<Ctx, Result>(
30
- rung: Middleware<Ctx, Result>,
50
+ rung: Rung<Ctx, Result>,
31
51
  inner: (ctx: Ctx) => Promise<Result>,
32
52
  ): (ctx: Ctx) => Promise<Result> {
33
53
  return async (ctx) => rung((given) => inner(given ?? ctx), ctx)
@@ -10,7 +10,7 @@ import { BOOLEAN_ATTRIBUTES } from './BOOLEAN_ATTRIBUTES.ts'
10
10
  import { escapeAttribute } from './escapeText.ts'
11
11
  import { RENDER_LOG } from './log.ts'
12
12
  import { textOf } from './Raw.ts'
13
- import { safeUrl } from './safeUrl.ts'
13
+ import { SAFE_SCHEMES, safeUrl } from './safeUrl.ts'
14
14
  import { URL_ATTRIBUTES } from './URL_ATTRIBUTES.ts'
15
15
 
16
16
  /**
@@ -31,7 +31,7 @@ export function attributeText(name: string, value: unknown): string | null {
31
31
  const safe = safeUrl(text)
32
32
  if (safe !== text)
33
33
  RENDER_LOG.warning(
34
- `abide: \`${name}\` was handed a URL whose scheme is not http:, https:, mailto:, tel:, blob: or data:, and is written empty. RULEBOOK 32.20.`,
34
+ `abide: \`${name}\` was handed a URL whose scheme is not one of ${[...SAFE_SCHEMES].join(' ')}, and is written empty. RULEBOOK 32.20.`,
35
35
  )
36
36
  return safe
37
37
  }
package/src/shared/csp.ts CHANGED
@@ -3,9 +3,9 @@
3
3
  // mint one for, so `csp.nonce` answers the one its document was served with, which is what a
4
4
  // binding that wrote it on the server hydrates to.
5
5
 
6
- import type { Middleware } from './Middleware.ts'
6
+ import type { Rung as LaneRung } from './Middleware.ts'
7
7
 
8
- type Rung = Middleware<{ request: Request }, Response>
8
+ type Rung = LaneRung<{ request: Request }, Response>
9
9
 
10
10
  /**
11
11
  * The server's content-security-policy half, as slots `#server/installServer.ts` fills from
package/src/shared/rpc.ts CHANGED
@@ -19,7 +19,7 @@ import { GLOBAL_GETS } from './GLOBAL_GETS.ts'
19
19
  import { HandlerRoute } from './HandlerRoute.ts'
20
20
  import { IN_PROCESS_ORIGIN } from './IN_PROCESS_ORIGIN.ts'
21
21
  import type { Issues } from './Issues.ts'
22
- import { compose, type Middleware } from './Middleware.ts'
22
+ import { compose, type Rung } from './Middleware.ts'
23
23
  import { outbox, type QueuedCall, sendingFor } from './outbox.ts'
24
24
  import { principalRotated } from './principalRotated.ts'
25
25
  import {
@@ -192,7 +192,7 @@ const SENT: unique symbol = Symbol('abide.sent')
192
192
 
193
193
  export type RpcOptions<Args = unknown> = {
194
194
  description?: string
195
- middleware?: Middleware<RpcContext<Args>, unknown>[]
195
+ middleware?: Rung<RpcContext<Args>, unknown>[]
196
196
  timeout?: number
197
197
  clients?: Clients
198
198
  crossOrigin?: boolean | string[]
@@ -6,15 +6,22 @@
6
6
  //
7
7
  // The warning on `abide:render` the clause also asks for is `attributeText`'s, which knows the
8
8
  // attribute's name.
9
+ //
10
+ // `abide-bridge:` IS IN THE SET because abide mints it: a pairing's `link` (44.40) is written into an
11
+ // `href` or the visitor cannot open it, and the docs' own pairing page does exactly that. It is the
12
+ // one scheme, not custom schemes generally — what it opens asks the visitor before it does anything,
13
+ // every time, naming the site it pairs with (44.42), so a link from anywhere can do no more than ask.
14
+ import { BRIDGE_SCHEME } from './BRIDGE_SCHEME.ts'
9
15
  import { textOf } from './Raw.ts'
10
16
 
11
- const SAFE_SCHEMES = new Set([
17
+ export const SAFE_SCHEMES = new Set([
12
18
  'http:',
13
19
  'https:',
14
20
  'mailto:',
15
21
  'tel:',
16
22
  'blob:',
17
23
  'data:',
24
+ `${BRIDGE_SCHEME}:`,
18
25
  ])
19
26
 
20
27
  /** A URL attribute's text, or `''` where its scheme is outside the safe set. */