stitchkit 0.74.1 → 0.75.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 (75) hide show
  1. package/dist/agent-runtime/sqlite-purge.d.ts +3 -3
  2. package/dist/agent-runtime/sqlite-purge.d.ts.map +1 -1
  3. package/dist/agent-runtime/sqlite.d.ts +5 -17
  4. package/dist/agent-runtime/sqlite.d.ts.map +1 -1
  5. package/dist/agent-runtime-coding-tools.js +2 -1
  6. package/dist/agent-runtime-harness.js +5 -4
  7. package/dist/agent-runtime-sqlite-bun.d.ts +1 -1
  8. package/dist/agent-runtime-sqlite-bun.d.ts.map +1 -1
  9. package/dist/agent-runtime-sqlite-node.d.ts +1 -1
  10. package/dist/agent-runtime-sqlite-node.d.ts.map +1 -1
  11. package/dist/agent-runtime.js +8 -7
  12. package/dist/application/keyspace-sqlite.d.ts +31 -0
  13. package/dist/application/keyspace-sqlite.d.ts.map +1 -0
  14. package/dist/application/keyspace.d.ts +142 -0
  15. package/dist/application/keyspace.d.ts.map +1 -0
  16. package/dist/application/watch-hub.d.ts +125 -0
  17. package/dist/application/watch-hub.d.ts.map +1 -0
  18. package/dist/application.d.ts +4 -0
  19. package/dist/application.d.ts.map +1 -1
  20. package/dist/application.js +455 -0
  21. package/dist/cli.js +7 -6
  22. package/dist/contract/index.js +2 -1
  23. package/dist/{index-aczggrty.js → index-28jsdwt1.js} +1 -1
  24. package/dist/index-35z5h2ty.js +594 -0
  25. package/dist/{index-f6n5n7nz.js → index-4kp4fxvp.js} +4 -18
  26. package/dist/{index-118v0z41.js → index-52400rdd.js} +8 -3
  27. package/dist/{index-da1aqnhb.js → index-65kathdm.js} +6 -4
  28. package/dist/{index-jqtsc9mj.js → index-6wr9f6qm.js} +2 -2
  29. package/dist/{index-7qy2ex0m.js → index-885chjxq.js} +1 -1
  30. package/dist/{index-k2zczx1g.js → index-9c5dbr5q.js} +1 -1
  31. package/dist/{index-s2rchahr.js → index-9my2n66s.js} +4 -4
  32. package/dist/{index-nemjkxjp.js → index-e0a57ymn.js} +1 -1
  33. package/dist/{index-vbf2p6me.js → index-endzd8sj.js} +1 -1
  34. package/dist/{index-vj3vvpaa.js → index-hsz2mmk0.js} +1 -17
  35. package/dist/{index-s4c8wy8m.js → index-k68e5jz0.js} +2 -2
  36. package/dist/index-rxfy4cq7.js +17 -0
  37. package/dist/{index-58jzmnn4.js → index-v5dm4bwq.js} +1 -1
  38. package/dist/{index-m668wzyc.js → index-v7xjfy10.js} +1 -1
  39. package/dist/index-vdvmcy7r.js +22 -0
  40. package/dist/{index-ezmn6ac6.js → index-xgahrwdf.js} +3 -3
  41. package/dist/index.js +26 -611
  42. package/dist/internal/sqlite.d.ts +27 -0
  43. package/dist/internal/sqlite.d.ts.map +1 -0
  44. package/dist/internal/stable-digest.d.ts +24 -0
  45. package/dist/internal/stable-digest.d.ts.map +1 -0
  46. package/dist/live/events.d.ts +155 -0
  47. package/dist/live/events.d.ts.map +1 -0
  48. package/dist/live/watch-client.d.ts +66 -0
  49. package/dist/live/watch-client.d.ts.map +1 -0
  50. package/dist/live/watch-contract.d.ts +157 -0
  51. package/dist/live/watch-contract.d.ts.map +1 -0
  52. package/dist/live.d.ts +18 -0
  53. package/dist/live.d.ts.map +1 -0
  54. package/dist/live.js +284 -0
  55. package/dist/node.js +5 -4
  56. package/dist/observability/index.js +4 -3
  57. package/dist/primitives.js +2 -1
  58. package/dist/remote.js +3 -2
  59. package/dist/server/create.d.ts.map +1 -1
  60. package/dist/server/event-bus.d.ts +49 -1
  61. package/dist/server/event-bus.d.ts.map +1 -1
  62. package/dist/server/index.d.ts +2 -1
  63. package/dist/server/index.d.ts.map +1 -1
  64. package/dist/server/index.js +224 -12
  65. package/dist/server/middleware/trust-fence.d.ts +116 -0
  66. package/dist/server/middleware/trust-fence.d.ts.map +1 -0
  67. package/dist/server/types.d.ts +16 -1
  68. package/dist/server/types.d.ts.map +1 -1
  69. package/dist/testing.js +3 -2
  70. package/dist/tool-invoker.js +6 -5
  71. package/dist/tools/mcp-round.d.ts.map +1 -1
  72. package/dist/tools.js +15 -30
  73. package/llms-full.txt +426 -4
  74. package/llms.txt +1 -0
  75. package/package.json +6 -2
package/llms-full.txt CHANGED
@@ -48,6 +48,7 @@ own, recorded as an ADR.
48
48
  |--------|--------|----------|-------|
49
49
  | `stitchkit` | browser **and** server | stable | `defineContract`, `createClient`, `createHttpClient`, `createSocketIOClient`, `parseSSE`, the error model |
50
50
  | `stitchkit/contract` | browser **and** server | stable | the contract layer alone — `defineContract`, errors, pagination |
51
+ | `stitchkit/live` | browser **and** server | evolving | `defineEvents` — topic declarations beside the operation contract, projected onto the realtime contract |
51
52
  | `stitchkit/primitives` | browser **and** server | evolving | generic values and declarations for lifecycle, access, audit, delivery and exports |
52
53
  | `stitchkit/server` | server (Bun) | stable | `createServer`, `implement`, hooks, auth, Socket.IO server, server primitives |
53
54
  | `stitchkit/node` | server (Node ≥ 22) | stable | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
@@ -61,7 +62,7 @@ own, recorded as an ADR.
61
62
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
62
63
  | `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
63
64
  | `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
64
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 10 of the 19 minors since 0.56.2, most recently 0.69.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
65
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 20 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
65
66
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
66
67
  | `stitchkit/agent-runtime/coding-tools` | server (Bun or Node) | evolving | bounded host-authorized direct file and shell tools; a root boundary, not an OS sandbox |
67
68
  | `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
@@ -69,7 +70,7 @@ own, recorded as an ADR.
69
70
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
70
71
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
71
72
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
72
- | `stitchkit/application` | server | evolving<br>_redefined in 4 of the 19 minors since 0.56.2, most recently 0.72.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
73
+ | `stitchkit/application` | server | evolving<br>_redefined in 4 of the 20 minors since 0.56.2, most recently 0.72.0_ | managed resource graph, readiness, admission, schedules and bounded shutdown |
73
74
  | `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
74
75
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
75
76
 
@@ -7471,6 +7472,320 @@ gives you the contract and the metadata (`idempotent`, the open `source` tag,
7471
7472
  [ADR 0028](../decisions/0028-revert-contract-dispatcher.md).
7472
7473
 
7473
7474
 
7475
+ ==============================================================================
7476
+ # Guide: Live data (docs/guide/live.md)
7477
+ ==============================================================================
7478
+
7479
+ ---
7480
+ title: Live data — announcements, watched reads, keyspaces and the trust fence
7481
+ description: How to declare events beside the operation contract, share one server-side read between every browser asking the same question, keep authoritative memory in front of a durable backend, and refuse a request whose authority you never agreed to answer on.
7482
+ type: guide
7483
+ status: active
7484
+ created: 2026-09-02
7485
+ updated: 2026-09-02
7486
+ ---
7487
+
7488
+ # Live data
7489
+
7490
+ Four things an application builds by hand the second time it needs them.
7491
+ `stitchkit/live` is **evolving**: it ships from its own entrypoint so that is
7492
+ visible rather than discovered in a minor.
7493
+
7494
+ - [Announcements — `defineEvents`](#announcements)
7495
+ - [Watched reads — `createWatchHub` / `createWatchClient`](#watched-reads)
7496
+ - [Keyspaces — `defineKeyspace`](#keyspaces)
7497
+ - [The trust fence — `createTrustFence`](#the-trust-fence)
7498
+
7499
+ ---
7500
+
7501
+ ## Announcements
7502
+
7503
+ `defineContract` says what a caller may **ask**. `defineEvents` says what the
7504
+ server may **announce**.
7505
+
7506
+ ```ts
7507
+ import { defineEvents } from 'stitchkit/live';
7508
+ import { z } from 'zod';
7509
+
7510
+ export const events = defineEvents(
7511
+ { prefix: 'notes' },
7512
+ {
7513
+ 'changed': { schema: z.object({ folder: z.string() }), mode: 'emit' },
7514
+ 'archiving': {
7515
+ schema: z.object({ folder: z.string() }),
7516
+ mode: 'decision',
7517
+ whenAllDefer: 'allow',
7518
+ listenerTimeoutMs: 2_000,
7519
+ },
7520
+ },
7521
+ );
7522
+ // events.topics has the keys 'notes.changed' and 'notes.archiving'
7523
+ ```
7524
+
7525
+ A topic has exactly one name: the prefixed one. It is the key of `topics`, the
7526
+ event on the wire, and the string you pass to `on`. The short key in the literal
7527
+ is where the full name is *built*, not a second name for the same topic.
7528
+
7529
+ ### The wire is the socket you already run
7530
+
7531
+ `toRealtimeContract` projects the declaration onto `RealtimeContract`. Everything
7532
+ after that is the machinery Stitchkit already ships — validation both ways,
7533
+ rejection reporting, rooms, and the client's `retain:` replay.
7534
+
7535
+ ```ts
7536
+ import { bindRealtimeServer } from 'stitchkit/server';
7537
+ import { bindRealtimeClient } from 'stitchkit';
7538
+ import { toRealtimeContract } from 'stitchkit/live';
7539
+
7540
+ const realtime = bindRealtimeServer(toRealtimeContract(events), { io });
7541
+ realtime.emit('notes.changed', { folder: 'inbox' });
7542
+
7543
+ // browser
7544
+ const live = bindRealtimeClient(toRealtimeContract(events), socket);
7545
+ live.on('notes.changed', (payload) => { /* payload is typed */ });
7546
+ ```
7547
+
7548
+ There is no `bindEvents`. Subscribing to an announcement is `bindRealtimeClient`,
7549
+ because a second `on()` over the same socket would be a second name for one
7550
+ thing.
7551
+
7552
+ Two details worth knowing before you meet them:
7553
+
7554
+ - A rejected announcement is reported **at the peer that refused it**, through
7555
+ `onRejected`. A fire-and-forget event has no acknowledgement channel to carry
7556
+ a refusal back to the sender.
7557
+ - An issue path names the tuple position — a bad `revision` field appears as
7558
+ `0.revision`, because event arguments are a tuple and one payload sits at
7559
+ index 0 of it.
7560
+
7561
+ ### Delivery modes are about **this process**
7562
+
7563
+ A browser cannot delay or veto a server's announcement, so a mode says nothing
7564
+ about the wire. It says what the server's own listeners may do.
7565
+
7566
+ | mode | what the caller gets back | a listener that fails |
7567
+ |---|---|---|
7568
+ | `emit` | nothing; announce and continue | isolated, reported, the others still run |
7569
+ | `serial` | a promise for the whole sequence | reported at its deadline; the next still runs |
7570
+ | `decision` | a promise for `allow` / `deny` | **denies**, with the reason |
7571
+
7572
+ Pass the declaration to the bus and it enforces the mode:
7573
+
7574
+ ```ts
7575
+ import { createEventBus } from 'stitchkit/server';
7576
+ import type { EventPayloads } from 'stitchkit/live';
7577
+
7578
+ const bus = createEventBus<EventPayloads<typeof events>>({ topics: events.topics });
7579
+
7580
+ bus.on('notes.archiving', async () =>
7581
+ (await runsOpen()) ? { outcome: 'deny', reason: 'a run is still open' } : { outcome: 'allow' },
7582
+ );
7583
+
7584
+ const decision = await bus.decide('notes.archiving', { folder: 'inbox' });
7585
+ bus.emit('notes.archiving', …); // throws: that topic is delivered with decide()
7586
+ ```
7587
+
7588
+ The asymmetry in the table is deliberate and is the point of the mode. For
7589
+ `emit`, isolating a failing listener means "the others carry on". For a vote, the
7590
+ same isolation would mean "counted as consent" — so a listener that throws, times
7591
+ out, or returns something that is not a decision **denies**. A listener that was
7592
+ asked and did not answer has not agreed.
7593
+
7594
+ `whenAllDefer` and `listenerTimeoutMs` have no defaults, and that is on purpose:
7595
+ either default would be a standing `allow`/`deny`, or an unbounded wait, applied
7596
+ to every topic whose author never thought about it.
7597
+
7598
+ ---
7599
+
7600
+ ## Watched reads
7601
+
7602
+ A watched read is a GET the server re-runs when a topic announces a change,
7603
+ pushing the new answer to everyone watching it.
7604
+
7605
+ ```ts
7606
+ // server
7607
+ import { createWatchHub } from 'stitchkit/application';
7608
+
7609
+ const hub = createWatchHub({
7610
+ read: (operation, args) => api[operation.action](args),
7611
+ watchable: (operation) => operation.service === 'notes',
7612
+ invalidatedBy: () => ['notes.changed'],
7613
+ subscribe: (topic, listener) => bus.on(topic, listener),
7614
+ holdMs: 30_000,
7615
+ });
7616
+ ```
7617
+
7618
+ ```ts
7619
+ // browser
7620
+ import { createWatchClient } from 'stitchkit/live';
7621
+
7622
+ const watch = createWatchClient(notesContract, { transport: live, holdMs: 30_000 });
7623
+
7624
+ const handle = watch.list({ folder: 'inbox' });
7625
+ const stop = handle.subscribe({
7626
+ value: (notes) => render(notes),
7627
+ state: (state) => {
7628
+ if (state.phase === 'unavailable') showBanner(state.message);
7629
+ },
7630
+ });
7631
+ ```
7632
+
7633
+ Two browsers asking the same question are **one read**. The identity of a
7634
+ question is `(service, action, digest of arguments)`, and the digest is
7635
+ order-independent — `{a, b}` and `{b, a}` are the same question, which a plain
7636
+ `JSON.stringify` key would get wrong exactly when two components happen to build
7637
+ their argument object in a different order.
7638
+
7639
+ ### Who shares with whom
7640
+
7641
+ A key is `(service, action, digest of arguments)`, and everyone on that key gets
7642
+ one read. So the question a sharing primitive has to answer is what keeps one
7643
+ caller's answer from reaching another — and here the answer is structural:
7644
+ `read` is **given no subscriber**. An answer that depends on who is asking has to
7645
+ carry the asker in its arguments, and the arguments are what the digest is taken
7646
+ over, so two callers who differ get two keys and two reads.
7647
+
7648
+ The one way to defeat that is to resolve an identity from ambient state *inside*
7649
+ `read` — a request-scoped context, a module-level "current user". Then every
7650
+ subscriber on that key receives whatever the first read happened to resolve.
7651
+ Put the identity in the arguments instead.
7652
+
7653
+ ### Three states, not two
7654
+
7655
+ `state.phase` comes from `LiveStatePhase`, the vocabulary the live-state
7656
+ controller already uses. The one to notice is `opening`: subscribed, nothing read
7657
+ yet. It is neither healthy nor broken, and rendering it as "unavailable" tells a
7658
+ user something is wrong when the truth is that it is early.
7659
+
7660
+ A failed read arrives as `unavailable` with the read's own `code` and `message`.
7661
+ The hub retries on its own backoff, and a success clears the state without anyone
7662
+ asking.
7663
+
7664
+ ### `watch` or `createLiveStateController`?
7665
+
7666
+ > **If you would have written `applyEvent` as `(_, next) => next`, you want
7667
+ > `watch`: the server sends the value whole.** `createLiveStateController` is for
7668
+ > a server that sends *deltas* you have to fold. `watch` is that controller with
7669
+ > the fold fixed to replacement, plus the key sharing and the retention — so
7670
+ > applying both to one value is always a mistake.
7671
+
7672
+ ### What it cannot promise
7673
+
7674
+ One source per **process**. Two processes behind a balancer are two reads, and no
7675
+ test can show you otherwise from inside one of them.
7676
+
7677
+ ---
7678
+
7679
+ ## Keyspaces
7680
+
7681
+ Authoritative memory in front of a durable backend: read synchronously, write
7682
+ through one serialised chain.
7683
+
7684
+ ```ts
7685
+ import { defineKeyspace, keyspaceResource, sqliteKeyspaceBackend } from 'stitchkit/application';
7686
+
7687
+ const sessions = defineKeyspace('sessions', {
7688
+ schema: SessionSchema,
7689
+ key: (session) => session.id,
7690
+ });
7691
+
7692
+ createApplication({
7693
+ resources: [
7694
+ keyspaceResource(sessions, {
7695
+ backend: sqliteKeyspaceBackend(sessions, { database }),
7696
+ onChanged: (change) => bus.emit('sessions.changed', { id: change.key }),
7697
+ dependsOn: ['database'],
7698
+ }),
7699
+ ],
7700
+ });
7701
+ ```
7702
+
7703
+ The order is **backend, then memory, then the change event**, and it is the whole
7704
+ point:
7705
+
7706
+ - Backend first is what makes the memory authoritative — a reader can never
7707
+ observe a value that did not survive. The reverse order makes writes feel
7708
+ instant and makes every rejected write a lie somebody already read.
7709
+ - The event last, after memory, because a subscriber woken by it reads
7710
+ immediately. An event emitted before the memory update is a wake-up to the old
7711
+ value.
7712
+
7713
+ It is a **resource**, read with `context.use(...)`, not a function you call
7714
+ wherever you need it. The kernel resolves its resource graph in the constructor
7715
+ and cannot register one afterwards, so a keyspace opened by a bare call inside
7716
+ another resource's `start` is never drained, never closed, and never ordered
7717
+ against the things that write to it.
7718
+
7719
+ "Durable" means the backend's `put` resolved. It does **not** mean `fsync` —
7720
+ SQLite in WAL mode with `synchronous = NORMAL` returns success before the data
7721
+ reaches the disk. What an acknowledgement is worth is the backend's to state and
7722
+ yours to configure. In the same spirit, a keyspace that closes with writes still
7723
+ queued reports how many through `onUnwritten`, because a graceful close is
7724
+ deadline-bounded and a forced one can cut a drain short.
7725
+
7726
+ ---
7727
+
7728
+ ## The trust fence
7729
+
7730
+ Compare the authority a request addressed against a list you declared, and refuse
7731
+ before anything is dispatched.
7732
+
7733
+ ```ts
7734
+ import { createTrustFence } from 'stitchkit/server';
7735
+
7736
+ const fence = createTrustFence({
7737
+ trustedHosts: ['app.internal', 'localhost:5180', '192.168.1.10:5180'],
7738
+ logger,
7739
+ });
7740
+
7741
+ createServer({
7742
+ services,
7743
+ hooks: composeLifecycleHooks(fence.hooks, applicationHooks),
7744
+ socket: { io, allowRequest: fence.allowRequest },
7745
+ });
7746
+ ```
7747
+
7748
+ ### Install **both** halves
7749
+
7750
+ `fence.hooks` fences HTTP. `fence.allowRequest` fences the Socket.IO lane — and
7751
+ that lane never reaches `hooks.onRequest` on either runtime: on Bun the fetch
7752
+ handler answers the socket prefix before the contract handler exists, and on Node
7753
+ `socket.attach` hands the upgrade to Socket.IO directly. A fence installed only
7754
+ in `hooks` leaves open the lane a live application pushes its data over.
7755
+
7756
+ Order matters too: `composeLifecycleHooks` stops at the first hook that answers,
7757
+ so the fence goes **first**. Behind a hook that returns a maintenance page, it
7758
+ never runs.
7759
+
7760
+ ### What it does not do
7761
+
7762
+ - **It does not gate operations.** "Only from this machine" is a property of an
7763
+ operation, and an operation is known after routing, not before it. Write it as
7764
+ an auth rule over `ctx.ipAddress` — `isLoopbackAddress` is exported for
7765
+ exactly that — where it composes with your other rules.
7766
+ - **It does not re-check an open connection.** Both lanes check at admission.
7767
+ - **It does not see an `OPTIONS` preflight** when `cors` is configured; that is
7768
+ answered earlier. The preflight carries no operation and no body, and the
7769
+ DNS-rebinding attack the fence exists for is same-origin and sends none.
7770
+
7771
+ An entry is `host` or `host:port` — no scheme, no path, no wildcard — and an
7772
+ entry that is not a readable authority stops startup naming itself, rather than
7773
+ quietly matching nothing. There is no implicit `localhost`: list it when you want
7774
+ it.
7775
+
7776
+ Every refusal answers the same bare 403. A response that said *which* rule
7777
+ refused would let a caller learn your trusted list one guess at a time; the
7778
+ reason goes to `onRefused` and the log, where you are.
7779
+
7780
+ ### One thing that changed
7781
+
7782
+ A route group can no longer declare `onRequest`. It never worked — the framework
7783
+ dispatched only the server-level hook — and it could not be made to work without
7784
+ matching group prefixes a second time, ahead of the real router. Use the
7785
+ server-level `hooks.onRequest` to refuse before dispatch, or the group's
7786
+ `authorize` to gate once the endpoint is known.
7787
+
7788
+
7474
7789
  ==============================================================================
7475
7790
  # Guide: Auth & errors (docs/guide/auth-and-errors.md)
7476
7791
  ==============================================================================
@@ -7925,6 +8240,15 @@ is turned into a `400 VALIDATION_ERROR` automatically — and it carries the
7925
8240
  offending fields as structured `details.issues`, so a machine client matches on
7926
8241
  them instead of parsing the text `message`:
7927
8242
 
8243
+ `path` and `code` are the framework's. **`message` is not** — it is `issue.message`
8244
+ as Zod produced it, so for a `.refine()` it is whatever the schema's author wrote.
8245
+ The framework never puts a submitted value into it, and its own messages describe
8246
+ types rather than values (`expected string, received undefined`, `Invalid UUID`),
8247
+ but that is a property of today's messages, not a guarantee anyone is in a
8248
+ position to make: the text belongs to whoever declared the schema. Forward `path`
8249
+ and `code` to an end user freely; treat `message` as internal unless you wrote
8250
+ every refine in the contract.
8251
+
7928
8252
  ```json
7929
8253
  {
7930
8254
  "error": {
@@ -9594,6 +9918,51 @@ makes one thing your job rather than the resolver's:
9594
9918
  The mechanical part is identical either way. Only the *noticing* differs, and an
9595
9919
  exact pin moves it onto you.
9596
9920
 
9921
+ ## Released migration: 0.75.0
9922
+
9923
+ Two mechanical renames and one option that has to move. Nothing about a passing
9924
+ request changes.
9925
+
9926
+ ### 1. A route group's `onRequest`
9927
+
9928
+ ```bash
9929
+ rg -n "hooks:\s*\{[^}]*onRequest" --glob '*.ts'
9930
+ ```
9931
+
9932
+ Any match **inside a `groups: [...]` entry** must move. The hook was accepted and
9933
+ never dispatched, so the behaviour you have today is "no hook"; the fix is to
9934
+ decide which one you meant:
9935
+
9936
+ ```ts
9937
+ // refuse before dispatch, for the whole server
9938
+ createServer({ groups, hooks: { onRequest: gate } });
9939
+
9940
+ // gate this group once the endpoint is known
9941
+ groups: [{ pathPrefix: '/admin', services, hooks: { authorize: gate } }]
9942
+ ```
9943
+
9944
+ A group that still declares `onRequest` fails at startup with that message.
9945
+
9946
+ ### 2. The SQLite boundary type
9947
+
9948
+ ```bash
9949
+ rg -l "AgentRuntimeSqlite(Database|Statement|Value)" | xargs sed -i \
9950
+ -e 's/AgentRuntimeSqliteDatabase/SqliteDatabase/g' \
9951
+ -e 's/AgentRuntimeSqliteStatement/SqliteStatement/g' \
9952
+ -e 's/AgentRuntimeSqliteValue/SqliteValue/g'
9953
+ ```
9954
+
9955
+ Type-only: the same three methods, the same structural compatibility with
9956
+ `bun:sqlite` and a `node:sqlite` wrapper, imported from the same entrypoints.
9957
+ `initializeAgentRuntimeSqlite` keeps its name — it really is the agent runtime's
9958
+ schema.
9959
+
9960
+ ### 3. Nothing else
9961
+
9962
+ `stitchkit/live`, the watch hub, the keyspace and the trust fence are all
9963
+ additive. Adopt them when you want them; the guide is
9964
+ [Live data](./live.md).
9965
+
9597
9966
  ## Released migration: 0.74.0
9598
9967
 
9599
9968
  One change, and it only reaches you if you catch a refusal the **client** raised — one it made while
@@ -12664,6 +13033,30 @@ from the root `stitchkit`.
12664
13033
 
12665
13034
  ---
12666
13035
 
13036
+ ## `stitchkit/live`
13037
+
13038
+ **Evolving.** Browser-and-server-safe declarations for the things that change while a caller is
13039
+ watching: announcements, and reads the server re-runs. It owns no transport — the wire is the
13040
+ realtime contract from `stitchkit`, and the server halves live in `stitchkit/application`.
13041
+ → [ADR 0150](../decisions/0150-an-event-declaration-projects-onto-the-socket-we-already-run.md),
13042
+ [ADR 0153](../decisions/0153-a-watched-read-is-one-read-per-question.md).
13043
+
13044
+ | Export | Kind | Summary |
13045
+ |--------|------|---------|
13046
+ | `defineEvents` | function | declare topics: a wire name, one payload schema and how the topic is delivered to in-process listeners |
13047
+ | `toRealtimeContract` | function | project a declaration onto `RealtimeContract`, so the existing validated socket carries it |
13048
+ | `EventDeliveryMode` / `EventDecision` / `EventUndecided` | _types_ | `emit` / `serial` / `decision`, one listener's vote, and the outcome when every listener defers |
13049
+ | `EventTopicDeclaration` / `EventTopicRegistry` / `EventsConfig` / `EventsDeclaration` | _types_ | one topic's schema, mode, `whenAllDefer` and `listenerTimeoutMs`, and the declaration they compose into |
13050
+ | `EventPayloads` / `EventTopicsOfMode` / `WireTopic` | _types_ | payload map keyed by wire topic, the topics of one mode, and the `prefix.name` a topic is addressed by |
13051
+ | `createWatchClient` | function | contract-shaped watch client: `watch.action(args)` returns a ref-counted handle sharing one subscription |
13052
+ | `WatchHandle` / `WatchListeners` / `WatchClientConfig` / `WatchTransport` / `TypedWatchClient` | _types_ | subscribe/close, the value and state listeners, hold window and open deadline, and the bound realtime client it rides |
13053
+ | `watchContract` | const | the four-event realtime contract a watched read travels on |
13054
+ | `WATCH_OPEN` / `WATCH_CLOSE` / `WATCH_VALUE` / `WATCH_STATE` | const | the event names of that contract |
13055
+ | `WatchKey` / `WatchKeySchema` / `watchKeyString` | type / schema / function | `(service, action, arguments digest)` — the identity both ends compute the same way |
13056
+ | `WatchValueFrame` / `WatchValueSchema` / `WatchStateFrame` / `WatchStateSchema` | _types_ | a value with its monotonic revision, and a phase from `LiveStatePhase` with the read's own code and message |
13057
+
13058
+ ---
13059
+
12667
13060
  ## `stitchkit/primitives`
12668
13061
 
12669
13062
  Browser-and-server-safe declarations for facts an application wants to define once. See the
@@ -12918,6 +13311,21 @@ Also re-exports the error helpers from `stitchkit/contract`.
12918
13311
 
12919
13312
  ---
12920
13313
 
13314
+ ### Trust fence
13315
+
13316
+ Refuses a request whose `Host` is not a declared authority, at both admission points — the socket
13317
+ lane never reaches the hooks on either runtime.
13318
+ → [ADR 0151](../decisions/0151-a-trust-fence-names-its-lanes-because-one-of-them-bypasses-hooks.md).
13319
+
13320
+ | Export | Kind | Summary |
13321
+ |--------|------|---------|
13322
+ | `createTrustFence` | function | `hooks` for the HTTP lane and `allowRequest` for the Socket.IO lane; a bare 403 that never says which rule refused |
13323
+ | `isLoopbackAddress` | function | whether an address is this machine — for the auth rule a privileged operation belongs in, not for the fence |
13324
+ | `TrustFence` / `TrustFenceConfig` / `TrustRefusal` / `TrustRefusalReason` / `TrustLane` | _types_ | the two admission points, the declared authorities, and why and where a request was refused |
13325
+ | `RouteGroupHooks` | _type_ | a route group's hooks — `LifecycleHooks` without `onRequest`, which runs before the group is known |
13326
+
13327
+ ---
13328
+
12921
13329
  ## `stitchkit/application`
12922
13330
 
12923
13331
  Server-only process-local application composition. See the
@@ -13067,6 +13475,20 @@ the function that derives it from a snapshot,
13067
13475
  `ApplicationResourceShutdownSchema` / `ApplicationResourceShutdown`, and
13068
13476
  `ApplicationShutdownResultSchema` / `ApplicationShutdownResult`.
13069
13477
 
13478
+ ### Keyspace and watched reads
13479
+
13480
+ | Export | Kind | Summary |
13481
+ |--------|------|---------|
13482
+ | `defineKeyspace` / `keyspaceResource` | function | a named record set read synchronously from memory and written through one serialised chain; backend first, then memory, then the change event |
13483
+ | `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle |
13484
+ | `memoryKeyspaceBackend` / `sqliteKeyspaceBackend` / `SqliteKeyspaceBackendConfig` | function / type | a disposable in-process backend, and one table with a key and a JSON payload over a caller-owned database |
13485
+ | `SqliteDatabase` / `SqliteStatement` / `SqliteValue` | _types_ | the minimal synchronous SQLite boundary the framework types against |
13486
+ | `createWatchHub` / `watchKey` | function | one read per question however many are asking: single-flight per key, re-read on a declared topic, publish only what changed |
13487
+ | `WatchHub` / `WatchHubConfig` / `WatchOperation` / `WatchSubscriber` / `AttachedWatcher` | _types_ | the hub, its read/watchable/invalidation policy, the operation identity, and one connection's attachment |
13488
+
13489
+ → [ADR 0152](../decisions/0152-a-keyspace-is-memory-that-nothing-reaches-before-it-is-durable.md),
13490
+ [ADR 0153](../decisions/0153-a-watched-read-is-one-read-per-question.md).
13491
+
13070
13492
  ## `stitchkit/application/grammy`
13071
13493
 
13072
13494
  Isolated optional-peer lifecycle adapters. Importing `stitchkit/application`
@@ -13405,7 +13827,7 @@ loaded by the neutral, browser or Node runtime surfaces.
13405
13827
  | `BunSqliteAgentRuntimeStoreConfig` | _type_ | database filename plus optional create and initialization policies |
13406
13828
  | `createSqliteAgentRuntimeStore` | function | build the normalized store over an injected synchronous SQLite boundary |
13407
13829
  | `initializeAgentRuntimeSqlite` | function | initialize or validate only Stitchkit's namespaced SQLite schema |
13408
- | `AgentRuntimeSqliteDatabase` / `AgentRuntimeSqliteStatement` / `AgentRuntimeSqliteValue` | _type_ | minimal runtime-neutral synchronous SQLite boundary |
13830
+ | `SqliteDatabase` / `SqliteStatement` / `SqliteValue` | _type_ | minimal runtime-neutral synchronous SQLite boundary |
13409
13831
  | `SqliteAgentRuntimeStore` / `SqliteAgentRuntimeStoreConfig` | _type_ | durable store handle, owned connection lifecycle and initialization policy |
13410
13832
 
13411
13833
  ---
@@ -13460,7 +13882,7 @@ the Bun leaf but imports only `node:sqlite`.
13460
13882
  | `createNodeSqliteAgentRuntimeStore` | function | open an owned Node `DatabaseSync`, initialize/validate schema v1 and return `{ store, close }` |
13461
13883
  | `NodeSqliteAgentRuntimeStoreConfig` | _type_ | database filename plus optional read-only and initialization policies; read-only requires an initialized schema |
13462
13884
  | `createSqliteAgentRuntimeStore` / `initializeAgentRuntimeSqlite` | function | shared normalized adapter and namespaced schema lifecycle |
13463
- | `AgentRuntimeSqliteDatabase` / `AgentRuntimeSqliteStatement` / `AgentRuntimeSqliteValue` | _type_ | minimal runtime-neutral synchronous SQLite boundary |
13885
+ | `SqliteDatabase` / `SqliteStatement` / `SqliteValue` | _type_ | minimal runtime-neutral synchronous SQLite boundary |
13464
13886
  | `SqliteAgentRuntimeStore` / `SqliteAgentRuntimeStoreConfig` | _type_ | durable store handle, owned connection lifecycle and initialization policy |
13465
13887
 
13466
13888
  ## `stitchkit/observability`
package/llms.txt CHANGED
@@ -16,6 +16,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
16
16
  - [Application migration recipes](https://github.com/max-listov/stitchkit/blob/master/docs/guide/application-migration-recipes.md): executable database, poller, queue-consumer and operational publishing cutovers
17
17
  - [CLI](https://github.com/max-listov/stitchkit/blob/master/docs/guide/cli.md): contracts as a command-line program
18
18
  - [Realtime](https://github.com/max-listov/stitchkit/blob/master/docs/guide/realtime.md): Socket.IO server/client wrappers, handshake auth, the cache bridge, a raw WebSocket lane
19
+ - [Live data](https://github.com/max-listov/stitchkit/blob/master/docs/guide/live.md): defineEvents beside the contract, watched reads shared by every subscriber, keyspaces with authoritative memory, and the trust fence
19
20
  - [Auth & errors](https://github.com/max-listov/stitchkit/blob/master/docs/guide/auth-and-errors.md): scopes, createAuthHook, JWT/cookies, the AppError model, the stitch error-code registry
20
21
  - [Observability](https://github.com/max-listov/stitchkit/blob/master/docs/guide/observability.md): request and tool-call observability, W3C trace context, createObservability
21
22
  - [Testing & deployment](https://github.com/max-listov/stitchkit/blob/master/docs/guide/testing-and-deployment.md): in-process testing; deploying on Bun and on Node (serveNode)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.74.1",
3
+ "version": "0.75.1",
4
4
  "description": "Contract-first backend framework — one defineContract() into an HTTP API, MCP tools, AI-agent tools and a typed client. Bun and Node.",
5
5
  "keywords": [
6
6
  "bun",
@@ -49,6 +49,10 @@
49
49
  "types": "./dist/index.d.ts",
50
50
  "import": "./dist/index.js"
51
51
  },
52
+ "./live": {
53
+ "types": "./dist/live.d.ts",
54
+ "import": "./dist/live.js"
55
+ },
52
56
  "./react": {
53
57
  "types": "./dist/react.d.ts",
54
58
  "import": "./dist/react.js"
@@ -152,7 +156,7 @@
152
156
  "scripts": {
153
157
  "build:native-contained-files": "node scripts/build-contained-files-native.mjs",
154
158
  "check": "bun x tsc --noEmit",
155
- "build:browser": "bun build src/index.ts src/react.ts src/contract/index.ts src/primitives.ts src/declaration.ts src/agent-runtime-browser.ts --outdir dist --target node --packages external --splitting --root src",
159
+ "build:browser": "bun build src/index.ts src/live.ts src/react.ts src/contract/index.ts src/primitives.ts src/declaration.ts src/agent-runtime-browser.ts --outdir dist --target node --packages external --splitting --root src",
156
160
  "build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/tool-invoker.ts src/cli.ts src/remote.ts src/files.ts src/testing.ts src/observability/index.ts src/agent-runtime.ts src/agent-runtime-harness.ts src/agent-runtime-coding-tools.ts src/agent-runtime-openrouter.ts src/agent-runtime-sqlite-bun.ts src/agent-runtime-sqlite-node.ts src/application.ts src/application-grammy.ts src/application-opentelemetry.ts src/telegram.ts --outdir dist --target node --packages external --splitting --root src",
157
161
  "build:js": "bun run build:browser && bun run build:server && bun scripts/preserve-webpack-ignore.mjs",
158
162
  "build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly && bun scripts/rewrite-declaration-specifiers.mjs",