@abide/abide 0.112.0 → 0.114.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +20 -0
- package/docs/REGISTRY.md +10 -10
- package/docs/RULEBOOK.md +30 -3
- package/docs/abide.md +52 -44
- package/package.json +1 -1
- package/src/compiler/appLayout.ts +16 -1
- package/src/compiler/classifyTemplate.ts +5 -5
- package/src/compiler/emitClient.ts +45 -2
- package/src/server/bridgeRelay.ts +93 -31
- package/src/server/claudeCode.ts +42 -9
- package/src/server/commands.ts +2 -1
- package/src/server/config.ts +13 -1
- package/src/server/createApp.ts +18 -6
- package/src/server/preload.ts +5 -0
- package/src/server/principal.ts +60 -7
- package/src/server/requestScope.ts +6 -0
- package/src/shared/FOREIGN_ATTRIBUTES.ts +24 -0
- package/src/shared/INTEGRATION_POINTS.ts +20 -0
- package/src/shared/IN_PROCESS_ORIGIN.ts +5 -0
- package/src/shared/reactive/Keyed.ts +41 -14
- package/src/shared/reactive/Reactive.ts +34 -2
- package/src/shared/rpc.ts +2 -1
- package/src/ui/bindSpread.ts +4 -2
- package/src/ui/index.ts +2 -2
- package/src/ui/parseMarkup.ts +20 -25
- package/src/ui/setAttribute.ts +25 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,25 @@
|
|
|
1
1
|
# @abide/abide
|
|
2
2
|
|
|
3
|
+
## 0.114.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 595ccb1: a Claude Code pairing belongs to the sign-in that minted its code — another sign-in at the browser is refused 409 and offered its own, and an open bridge status follows a sign-in or sign-out ([`7036ed1`](https://github.com/briancray/abide/commit/7036ed181763c74eaf72ad4550e05c9d63a2d6ab))
|
|
8
|
+
|
|
9
|
+
## 0.113.0
|
|
10
|
+
|
|
11
|
+
### Minor Changes
|
|
12
|
+
|
|
13
|
+
- 3c32bf6: a Store may watch — each change it reports re-reads get without running the body, and the watch stops when the value is released ([`a5539eb`](https://github.com/briancray/abide/commit/a5539eb9d1673a3b4dd98c0e35cc8d8dfb8c5e96))
|
|
14
|
+
- 3c32bf6: App.run takes a principal — the claims its scope resolves from through onPrincipal, with no cookie written ([`c0db93c`](https://github.com/briancray/abide/commit/c0db93cb8f41533137a7cb3f833627bebbe9d65f))
|
|
15
|
+
- 3c32bf6: under bun test with the test preload, an app that declares no ABIDE_PRINCIPAL_SECRET signs in with one the preload supplies ([`db09b9d`](https://github.com/briancray/abide/commit/db09b9d3aceeffe3d385ae295601ca9158dc2b86))
|
|
16
|
+
|
|
17
|
+
### Patch Changes
|
|
18
|
+
|
|
19
|
+
- 3c32bf6: an element the client builds takes the namespace the parser gives it there — a component inside an svg draws — and an xlink:href it writes is XLink's; the harness counts createElementNS and setAttributeNS ([`0dd560a`](https://github.com/briancray/abide/commit/0dd560a35d54a86d8ff5d3bc7762ed6c6968c71f))
|
|
20
|
+
- 3c32bf6: a Keyed holds no entry for args its gate refuses, and the gate runs once per entry rather than on every refresh ([`1560749`](https://github.com/briancray/abide/commit/1560749419b1c571130c812fb0f6c3e2bac2a021))
|
|
21
|
+
- 3c32bf6: abide check takes a url literal naming one of the framework's own fetched addresses — openapi, mcp, health, principal ([`c8af3dc`](https://github.com/briancray/abide/commit/c8af3dc746766fc5cbe322dbde551b15137c7330))
|
|
22
|
+
|
|
3
23
|
## 0.112.0
|
|
4
24
|
|
|
5
25
|
### Minor Changes
|
package/docs/REGISTRY.md
CHANGED
|
@@ -68,7 +68,7 @@ Accepted, Failures>` is the inner value's own `Accepted`. Then `Failures`, then
|
|
|
68
68
|
| `reactive` | `<Accepted = undefined, Shaped = Accepted, Gate = Schema<Accepted>>(initial: undefined, options?: Omit<ReactiveOptions<Accepted, Kept<Shaped>, never>, 'schema'> & { transform?: (value: Accepted) => Shaped; schema?: Gate & Schema<Accepted> }) => Reactive<Kept<Shaped>, Accepted, Refused<Shaped> \| Gated<Gate, Accepted>>` | The undefined overload, for a value opening at `undefined`. | 4.15, 4.16 |
|
|
69
69
|
| `reactive` | `<Accepted>() => Reactive<Kept<Accepted>, Accepted, Refused<Accepted>>` | The empty overload, for a value nothing has written yet. | 4.14 |
|
|
70
70
|
| `Transformer` | `<Accepted, Stored, Failures = never>(value: Accepted) => Stored \| Failures` | The shape of a `transform`. | 4.6, 4.7, 4.8 |
|
|
71
|
-
| `Store` | `{ get: () => Stored \| undefined \| Promise<Stored \| undefined>; set: (value: Stored, retention: { ttl: number }) => void \| Promise<void
|
|
71
|
+
| `Store` | `{ get: () => Stored \| undefined \| Promise<Stored \| undefined>; set: (value: Stored, retention: { ttl: number }) => void \| Promise<void>; watch?: (changed: () => void) => () => void }` | Where a value lives when this process does not. | 8.1, 8.2, 8.3, 8.4, 8.5, 8.6, 8.7, 8.8, 8.9, 8.10, 8.11, 8.12, 8.13, 8.14, 8.15, 8.16, 8.17, 8.18, 8.19, 8.20, 8.21 |
|
|
72
72
|
| `Shared` | `interface Shared {}` | The app's declaration-merged registry of shared keys. | 10.3 |
|
|
73
73
|
| `reactive.share` | `<Key extends string, Value>(key: Key, create: () => Reactive<Value>) => Key extends keyof Shared ? Reactive<Shared[Key]> : Reactive<Value>` | Get-or-create a reactive value in the component's scope by key. | 10.1, 10.2, 10.4, 10.5, 10.6, 10.7 |
|
|
74
74
|
|
|
@@ -94,7 +94,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
94
94
|
| `structural` | `(next: unknown, previous: unknown) => boolean` | The by-value comparator an app declares on `identity`. | 5.4, 5.5, 18.4 |
|
|
95
95
|
| `by` | `(keyof Element & string) \| ((element: Element) => unknown)`, over the `Element` a `Stored` array holds | What tells one element of the value from another. | 5.22, 18.4, 18.15, 32.32 |
|
|
96
96
|
| `ttl` | `number` | The life of a production, or of a keyed entry. | 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 6.7, 6.8, 6.9, 6.10, 11.6, 11.35, 11.45, 11.48, 11.49, 11.69 |
|
|
97
|
-
| `args` | `Schema<Args>` | The gate on a keyed value's args. | 9.4, 11.28, 11.30, 11.40, 11.41, 11.56, 11.60, 11.68 |
|
|
97
|
+
| `args` | `Schema<Args>` | The gate on a keyed value's args. | 9.4, 11.28, 11.30, 11.40, 11.41, 11.56, 11.60, 11.68, 11.72 |
|
|
98
98
|
| `global` | `boolean` | Opts out of the default scope into one cache shared by every caller in the process. | 11.34, 11.35, 11.36, 11.37, 11.38, 11.39, 11.53, 11.67 |
|
|
99
99
|
| `tags` | `string[] \| ((args: Args) => string[])` | What a `Selection` matches on. | 11.43, 11.44, 18.4, 18.15 |
|
|
100
100
|
| `throttle` | `number` | A ceiling on how often the value changes. | 5.8, 5.9, 5.11, 5.12, 5.14, 5.15, 5.17, 5.18, 5.19, 5.20, 5.21, 5.23, 5.24 |
|
|
@@ -338,10 +338,10 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
338
338
|
|
|
339
339
|
| Name | Signature | Meaning | Rules |
|
|
340
340
|
| --- | --- | --- | --- |
|
|
341
|
-
| `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 |
|
|
341
|
+
| `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 |
|
|
342
342
|
| `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 |
|
|
343
|
-
| `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; expires: number }` | The command that pairs a bridge with this browser, granting the built-in tools it is given. | 44.14, 44.32 |
|
|
344
|
-
| `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | 44.20, 44.22, 44.32 |
|
|
343
|
+
| `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; expires: number }` | The command that pairs a bridge with this browser, granting the built-in tools it is given. | 44.14, 44.32, 44.33 |
|
|
344
|
+
| `claudeCode.bridge` | `(options?: { tools?: string[] }) => AsyncGenerator<BridgeStatus>` | This browser's pairing, as it changes. | 44.20, 44.22, 44.32, 44.33, 44.34 |
|
|
345
345
|
| `BridgeStatus` | `{ paired: boolean; command: string \| undefined }` | Whether a bridge is paired, and the command that pairs one where none is. | 44.20 |
|
|
346
346
|
| `/__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 |
|
|
347
347
|
|
|
@@ -529,7 +529,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
529
529
|
| `{expr}` | Reactive text, escaped. | 32.1, 32.23 |
|
|
530
530
|
| formatting whitespace | What a run of whitespace renders as. | 33.17, 33.18 |
|
|
531
531
|
| `{raw(...)}` | Raw HTML. | 32.2, 32.25, 32.26, 32.30, 32.31, 32.70 |
|
|
532
|
-
| `name={expr}` | A reactive attribute or property. | 32.3, 32.20, 32.23, 32.37, 32.71 |
|
|
532
|
+
| `name={expr}` | A reactive attribute or property. | 32.3, 32.20, 32.23, 32.37, 32.70, 32.71 |
|
|
533
533
|
| `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | 32.4 |
|
|
534
534
|
| `name="…{expr}…"` | An interpolated quoted value. | 32.3, 32.71 |
|
|
535
535
|
| `bind:value` | Two-way bind on an element. | 5.16, 32.5, 32.58, 32.59, 32.60, 32.61, 32.62, 32.72 |
|
|
@@ -564,10 +564,10 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
564
564
|
| Spelling | Meaning | Rules |
|
|
565
565
|
| --- | --- | --- |
|
|
566
566
|
| `{#component Name(pattern)}` | An inline component. | 32.39, 32.40, 32.41, 32.42, 32.43, 32.44, 32.45, 32.46, 32.47, 32.48, 32.49, 32.50, 32.51, 32.52, 32.53, 32.54, 32.55, 32.63, 32.64 |
|
|
567
|
-
| `<Name/>` | A component invocation. | 32.17, 32.34, 32.35, 32.36, 33.19, 33.21, 41.24, 41.28 |
|
|
567
|
+
| `<Name/>` | A component invocation. | 32.17, 32.34, 32.35, 32.36, 32.70, 33.19, 33.21, 41.24, 41.28 |
|
|
568
568
|
| `<slot/>` | The position children render into. | 32.18 |
|
|
569
569
|
| `<slot>fallback</slot>` | Renders children, or a fallback. | 32.18 |
|
|
570
|
-
| `<Tag>…</Tag>` | Children passed to a component's slot. | 32.18 |
|
|
570
|
+
| `<Tag>…</Tag>` | Children passed to a component's slot. | 32.18, 32.70 |
|
|
571
571
|
|
|
572
572
|
## Props
|
|
573
573
|
|
|
@@ -629,7 +629,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
629
629
|
| `APP_NAME` | `string` | The app's name, and `log`'s default namespace. | 27.3, 28.4 |
|
|
630
630
|
| `APP_VERSION` | `string` | The version beside that name. | 27.3 |
|
|
631
631
|
| `APP_DATA_DIR` | `string` | The platform's per-user data directory. | 27.3, 36.4 |
|
|
632
|
-
| `ABIDE_PRINCIPAL_SECRET` | `string \| null` | What seals the principal cookie. | 26.11, 36.5 |
|
|
632
|
+
| `ABIDE_PRINCIPAL_SECRET` | `string \| null` | What seals the principal cookie. | 26.11, 36.5, 38.120 |
|
|
633
633
|
| `ABIDE_PRINCIPAL_TTL` | `number` | The principal cookie's life, in ms. | 26.15, 26.16 |
|
|
634
634
|
| `ABIDE_APP_TOKEN` | `string \| null` | The bearer the remote CLI sends. | 36.6 |
|
|
635
635
|
| `ABIDE_APP_URL` | `string \| null` | Names an app somewhere else, for the remote CLI. | 36.6 |
|
|
@@ -706,7 +706,7 @@ type Gated<Gate, Accepted> = [Schema<Accepted>] extends [Gate] ? never : Failed<
|
|
|
706
706
|
| `createApp` | `() => Promise<App>` | The app built and booted, with no socket bound. | 42.1, 42.2, 42.3, 42.4, 42.16, 37.4, 37.6, 38.100 |
|
|
707
707
|
| `App` | `interface App` | What a host, a test or a script holds the app as. | 42.12, 42.13 |
|
|
708
708
|
| `App.fetch` | `(request: Request) => Promise<Response>` | The whole request path, entered in process. | 42.5, 42.6 |
|
|
709
|
-
| `App.run` | `<Result>(fn: () => Result \| Promise<Result>, options?: { request?: Request }) => Promise<Result>` | One scope around a function, and nothing else. | 42.7, 42.8, 11.38 |
|
|
709
|
+
| `App.run` | `<Result>(fn: () => Result \| Promise<Result>, options?: { request?: Request; principal?: Record<string, unknown> }) => Promise<Result>` | One scope around a function, and nothing else. | 42.7, 42.8, 42.19, 11.38 |
|
|
710
710
|
| `App.listen` | `(options?: { port?: number; hostname?: string; unix?: string }) => Promise<void>` | The socket, bound. | 42.9, 16.23, 16.56, 18.28, 18.29, 18.30 |
|
|
711
711
|
| `App.stop` | `() => Promise<void>` | The drain and the teardown. | 42.10, 42.11, 42.15, 42.17, 37.4 |
|
|
712
712
|
| `App[Symbol.asyncDispose]` | `() => Promise<void>` | `App.stop`, under the name `await using` calls. | 42.14 |
|
package/docs/RULEBOOK.md
CHANGED
|
@@ -413,6 +413,12 @@ the value of that write standing in its place.
|
|
|
413
413
|
|
|
414
414
|
8.19 A `Store`'s `get` answering a miss with nothing left to serve MUST land an error on `s.error`.
|
|
415
415
|
|
|
416
|
+
8.20 A `Store`'s `watch` MUST be called once, where the `Reactive` is constructed, and each call of
|
|
417
|
+
the function it is handed MUST run `get` again as a restore, per 8.18, and MUST NOT run the body.
|
|
418
|
+
|
|
419
|
+
8.21 What a `Store`'s `watch` returns MUST be called once the `Reactive` is released: a `Keyed`
|
|
420
|
+
entry evicted, or the setup or request that built it, or its `Keyed`, ended.
|
|
421
|
+
|
|
416
422
|
# 9. Streams and wired values
|
|
417
423
|
|
|
418
424
|
9.1 A streaming producer's `Stored` MUST be the chunks accumulated so far, materialized on the
|
|
@@ -641,6 +647,9 @@ start again when the load ends.
|
|
|
641
647
|
11.71 An unkeyed `reactive` with a `store`, built on a server outside every request, MUST NOT run
|
|
642
648
|
the `store`'s `get` before it is reached.
|
|
643
649
|
|
|
650
|
+
11.72 A `Keyed` MUST NOT hold an entry for an `Args` its `args` gate refuses, and the gate MUST run
|
|
651
|
+
once per entry.
|
|
652
|
+
|
|
644
653
|
# 12. Effects
|
|
645
654
|
|
|
646
655
|
12.1 A `Disposer` MUST run before each rerun of its `Effect`, and once more at teardown.
|
|
@@ -1725,8 +1734,8 @@ the page shown, and MUST NOT where only its query or fragment does.
|
|
|
1725
1734
|
30.25 A `navigate` that does not remount the page MUST NOT move the scroll, except to the element a
|
|
1726
1735
|
fragment it carries names.
|
|
1727
1736
|
|
|
1728
|
-
30.26 `abide check` MUST refuse a `url` literal that no page route of the app (23.3) is filed under
|
|
1729
|
-
|
|
1737
|
+
30.26 `abide check` MUST refuse a `url` literal that no page route of the app (23.3) is filed under,
|
|
1738
|
+
no public file of the app is served at, and that is none of the four addresses 21.4 names.
|
|
1730
1739
|
|
|
1731
1740
|
30.27 A `navigate` whose request fails, or whose route's chunks fail to load, MUST leave the address
|
|
1732
1741
|
to the document, save where 30.32 holds.
|
|
@@ -2001,7 +2010,9 @@ reading layout, querying its descendants or calling `focus()` acts on a connecte
|
|
|
2001
2010
|
32.69 `bind:element` on a component MUST be a build error.
|
|
2002
2011
|
|
|
2003
2012
|
32.70 Markup the client parses for a slot holding a value branded by `raw`, a `{#if}` arm or a
|
|
2004
|
-
`{#for}` row MUST be parsed as the document's parser parses it inside the element it goes into
|
|
2013
|
+
`{#for}` row MUST be parsed as the document's parser parses it inside the element it goes into, and
|
|
2014
|
+
an element the client builds — a component's own, or children a caller hands one — MUST take the
|
|
2015
|
+
namespace the document's parser gives it there, as MUST an attribute the client writes on it.
|
|
2005
2016
|
|
|
2006
2017
|
32.71 A `class` or `style` attribute beside a `class:name` or `style:prop` MUST keep moving (32.3)
|
|
2007
2018
|
and MUST write only its own classes or declarations; a class either of them holds MUST stay on the
|
|
@@ -2672,6 +2683,10 @@ it, MUST hydrate it there with that component's browser module, and MUST run eve
|
|
|
2672
2683
|
hydration woke before it settles; it MUST throw where the component was not imported from a
|
|
2673
2684
|
`.abide` and bound, or where the process has no DOM.
|
|
2674
2685
|
|
|
2686
|
+
38.120 Under `bun test` with `@abide/abide/test` preloaded, an app that declares no
|
|
2687
|
+
`ABIDE_PRINCIPAL_SECRET` MUST seal and verify the principal cookie with a secret the preload
|
|
2688
|
+
supplies, and `abide start` MUST NOT supply that secret.
|
|
2689
|
+
|
|
2675
2690
|
# 39. Head and view transitions
|
|
2676
2691
|
|
|
2677
2692
|
39.1 A `<head>` element in a `.abide` file MUST contribute its children to the document head.
|
|
@@ -2990,6 +3005,10 @@ connection still answering, and MUST then run `onStop` without waiting on what i
|
|
|
2990
3005
|
|
|
2991
3006
|
42.18 `ABIDE_STOP_TIMEOUT` MUST default to 5000.
|
|
2992
3007
|
|
|
3008
|
+
42.19 A `principal` on `App.run` MUST be the claims its scope's `principal` resolves from, through
|
|
3009
|
+
`onPrincipal` as a sealed cookie's are, MUST NOT write the principal cookie, and MUST give the scope
|
|
3010
|
+
a request where `request` is not given.
|
|
3011
|
+
|
|
2993
3012
|
# 43. The measurement harness
|
|
2994
3013
|
|
|
2995
3014
|
43.1 `@abide/harness/measure`, `@abide/harness/engine` and `@abide/harness/server` MUST each be
|
|
@@ -3172,6 +3191,14 @@ and naming it `unfinished` where it ended without a `done`.
|
|
|
3172
3191
|
44.32 A command `claudeCode.bridgeCommand` or `claudeCode.bridge` mints with `tools` MUST grant
|
|
3173
3192
|
those built-in tools after its `--` as `--tools`, and one minted without `tools` MUST carry no `--`.
|
|
3174
3193
|
|
|
3194
|
+
44.33 A pairing MUST be to the claims the principal was sealed with (26.11), or to none, where its
|
|
3195
|
+
code was minted, and `claudeCode` and `claudeCode.bridge` MUST read it as absent to a request from
|
|
3196
|
+
that browser carrying any other.
|
|
3197
|
+
|
|
3198
|
+
44.34 A `claudeCode.bridge` stream MUST yield the pairing of whoever a `principal.set` or a
|
|
3199
|
+
`principal.clear` made at its browser since it opened signs in, at the caller 26.9 rotates it to,
|
|
3200
|
+
and the command it yields MUST pair that one.
|
|
3201
|
+
|
|
3175
3202
|
# 45. Offline
|
|
3176
3203
|
|
|
3177
3204
|
45.1 Where `ABIDE_OFFLINE` is on, a page `abide build` built MUST register the worker
|
package/docs/abide.md
CHANGED
|
@@ -2382,6 +2382,8 @@ test('the view picked is the view the next visit opens on', async () => {
|
|
|
2382
2382
|
|
|
2383
2383
|
A `Store` is a `get` and a `set`. The value reads the store when it is built and falls back to `'board'` on a miss, and every change is written back. The reads and writes in the markup do not change.
|
|
2384
2384
|
|
|
2385
|
+
Where the stored value can change from outside the app, such as a file another program edits, the store adds `watch`. It is handed a function to call when that happens, and returns the function that stops watching. The value reads `get` again, and the body does not run.
|
|
2386
|
+
|
|
2385
2387
|
This store goes through two handlers, so the server and the browser read the same preference and the first paint is already right. A store over `localStorage` works too, but only in a browser, so the server paints the fallback first. After a write, `set` invalidates the read it made, so the next read asks the server rather than this tab's copy.
|
|
2386
2388
|
|
|
2387
2389
|
## Next
|
|
@@ -3718,7 +3720,6 @@ export default POST(() => principal.clear(), { description: 'Signs out.' })
|
|
|
3718
3720
|
import { expect, test } from 'bun:test'
|
|
3719
3721
|
import { principal } from '@abide/abide'
|
|
3720
3722
|
import { createApp } from '@abide/abide/server'
|
|
3721
|
-
import { asMember } from './signedIn.ts'
|
|
3722
3723
|
|
|
3723
3724
|
const signIn = (email: string) =>
|
|
3724
3725
|
new Request('http://crm.invalid/account/signIn', {
|
|
@@ -3727,17 +3728,28 @@ const signIn = (email: string) =>
|
|
|
3727
3728
|
body: JSON.stringify({ email }),
|
|
3728
3729
|
})
|
|
3729
3730
|
|
|
3730
|
-
test('signing in seals the
|
|
3731
|
+
test('signing in seals the id into an HttpOnly cookie', async () => {
|
|
3731
3732
|
await using app = await createApp()
|
|
3732
3733
|
const answer = await app.fetch(signIn('maya@crm.example'))
|
|
3733
3734
|
const [sealed = ''] = answer.headers.getSetCookie()
|
|
3734
3735
|
expect(sealed).toStartWith('abide-principal=')
|
|
3735
3736
|
expect(sealed).toContain('HttpOnly')
|
|
3737
|
+
|
|
3738
|
+
// The seal is a JWT; its payload is the id, and the times abide adds.
|
|
3739
|
+
const token = sealed.split(';')[0]?.split('=')[1] ?? ''
|
|
3740
|
+
const payload = JSON.parse(
|
|
3741
|
+
Buffer.from(token.split('.')[1] ?? '', 'base64url').toString(),
|
|
3742
|
+
)
|
|
3743
|
+
expect(Object.keys(payload).sort()).toEqual(['exp', 'iat', 'id'])
|
|
3736
3744
|
})
|
|
3737
3745
|
|
|
3738
3746
|
test('a request carrying the cookie is signed in', async () => {
|
|
3739
3747
|
await using app = await createApp()
|
|
3740
|
-
const
|
|
3748
|
+
const answer = await app.fetch(signIn('maya@crm.example'))
|
|
3749
|
+
const [sealed = ''] = answer.headers.getSetCookie()
|
|
3750
|
+
const request = new Request('http://crm.invalid/', {
|
|
3751
|
+
headers: { cookie: sealed.split(';')[0] ?? '' },
|
|
3752
|
+
})
|
|
3741
3753
|
const signedIn = await app.run(async () => await principal.authenticated, {
|
|
3742
3754
|
request,
|
|
3743
3755
|
})
|
|
@@ -3749,7 +3761,7 @@ test('a request carrying the cookie is signed in', async () => {
|
|
|
3749
3761
|
|
|
3750
3762
|
The answer also tells the browser the caller changed. Every tab of the site reads `principal` again, so the form's own button flips with nothing passed to it.
|
|
3751
3763
|
|
|
3752
|
-
The seal is a JWT signed with `ABIDE_PRINCIPAL_SECRET`, and it lives for `ABIDE_PRINCIPAL_TTL` milliseconds, a week unless you set it. The chapter app keeps a development secret in its `.env`, so set your own wherever the app is deployed. A comma-separated list rotates a secret in: the first signs, and every one verifies.
|
|
3764
|
+
The seal is a JWT signed with `ABIDE_PRINCIPAL_SECRET`, and it lives for `ABIDE_PRINCIPAL_TTL` milliseconds, a week unless you set it. The chapter app keeps a development secret in its `.env`, so set your own wherever the app is deployed. A comma-separated list rotates a secret in: the first signs, and every one verifies. Under `bun test` an app that sets no secret is given one, so a test signs in from a fresh clone.
|
|
3753
3765
|
|
|
3754
3766
|
| Read | What it is |
|
|
3755
3767
|
| --- | --- |
|
|
@@ -3808,21 +3820,11 @@ onPrincipal((claims) => {
|
|
|
3808
3820
|
import { expect, test } from 'bun:test'
|
|
3809
3821
|
import { principal } from '@abide/abide'
|
|
3810
3822
|
import { createApp } from '@abide/abide/server'
|
|
3811
|
-
import { asMember } from './signedIn.ts'
|
|
3812
3823
|
|
|
3813
|
-
test('the
|
|
3824
|
+
test('the claims resolve to the member they name', async () => {
|
|
3814
3825
|
await using app = await createApp()
|
|
3815
|
-
const request = await asMember(app, 'theo@crm.example')
|
|
3816
|
-
|
|
3817
|
-
// The seal is a JWT; its payload is the id, and the times abide adds.
|
|
3818
|
-
const sealed = request.headers.get('cookie') ?? ''
|
|
3819
|
-
const payload = JSON.parse(
|
|
3820
|
-
Buffer.from(sealed.split('.')[1] ?? '', 'base64url').toString(),
|
|
3821
|
-
)
|
|
3822
|
-
expect(Object.keys(payload).sort()).toEqual(['exp', 'iat', 'id'])
|
|
3823
|
-
|
|
3824
3826
|
const member = await app.run(async () => await principal.resolved, {
|
|
3825
|
-
|
|
3827
|
+
principal: { id: 'theo' },
|
|
3826
3828
|
})
|
|
3827
3829
|
expect(member.name).toBe('Theo Park')
|
|
3828
3830
|
expect(member.role).toBe('sales')
|
|
@@ -3831,6 +3833,8 @@ test('the cookie holds an id, and the request reads the member', async () => {
|
|
|
3831
3833
|
|
|
3832
3834
|
`onPrincipal` is handed the claims and returns your half of the caller. It runs on the server, at most once per request, where the team records are. So the cookie stays small, and a rename shows on the next request rather than at the next sign-in. Filed in any handler file or `.abide`, it resolves the caller for every route.
|
|
3833
3835
|
|
|
3836
|
+
A test signs in without the cookie. `app.run(fn, { principal: { id: 'theo' } })` hands those claims to `onPrincipal` as a sealed cookie's are, and the card's test reads the member they name.
|
|
3837
|
+
|
|
3834
3838
|
What it returns is read as `principal.resolved`, typed by `Principal`. Declare your fields on that interface, as the app's `lib/TEAM.server.ts` does beside the team it reads. abide's own three fields are `authenticated`, `expiresAt` and `error`, and they win any collision, so a resolver cannot sign a caller in by returning `authenticated: true`. A resolver that throws fills `error` and leaves the caller signed out.
|
|
3835
3839
|
|
|
3836
3840
|
The tabs on this card, the deal-value card and the export card are `lib/SignInAs.abide`, which calls the same `signIn` handler.
|
|
@@ -3934,19 +3938,20 @@ export default GET(
|
|
|
3934
3938
|
import { expect, test } from 'bun:test'
|
|
3935
3939
|
import { createApp } from '@abide/abide/server'
|
|
3936
3940
|
import getContact from '../getContact.ts'
|
|
3937
|
-
import { asMember } from './signedIn.ts'
|
|
3938
3941
|
|
|
3939
3942
|
test('an admin reads the deal value', async () => {
|
|
3940
3943
|
await using app = await createApp()
|
|
3941
|
-
const
|
|
3942
|
-
|
|
3944
|
+
const contact = await app.run(async () => await getContact(), {
|
|
3945
|
+
principal: { id: 'maya' },
|
|
3946
|
+
})
|
|
3943
3947
|
expect(contact.dealValue).toBe('$48,000')
|
|
3944
3948
|
})
|
|
3945
3949
|
|
|
3946
3950
|
test('sales reads the rest of the contact', async () => {
|
|
3947
3951
|
await using app = await createApp()
|
|
3948
|
-
const
|
|
3949
|
-
|
|
3952
|
+
const contact = await app.run(async () => await getContact(), {
|
|
3953
|
+
principal: { id: 'theo' },
|
|
3954
|
+
})
|
|
3950
3955
|
expect(contact.name).toBe('Grace Hopper')
|
|
3951
3956
|
expect(contact.dealValue).toBeUndefined()
|
|
3952
3957
|
})
|
|
@@ -4009,23 +4014,24 @@ export default POST(async () => CONTACTS, {
|
|
|
4009
4014
|
import { expect, test } from 'bun:test'
|
|
4010
4015
|
import { createApp } from '@abide/abide/server'
|
|
4011
4016
|
import exportContacts from '../exportContacts.ts'
|
|
4012
|
-
import { asMember } from './signedIn.ts'
|
|
4013
4017
|
|
|
4014
4018
|
test('an admin exports every contact', async () => {
|
|
4015
4019
|
await using app = await createApp()
|
|
4016
|
-
const request = await asMember(app, 'maya@crm.example')
|
|
4017
4020
|
const contacts = await app.run(async () => await exportContacts(), {
|
|
4018
|
-
|
|
4021
|
+
principal: { id: 'maya' },
|
|
4019
4022
|
})
|
|
4020
4023
|
expect(contacts).toHaveLength(3)
|
|
4021
4024
|
})
|
|
4022
4025
|
|
|
4023
4026
|
test('sales and a signed-out caller are refused 403', async () => {
|
|
4024
4027
|
await using app = await createApp()
|
|
4025
|
-
|
|
4026
|
-
|
|
4028
|
+
const callers = [
|
|
4029
|
+
{ principal: { id: 'theo' } },
|
|
4030
|
+
{ request: new Request('http://crm.invalid/') },
|
|
4031
|
+
]
|
|
4032
|
+
for (const caller of callers) {
|
|
4027
4033
|
try {
|
|
4028
|
-
await app.run(async () => await exportContacts(),
|
|
4034
|
+
await app.run(async () => await exportContacts(), caller)
|
|
4029
4035
|
throw new Error('the export was not refused')
|
|
4030
4036
|
} catch (refusal) {
|
|
4031
4037
|
if (!exportContacts.isError(refusal, 'HttpError')) throw refusal
|
|
@@ -4961,13 +4967,13 @@ A handler file already carries an address, a method, an argument type and a desc
|
|
|
4961
4967
|
|
|
4962
4968
|
```abide src/assistant/lib/OpenApiOperation.abide
|
|
4963
4969
|
<script>
|
|
4964
|
-
import { reactive,
|
|
4970
|
+
import { reactive, url } from '@abide/abide'
|
|
4965
4971
|
|
|
4966
4972
|
type Operation = { description: string; parameters: object[] }
|
|
4967
4973
|
let operation = reactive('')
|
|
4968
4974
|
|
|
4969
4975
|
async function readDocument() {
|
|
4970
|
-
const answer = await fetch(url('/__abide/openapi.json'
|
|
4976
|
+
const answer = await fetch(url('/__abide/openapi.json'))
|
|
4971
4977
|
const document: { paths: Record<string, { get: Operation }> } =
|
|
4972
4978
|
await answer.json()
|
|
4973
4979
|
for (const [path, { get }] of Object.entries(document.paths))
|
|
@@ -5046,7 +5052,7 @@ abide openapi > openapi.json
|
|
|
5046
5052
|
|
|
5047
5053
|
```abide src/assistant/lib/ModelToolCall.abide
|
|
5048
5054
|
<script>
|
|
5049
|
-
import { reactive,
|
|
5055
|
+
import { reactive, url } from '@abide/abide'
|
|
5050
5056
|
|
|
5051
5057
|
let tool = reactive('—')
|
|
5052
5058
|
let description = reactive('—')
|
|
@@ -5054,7 +5060,7 @@ let answer = reactive('—')
|
|
|
5054
5060
|
|
|
5055
5061
|
// One JSON-RPC message to the app's MCP endpoint, as a model's client sends it.
|
|
5056
5062
|
async function ask(method: string, params: object) {
|
|
5057
|
-
const reply = await fetch(url('/__abide/mcp'
|
|
5063
|
+
const reply = await fetch(url('/__abide/mcp'), {
|
|
5058
5064
|
method: 'POST',
|
|
5059
5065
|
headers: { 'content-type': 'application/json' },
|
|
5060
5066
|
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method, params }),
|
|
@@ -5147,14 +5153,14 @@ abide mcp --url https://crm.example
|
|
|
5147
5153
|
|
|
5148
5154
|
```abide src/assistant/lib/WithheldExport.abide
|
|
5149
5155
|
<script>
|
|
5150
|
-
import { reactive,
|
|
5156
|
+
import { reactive, url } from '@abide/abide'
|
|
5151
5157
|
import exportContacts from '../exportContacts.ts'
|
|
5152
5158
|
|
|
5153
5159
|
let listed = reactive('—')
|
|
5154
5160
|
let answers = reactive('—')
|
|
5155
5161
|
|
|
5156
5162
|
async function check() {
|
|
5157
|
-
const reply = await fetch(url('/__abide/mcp'
|
|
5163
|
+
const reply = await fetch(url('/__abide/mcp'), {
|
|
5158
5164
|
method: 'POST',
|
|
5159
5165
|
headers: { 'content-type': 'application/json' },
|
|
5160
5166
|
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'tools/list' }),
|
|
@@ -5638,6 +5644,8 @@ Pass the same `tools` to `claudeCode.bridge` or `claudeCode.bridgeCommand` and t
|
|
|
5638
5644
|
|
|
5639
5645
|
The visitor pairs once per visit by running a command, which dials out to the site. Asking with nothing paired is refused 409.
|
|
5640
5646
|
|
|
5647
|
+
The tools the conversation calls run as the visitor, on the sign-in their ask carried, so a tool reads `principal` as any handler does. The pairing belongs to that sign-in too. If someone else signs in at the same browser, their asks are refused 409 and the status offers them a command of their own, so nobody's turn runs on another member's Claude Code.
|
|
5648
|
+
|
|
5641
5649
|
| Name | What it does for the CRM |
|
|
5642
5650
|
| --- | --- |
|
|
5643
5651
|
| `claudeCode.bridge` | Streams this browser's pairing: unpaired with the command to run, then paired. |
|
|
@@ -5743,7 +5751,7 @@ test('the summary answers in process as it does on the wire', async () => {
|
|
|
5743
5751
|
| --- | --- |
|
|
5744
5752
|
| `App` | what `createApp` hands back, to a test, a script or a host |
|
|
5745
5753
|
| `App.fetch` | one request, through the whole app, in this process |
|
|
5746
|
-
| `App.run` | runs a function inside one request's scope, so two reads of one key load once |
|
|
5754
|
+
| `App.run` | runs a function inside one request's scope, so two reads of one key load once, signed in as the `principal` claims you give it |
|
|
5747
5755
|
| `App.listen` | binds the socket, for a test that needs a real `WebSocket` |
|
|
5748
5756
|
| `App.stop` | drains what is in flight, then runs `onStop` |
|
|
5749
5757
|
| `App[Symbol.asyncDispose]` | `stop`, under the name `await using` calls |
|
|
@@ -6827,7 +6835,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6827
6835
|
|
|
6828
6836
|
| Name | Signature | Meaning | Rules |
|
|
6829
6837
|
| --- | --- | --- | --- |
|
|
6830
|
-
| `Store` | `{ get: () => Stored \| undefined \| Promise<Stored \| undefined>; set: (value: Stored, retention: { ttl: number }) => void \| Promise<void
|
|
6838
|
+
| `Store` | `{ get: () => Stored \| undefined \| Promise<Stored \| undefined>; set: (value: Stored, retention: { ttl: number }) => void \| Promise<void>; watch?: (changed: () => void) => () => void }` | Where a value lives when this process does not. | [8.1](/rulebook#8.1), [8.2](/rulebook#8.2), [8.3](/rulebook#8.3), [8.4](/rulebook#8.4), [8.5](/rulebook#8.5), [8.6](/rulebook#8.6), [8.7](/rulebook#8.7), [8.8](/rulebook#8.8), [8.9](/rulebook#8.9), [8.10](/rulebook#8.10), [8.11](/rulebook#8.11), [8.12](/rulebook#8.12), [8.13](/rulebook#8.13), [8.14](/rulebook#8.14), [8.15](/rulebook#8.15), [8.16](/rulebook#8.16), [8.17](/rulebook#8.17), [8.18](/rulebook#8.18), [8.19](/rulebook#8.19), [8.20](/rulebook#8.20), [8.21](/rulebook#8.21) |
|
|
6831
6839
|
|
|
6832
6840
|
## Taught in
|
|
6833
6841
|
|
|
@@ -6869,7 +6877,7 @@ Arrive here knowing which name you want. Each page is read off the registry: the
|
|
|
6869
6877
|
| `identity` | `(next: Stored, previous: Stored) => unknown` | What makes it the same value. | [5.1](/rulebook#5.1), [5.2](/rulebook#5.2), [5.3](/rulebook#5.3), [5.4](/rulebook#5.4), [5.5](/rulebook#5.5), [9.3](/rulebook#9.3), [11.17](/rulebook#11.17), [18.4](/rulebook#18.4) |
|
|
6870
6878
|
| `by` | `(keyof Element & string) \| ((element: Element) => unknown)`, over the `Element` a `Stored` array holds | What tells one element of the value from another. | [5.22](/rulebook#5.22), [18.4](/rulebook#18.4), [18.15](/rulebook#18.15), [32.32](/rulebook#32.32) |
|
|
6871
6879
|
| `ttl` | `number` | The life of a production, or of a keyed entry. | [6.1](/rulebook#6.1), [6.2](/rulebook#6.2), [6.3](/rulebook#6.3), [6.4](/rulebook#6.4), [6.5](/rulebook#6.5), [6.6](/rulebook#6.6), [6.7](/rulebook#6.7), [6.8](/rulebook#6.8), [6.9](/rulebook#6.9), [6.10](/rulebook#6.10), [11.6](/rulebook#11.6), [11.35](/rulebook#11.35), [11.45](/rulebook#11.45), [11.48](/rulebook#11.48), [11.49](/rulebook#11.49), [11.69](/rulebook#11.69) |
|
|
6872
|
-
| `args` | `Schema<Args>` | The gate on a keyed value's args. | [9.4](/rulebook#9.4), [11.28](/rulebook#11.28), [11.30](/rulebook#11.30), [11.40](/rulebook#11.40), [11.41](/rulebook#11.41), [11.56](/rulebook#11.56), [11.60](/rulebook#11.60), [11.68](/rulebook#11.68) |
|
|
6880
|
+
| `args` | `Schema<Args>` | The gate on a keyed value's args. | [9.4](/rulebook#9.4), [11.28](/rulebook#11.28), [11.30](/rulebook#11.30), [11.40](/rulebook#11.40), [11.41](/rulebook#11.41), [11.56](/rulebook#11.56), [11.60](/rulebook#11.60), [11.68](/rulebook#11.68), [11.72](/rulebook#11.72) |
|
|
6873
6881
|
| `global` | `boolean` | Opts out of the default scope into one cache shared by every caller in the process. | [11.34](/rulebook#11.34), [11.35](/rulebook#11.35), [11.36](/rulebook#11.36), [11.37](/rulebook#11.37), [11.38](/rulebook#11.38), [11.39](/rulebook#11.39), [11.53](/rulebook#11.53), [11.67](/rulebook#11.67) |
|
|
6874
6882
|
| `tags` | `string[] \| ((args: Args) => string[])` | What a `Selection` matches on. | [11.43](/rulebook#11.43), [11.44](/rulebook#11.44), [18.4](/rulebook#18.4), [18.15](/rulebook#18.15) |
|
|
6875
6883
|
| `throttle` | `number` | A ceiling on how often the value changes. | [5.8](/rulebook#5.8), [5.9](/rulebook#5.9), [5.11](/rulebook#5.11), [5.12](/rulebook#5.12), [5.14](/rulebook#5.14), [5.15](/rulebook#5.15), [5.17](/rulebook#5.17), [5.18](/rulebook#5.18), [5.19](/rulebook#5.19), [5.20](/rulebook#5.20), [5.21](/rulebook#5.21), [5.23](/rulebook#5.23), [5.24](/rulebook#5.24) |
|
|
@@ -7854,9 +7862,9 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
7854
7862
|
|
|
7855
7863
|
| Name | Signature | Meaning | Rules |
|
|
7856
7864
|
| --- | --- | --- | --- |
|
|
7857
|
-
| `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) |
|
|
7858
|
-
| `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; expires: number }` | The command that pairs a bridge with this browser, granting the built-in tools it is given. | [44.14](/rulebook#44.14), [44.32](/rulebook#44.32) |
|
|
7859
|
-
| `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) |
|
|
7865
|
+
| `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) |
|
|
7866
|
+
| `claudeCode.bridgeCommand` | `(options?: { tools?: string[] }) => { command: string; expires: number }` | The command that pairs 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) |
|
|
7867
|
+
| `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) |
|
|
7860
7868
|
| `/__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) |
|
|
7861
7869
|
|
|
7862
7870
|
## Taught in
|
|
@@ -8628,7 +8636,7 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8628
8636
|
| `{expr}` | Reactive text, escaped. | [32.1](/rulebook#32.1), [32.23](/rulebook#32.23) |
|
|
8629
8637
|
| formatting whitespace | What a run of whitespace renders as. | [33.17](/rulebook#33.17), [33.18](/rulebook#33.18) |
|
|
8630
8638
|
| `{raw(...)}` | Raw HTML. | [32.2](/rulebook#32.2), [32.25](/rulebook#32.25), [32.26](/rulebook#32.26), [32.30](/rulebook#32.30), [32.31](/rulebook#32.31), [32.70](/rulebook#32.70) |
|
|
8631
|
-
| `name={expr}` | A reactive attribute or property. | [32.3](/rulebook#32.3), [32.20](/rulebook#32.20), [32.23](/rulebook#32.23), [32.37](/rulebook#32.37), [32.71](/rulebook#32.71) |
|
|
8639
|
+
| `name={expr}` | A reactive attribute or property. | [32.3](/rulebook#32.3), [32.20](/rulebook#32.20), [32.23](/rulebook#32.23), [32.37](/rulebook#32.37), [32.70](/rulebook#32.70), [32.71](/rulebook#32.71) |
|
|
8632
8640
|
| `on<event>={fn}` | A native listener on an element, and an ordinary prop on a component. | [32.4](/rulebook#32.4) |
|
|
8633
8641
|
| `name="…{expr}…"` | An interpolated quoted value. | [32.3](/rulebook#32.3), [32.71](/rulebook#32.71) |
|
|
8634
8642
|
| `bind:value` | Two-way bind on an element. | [5.16](/rulebook#5.16), [32.5](/rulebook#32.5), [32.58](/rulebook#32.58), [32.59](/rulebook#32.59), [32.60](/rulebook#32.60), [32.61](/rulebook#32.61), [32.62](/rulebook#32.62), [32.72](/rulebook#32.72) |
|
|
@@ -8656,10 +8664,10 @@ Generated surfaces: what is written in place rather than imported.
|
|
|
8656
8664
|
| Spelling | Meaning | Rules |
|
|
8657
8665
|
| --- | --- | --- |
|
|
8658
8666
|
| `{#component Name(pattern)}` | An inline component. | [32.39](/rulebook#32.39), [32.40](/rulebook#32.40), [32.41](/rulebook#32.41), [32.42](/rulebook#32.42), [32.43](/rulebook#32.43), [32.44](/rulebook#32.44), [32.45](/rulebook#32.45), [32.46](/rulebook#32.46), [32.47](/rulebook#32.47), [32.48](/rulebook#32.48), [32.49](/rulebook#32.49), [32.50](/rulebook#32.50), [32.51](/rulebook#32.51), [32.52](/rulebook#32.52), [32.53](/rulebook#32.53), [32.54](/rulebook#32.54), [32.55](/rulebook#32.55), [32.63](/rulebook#32.63), [32.64](/rulebook#32.64) |
|
|
8659
|
-
| `<Name/>` | A component invocation. | [32.17](/rulebook#32.17), [32.34](/rulebook#32.34), [32.35](/rulebook#32.35), [32.36](/rulebook#32.36), [33.19](/rulebook#33.19), [33.21](/rulebook#33.21), [41.24](/rulebook#41.24), [41.28](/rulebook#41.28) |
|
|
8667
|
+
| `<Name/>` | A component invocation. | [32.17](/rulebook#32.17), [32.34](/rulebook#32.34), [32.35](/rulebook#32.35), [32.36](/rulebook#32.36), [32.70](/rulebook#32.70), [33.19](/rulebook#33.19), [33.21](/rulebook#33.21), [41.24](/rulebook#41.24), [41.28](/rulebook#41.28) |
|
|
8660
8668
|
| `<slot/>` | The position children render into. | [32.18](/rulebook#32.18) |
|
|
8661
8669
|
| `<slot>fallback</slot>` | Renders children, or a fallback. | [32.18](/rulebook#32.18) |
|
|
8662
|
-
| `<Tag>…</Tag>` | Children passed to a component's slot. | [32.18](/rulebook#32.18) |
|
|
8670
|
+
| `<Tag>…</Tag>` | Children passed to a component's slot. | [32.18](/rulebook#32.18), [32.70](/rulebook#32.70) |
|
|
8663
8671
|
|
|
8664
8672
|
## Script / style blocks
|
|
8665
8673
|
|
|
@@ -8837,7 +8845,7 @@ Configuration: what is written in place rather than imported.
|
|
|
8837
8845
|
| `APP_NAME` | `string` | The app's name, and `log`'s default namespace. | [27.3](/rulebook#27.3), [28.4](/rulebook#28.4) |
|
|
8838
8846
|
| `APP_VERSION` | `string` | The version beside that name. | [27.3](/rulebook#27.3) |
|
|
8839
8847
|
| `APP_DATA_DIR` | `string` | The platform's per-user data directory. | [27.3](/rulebook#27.3), [36.4](/rulebook#36.4) |
|
|
8840
|
-
| `ABIDE_PRINCIPAL_SECRET` | `string \| null` | What seals the principal cookie. | [26.11](/rulebook#26.11), [36.5](/rulebook#36.5) |
|
|
8848
|
+
| `ABIDE_PRINCIPAL_SECRET` | `string \| null` | What seals the principal cookie. | [26.11](/rulebook#26.11), [36.5](/rulebook#36.5), [38.120](/rulebook#38.120) |
|
|
8841
8849
|
| `ABIDE_PRINCIPAL_TTL` | `number` | The principal cookie's life, in ms. | [26.15](/rulebook#26.15), [26.16](/rulebook#26.16) |
|
|
8842
8850
|
| `ABIDE_APP_TOKEN` | `string \| null` | The bearer the remote CLI sends. | [36.6](/rulebook#36.6) |
|
|
8843
8851
|
| `ABIDE_APP_URL` | `string \| null` | Names an app somewhere else, for the remote CLI. | [36.6](/rulebook#36.6) |
|
|
@@ -9177,7 +9185,7 @@ Configuration: what is written in place rather than imported.
|
|
|
9177
9185
|
| --- | --- | --- | --- |
|
|
9178
9186
|
| `App` | `interface App` | What a host, a test or a script holds the app as. | [42.12](/rulebook#42.12), [42.13](/rulebook#42.13) |
|
|
9179
9187
|
| `App.fetch` | `(request: Request) => Promise<Response>` | The whole request path, entered in process. | [42.5](/rulebook#42.5), [42.6](/rulebook#42.6) |
|
|
9180
|
-
| `App.run` | `<Result>(fn: () => Result \| Promise<Result>, options?: { request?: Request }) => Promise<Result>` | One scope around a function, and nothing else. | [42.7](/rulebook#42.7), [42.8](/rulebook#42.8), [11.38](/rulebook#11.38) |
|
|
9188
|
+
| `App.run` | `<Result>(fn: () => Result \| Promise<Result>, options?: { request?: Request; principal?: Record<string, unknown> }) => Promise<Result>` | One scope around a function, and nothing else. | [42.7](/rulebook#42.7), [42.8](/rulebook#42.8), [42.19](/rulebook#42.19), [11.38](/rulebook#11.38) |
|
|
9181
9189
|
| `App.listen` | `(options?: { port?: number; hostname?: string; unix?: string }) => Promise<void>` | The socket, bound. | [42.9](/rulebook#42.9), [16.23](/rulebook#16.23), [16.56](/rulebook#16.56), [18.28](/rulebook#18.28), [18.29](/rulebook#18.29), [18.30](/rulebook#18.30) |
|
|
9182
9190
|
| `App.stop` | `() => Promise<void>` | The drain and the teardown. | [42.10](/rulebook#42.10), [42.11](/rulebook#42.11), [42.15](/rulebook#42.15), [42.17](/rulebook#42.17), [37.4](/rulebook#37.4) |
|
|
9183
9191
|
| `App[Symbol.asyncDispose]` | `() => Promise<void>` | `App.stop`, under the name `await using` calls. | [42.14](/rulebook#42.14) |
|
package/package.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import { relative as relativeTo } from 'node:path' // bun exposes no path helpers of its own
|
|
2
2
|
import { appFiles } from '#server/appTree.ts'
|
|
3
3
|
import { type Placement, placement, routeOf } from '#server/placement.ts'
|
|
4
|
+
import {
|
|
5
|
+
HEALTH_ADDRESS,
|
|
6
|
+
MCP_ADDRESS,
|
|
7
|
+
OPENAPI_ADDRESS,
|
|
8
|
+
PRINCIPAL_ADDRESS,
|
|
9
|
+
} from '#shared/ADDRESSES.ts'
|
|
4
10
|
import { LAUNCHER_PAGE } from '#shared/LAUNCHER_FILES.ts'
|
|
5
11
|
import {
|
|
6
12
|
coversRoute,
|
|
@@ -76,7 +82,7 @@ export type AppLayout = {
|
|
|
76
82
|
errors: AppRoute[]
|
|
77
83
|
/** Every public file's source path to its address. 36.11. */
|
|
78
84
|
published: Map<string, string>
|
|
79
|
-
/** 30.26 — every address a link may name: a page's,
|
|
85
|
+
/** 30.26 — every address a link may name: a page's, a public file's, and 21.4's four. */
|
|
80
86
|
addresses: string[]
|
|
81
87
|
/** What the tree refuses, each at its file; a build stops naming every one. */
|
|
82
88
|
refused: Diagnosis[]
|
|
@@ -150,6 +156,7 @@ export async function appLayout(root: string): Promise<AppLayout> {
|
|
|
150
156
|
const addresses = new Set<string>()
|
|
151
157
|
for (const route of routes) addresses.add(route.pattern)
|
|
152
158
|
for (const address of published.values()) addresses.add(address)
|
|
159
|
+
for (const address of FETCHED_ADDRESSES) addresses.add(address)
|
|
153
160
|
return {
|
|
154
161
|
root,
|
|
155
162
|
sources,
|
|
@@ -165,6 +172,14 @@ export async function appLayout(root: string): Promise<AppLayout> {
|
|
|
165
172
|
}
|
|
166
173
|
}
|
|
167
174
|
|
|
175
|
+
// 30.26, 21.4 — the framework's own addresses an app fetches, which every app answers at.
|
|
176
|
+
const FETCHED_ADDRESSES = [
|
|
177
|
+
OPENAPI_ADDRESS,
|
|
178
|
+
MCP_ADDRESS,
|
|
179
|
+
HEALTH_ADDRESS,
|
|
180
|
+
PRINCIPAL_ADDRESS,
|
|
181
|
+
]
|
|
182
|
+
|
|
168
183
|
/** The file at each of `paths`, read. The reads overlap. */
|
|
169
184
|
function textsAt(
|
|
170
185
|
paths: readonly string[],
|
|
@@ -532,11 +532,11 @@ export function isSingleRooted(children: TemplateNode[]): boolean {
|
|
|
532
532
|
)
|
|
533
533
|
}
|
|
534
534
|
|
|
535
|
-
// CONTENT
|
|
536
|
-
//
|
|
537
|
-
//
|
|
538
|
-
//
|
|
539
|
-
//
|
|
535
|
+
// CONTENT A ROW THE BROWSER BUILDS LEAVES TO THE PARSER: an `<svg>` or `<math>` that opens its
|
|
536
|
+
// namespace inside the row (a build only carries one in, off the parent it goes into — 32.70), a
|
|
537
|
+
// raw-text element's contents, which are not decoded the way body text is, and a `<template>`'s
|
|
538
|
+
// children, which go into its `content`. A row that touches any of it is left to the parser — the
|
|
539
|
+
// general path, as the fallback it is.
|
|
540
540
|
const PARSER_OWNED = new Set([
|
|
541
541
|
'svg',
|
|
542
542
|
'math',
|