stitchkit 0.118.0 → 0.119.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.
Files changed (151) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/dist/browser/http.d.ts.map +1 -1
  3. package/dist/contract/client-types.d.ts +6 -3
  4. package/dist/contract/client-types.d.ts.map +1 -1
  5. package/dist/contract/define-validation.d.ts.map +1 -1
  6. package/dist/contract/define.d.ts +18 -12
  7. package/dist/contract/define.d.ts.map +1 -1
  8. package/dist/contract/endpoint-capabilities.d.ts +28 -0
  9. package/dist/contract/endpoint-capabilities.d.ts.map +1 -0
  10. package/dist/contract/factory.d.ts +49 -19
  11. package/dist/contract/factory.d.ts.map +1 -1
  12. package/dist/contract/tool-view.d.ts +1 -1
  13. package/dist/contract/tool-view.d.ts.map +1 -1
  14. package/dist/contract/transports.d.ts +5 -0
  15. package/dist/contract/transports.d.ts.map +1 -0
  16. package/dist/entrypoints/agent-runtime/coding-tools.js +6 -5
  17. package/dist/entrypoints/agent-runtime/harness.js +6 -5
  18. package/dist/entrypoints/agent-runtime/sandbox.js +6 -5
  19. package/dist/entrypoints/agent-runtime/sqlite/bun.js +9 -8
  20. package/dist/entrypoints/agent-runtime/sqlite/node.js +9 -8
  21. package/dist/entrypoints/agent-runtime/testing.js +9 -8
  22. package/dist/entrypoints/agent-runtime.js +14 -13
  23. package/dist/entrypoints/application/diagnostic-journal.js +3 -3
  24. package/dist/entrypoints/application/directory-inbox.js +4 -4
  25. package/dist/entrypoints/application/opentelemetry.js +1 -1
  26. package/dist/entrypoints/application.js +5 -5
  27. package/dist/entrypoints/cli/publish.js +4 -4
  28. package/dist/entrypoints/cli.js +8 -7
  29. package/dist/entrypoints/contract.d.ts +1 -1
  30. package/dist/entrypoints/contract.d.ts.map +1 -1
  31. package/dist/entrypoints/contract.js +6 -4
  32. package/dist/entrypoints/files.js +3 -3
  33. package/dist/entrypoints/geo/contract.d.ts +7 -0
  34. package/dist/entrypoints/geo/contract.d.ts.map +1 -0
  35. package/dist/entrypoints/geo/contract.js +6 -0
  36. package/dist/entrypoints/geo.js +60 -23
  37. package/dist/entrypoints/http.js +2 -2
  38. package/dist/entrypoints/index.js +8 -6
  39. package/dist/entrypoints/live.js +1 -1
  40. package/dist/entrypoints/node.js +7 -6
  41. package/dist/entrypoints/primitives.js +1 -1
  42. package/dist/entrypoints/process/owner-loss.js +1 -1
  43. package/dist/entrypoints/process.js +3 -3
  44. package/dist/entrypoints/react.js +1 -1
  45. package/dist/entrypoints/remote.js +4 -3
  46. package/dist/entrypoints/server.d.ts +1 -1
  47. package/dist/entrypoints/server.d.ts.map +1 -1
  48. package/dist/entrypoints/server.js +42 -27
  49. package/dist/entrypoints/telegram.js +2 -2
  50. package/dist/entrypoints/testing.js +7 -6
  51. package/dist/entrypoints/tools/connections.js +3 -2
  52. package/dist/entrypoints/tools/contract.js +1 -1
  53. package/dist/entrypoints/tools/invoker.js +5 -4
  54. package/dist/entrypoints/tools/mcp.js +7 -6
  55. package/dist/entrypoints/tools.js +17 -15
  56. package/dist/entrypoints/tracking/contract.js +5 -3
  57. package/dist/entrypoints/tracking/performance.js +3 -3
  58. package/dist/entrypoints/tracking.d.ts +1 -1
  59. package/dist/entrypoints/tracking.d.ts.map +1 -1
  60. package/dist/entrypoints/tracking.js +63 -83
  61. package/dist/entrypoints/xlsx.js +1 -1
  62. package/dist/geo/index.d.ts +2 -1
  63. package/dist/geo/index.d.ts.map +1 -1
  64. package/dist/geo/record.d.ts +8 -0
  65. package/dist/geo/record.d.ts.map +1 -0
  66. package/dist/geo/resolver.d.ts +1 -3
  67. package/dist/geo/resolver.d.ts.map +1 -1
  68. package/dist/geo/schema.d.ts +44 -0
  69. package/dist/geo/schema.d.ts.map +1 -0
  70. package/dist/geo/types.d.ts +2 -13
  71. package/dist/geo/types.d.ts.map +1 -1
  72. package/dist/{index-n5sv4z0w.js → index-1k18qzqa.js} +13 -15
  73. package/dist/{index-tsngj2b5.js → index-2dfvv1jg.js} +206 -206
  74. package/dist/{index-d9hq291n.js → index-4jpykzvw.js} +70 -81
  75. package/dist/{index-ghtpfz4z.js → index-57gqaq8b.js} +2 -2
  76. package/dist/index-69t14s4a.js +39 -0
  77. package/dist/{index-6p3frkv5.js → index-6a69xzrx.js} +4 -4
  78. package/dist/{index-dky0dcmf.js → index-72r9k0av.js} +6 -2
  79. package/dist/{index-vv1sch8a.js → index-8vzh5wpv.js} +13 -8
  80. package/dist/{index-xx1j310k.js → index-8yxgcyt6.js} +171 -169
  81. package/dist/{index-p6apg0jg.js → index-9frf01t9.js} +16 -9
  82. package/dist/{index-q0th8q0z.js → index-9mjrnqxs.js} +1 -3
  83. package/dist/{index-ega3yz4c.js → index-aqwg2dm5.js} +1 -1
  84. package/dist/{index-073nw9qr.js → index-ba7are04.js} +524 -522
  85. package/dist/{index-095ya5hm.js → index-bbvc59gw.js} +13 -11
  86. package/dist/{index-txg1npmg.js → index-cyxp8rbf.js} +1 -1
  87. package/dist/index-d3xdyjt6.js +38 -0
  88. package/dist/{index-n8z3rvzw.js → index-f4njz9j0.js} +3 -5
  89. package/dist/{index-90p34rts.js → index-fcqj44mb.js} +3 -1
  90. package/dist/{index-86nbfysb.js → index-ff39f9dr.js} +3 -3
  91. package/dist/{index-75ns2ybj.js → index-fqd4yqwd.js} +1 -1
  92. package/dist/{index-nqxrm176.js → index-g444vfz2.js} +1 -1
  93. package/dist/{index-vwhrnp67.js → index-kz9734jc.js} +3 -3
  94. package/dist/{index-dr3t5n1v.js → index-nn9qvkww.js} +46 -46
  95. package/dist/{index-qezhmp51.js → index-nz06qw5a.js} +3 -1
  96. package/dist/{index-1bx1xv1k.js → index-pfvde2fz.js} +5 -2
  97. package/dist/{index-s80awwsw.js → index-qch1vcaa.js} +3 -1
  98. package/dist/{index-cycp3r3e.js → index-rxcgp4b9.js} +2 -2
  99. package/dist/{index-w7jq3fz9.js → index-s0qjs6ee.js} +1198 -1196
  100. package/dist/{index-p0rf7sq9.js → index-s20s2mad.js} +209 -209
  101. package/dist/{index-v6vb5qx8.js → index-s3yxw6kt.js} +1 -1
  102. package/dist/{index-cqmqyjax.js → index-sa99p98w.js} +1 -1
  103. package/dist/{index-0gxvns6s.js → index-sba3e6m4.js} +1 -0
  104. package/dist/{index-f24bdt13.js → index-v1swr26p.js} +3 -1
  105. package/dist/{index-rhzkndr5.js → index-x4p3kw1p.js} +533 -530
  106. package/dist/{index-hdjmdwsj.js → index-x8d9fdeg.js} +513 -507
  107. package/dist/{index-htxh46ej.js → index-ycqhyars.js} +2 -2
  108. package/dist/{index-9bwcthxw.js → index-yj6saztf.js} +31 -13
  109. package/dist/{index-tdpmfks8.js → index-ynrf908v.js} +1 -1
  110. package/dist/{index-f6m4c545.js → index-zrdcmg6j.js} +4 -6
  111. package/dist/process/bun-child.d.ts +3 -2
  112. package/dist/process/bun-child.d.ts.map +1 -1
  113. package/dist/server/contract-method.d.ts.map +1 -1
  114. package/dist/server/file.d.ts +12 -6
  115. package/dist/server/file.d.ts.map +1 -1
  116. package/dist/tools/internal/surface-projector.d.ts.map +1 -1
  117. package/dist/tools/operations/async-operation-canonical-types.d.ts +1 -1
  118. package/dist/tracking/attribution.d.ts +21 -25
  119. package/dist/tracking/attribution.d.ts.map +1 -1
  120. package/dist/tracking/client-visit.d.ts +5 -7
  121. package/dist/tracking/client-visit.d.ts.map +1 -1
  122. package/dist/tracking/client.d.ts +2 -2
  123. package/dist/tracking/client.d.ts.map +1 -1
  124. package/dist/tracking/contract.d.ts +1 -1
  125. package/dist/tracking/host.d.ts +0 -1
  126. package/dist/tracking/host.d.ts.map +1 -1
  127. package/dist/tracking/privacy.d.ts +6 -0
  128. package/dist/tracking/privacy.d.ts.map +1 -1
  129. package/dist/tracking/schemas.d.ts +2 -2
  130. package/dist/tracking/schemas.d.ts.map +1 -1
  131. package/llms/contract.txt +46 -24
  132. package/llms/geo-contract.txt +23 -0
  133. package/llms/geo.txt +40 -4
  134. package/llms/server.2.txt +19 -6
  135. package/llms/server.5.txt +2 -1
  136. package/llms/stitchkit.txt +4 -3
  137. package/llms/testing.txt +3 -3
  138. package/llms/tools.2.txt +2 -2
  139. package/llms/tools.3.txt +1 -1
  140. package/llms/tracking-contract.txt +1 -1
  141. package/llms/tracking.txt +27 -5
  142. package/llms/{upgrading-0.105.0-0.118.0.txt → upgrading-0.106.0-0.119.0.txt} +121 -410
  143. package/llms/upgrading-0.37.0-0.53.0.txt +969 -0
  144. package/llms/{upgrading-0.44.0-0.62.0.txt → upgrading-0.55.0-0.68.0.txt} +509 -528
  145. package/llms/{upgrading-0.63.0-0.83.0.txt → upgrading-0.69.0-0.95.0.txt} +506 -509
  146. package/llms/{upgrading-0.84.0-0.104.0.txt → upgrading-0.96.0-0.105.0.txt} +410 -506
  147. package/llms/upgrading.txt +5 -5
  148. package/llms-full.txt +271 -48
  149. package/llms.txt +11 -10
  150. package/package.json +23 -19
  151. package/llms/upgrading-0.37.0-0.44.0.txt +0 -444
@@ -11,11 +11,11 @@ Contains: Guide: Upgrading, general (docs/guide/upgrading.md)
11
11
 
12
12
  Load this slice, then every range slice between your installed version and the target:
13
13
 
14
- - 0.105.0 → 0.118.0: `llms/upgrading-0.105.0-0.118.0.txt`
15
- - 0.84.0 → 0.104.0: `llms/upgrading-0.84.0-0.104.0.txt`
16
- - 0.63.0 → 0.83.0: `llms/upgrading-0.63.0-0.83.0.txt`
17
- - 0.44.0 → 0.62.0: `llms/upgrading-0.44.0-0.62.0.txt`
18
- - 0.37.0 → 0.44.0: `llms/upgrading-0.37.0-0.44.0.txt`
14
+ - 0.106.0 → 0.119.0: `llms/upgrading-0.106.0-0.119.0.txt`
15
+ - 0.96.0 → 0.105.0: `llms/upgrading-0.96.0-0.105.0.txt`
16
+ - 0.69.0 → 0.95.0: `llms/upgrading-0.69.0-0.95.0.txt`
17
+ - 0.55.0 → 0.68.0: `llms/upgrading-0.55.0-0.68.0.txt`
18
+ - 0.37.0 → 0.53.0: `llms/upgrading-0.37.0-0.53.0.txt`
19
19
 
20
20
  # Upgrading stitchkit
21
21
 
package/llms-full.txt CHANGED
@@ -78,16 +78,17 @@ own, recorded as an ADR.
78
78
  | `stitchkit/release` | browser **and** server | evolving | a page follows the release it was built for — `createReleaseMarker` on the server, `createReleaseWatcher` in the browser, the `X-Build-Id` header and a socket event between them |
79
79
  | `stitchkit/voice` | browser **and** server | evolving | the mechanics of a live voice reply — `SentenceCutter`, `speakableText`, a `SpeechQueue` that synthesises ahead and stops on interruption, the conversation phases |
80
80
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
81
+ | `stitchkit/geo/contract` | browser **and** server | evolving | the place snapshot schema, for a shared contract that stores or sends `GeoAttribution`; brings `zod` |
81
82
  | `stitchkit/xlsx` | browser **and** server | evolving | a workbook from rows and typed columns, with no dependency; text never runs as a formula |
82
83
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
83
84
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
84
- | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 63 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
85
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 64 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
85
86
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, the managed-resource conformance kit and manual time for schedules and retries |
86
87
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
87
88
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
88
89
  | `stitchkit/react/keyboard` | browser | evolving | which part of a screen gets a key — ordered layers, one Escape per level, list zones with real focus; see [keyboard guide](keyboard.md) |
89
90
  | `stitchkit/react/keyboard/testing` | server (Bun or Node) | evolving | the keyboard layers and zones without a DOM — fakes for the window, elements and key events beside the two engines, to test the order of layers, `scope`/`inert` and the edges of a zone in milliseconds |
90
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 25 of the 63 minors since 0.56.2, most recently 0.115.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
91
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 25 of the 64 minors since 0.56.2, most recently 0.115.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
91
92
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
92
93
  | `stitchkit/agent-runtime/testing` | tests on Bun or Node | evolving | credential-free replay, scripted provider faults, deterministic race controls and the agent-store conformance kit |
93
94
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -99,7 +100,7 @@ own, recorded as an ADR.
99
100
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
100
101
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
101
102
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
102
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 14 of the 63 minors since 0.56.2, most recently 0.110.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
103
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 14 of the 64 minors since 0.56.2, most recently 0.110.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
103
104
  | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
104
105
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
105
106
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
@@ -445,7 +446,7 @@ Both entrypoints are **evolving**.
445
446
  | the event envelope, the batch, the dispositions, the visit entry and lease schemas | the event **types**, their metadata, their labels |
446
447
  | the tab-shared outbox with reserved sequences and a short flush lease | which storage adapter (IndexedDB in a tab) |
447
448
  | the page-leave beacon that arrives, and its queued insurance copy | the session, the router, the React provider |
448
- | visible time, scroll milestones, `data-track` clicks, UTM first/current touch | the referrer → source map, the attribute names |
449
+ | visible time, scroll milestones, `data-track` clicks, raw first/current touch | channel classification, click-marker names, the attribute names |
449
450
  | `dispositionTrackingBatch`, `issueVisitLease`, `activeIntervalOf`, presence | the tables, the transaction, adoption, geo, reports |
450
451
 
451
452
  ## Contract
@@ -492,7 +493,7 @@ const client = createTrackingClient<BrowserEvents>({
492
493
  deliver: (batch) => api.tracking.track(batch), // or a socket first, HTTP as the fallback
493
494
  unloadUrl: urls.tracking.track(),
494
495
  outbox: createTrackingOutbox(indexedDbOutboxStorage('my-app-tracking')),
495
- referrerMap: [{ pattern: /t\.me|telegram\.org/, source: 'telegram', medium: 'social' }],
496
+ clickIdParams: ['gclid', 'gbraid', 'wbraid', 'msclkid', 'yclid', 'ttclid', 'fbclid'],
496
497
  isAction: isInteractionAction,
497
498
  onVisit: (visitId) => rum.setVisit(visitId),
498
499
  })
@@ -505,6 +506,28 @@ client.track('ITEM_VIEW', { itemId, itemTitle })
505
506
  client.track('SIGN_OUT')
506
507
  ```
507
508
 
509
+ Attribution carries observations, not a channel verdict. `utm` contains only the current URL's
510
+ `utm_*` parameters, retaining case and whitespace up to 200 characters per field; tags without
511
+ `utm_source` are valid. Parameter names are case-sensitive (`UTM_SOURCE` is not a tag), a repeated
512
+ tag keeps its first value, and a whitespace-only value is kept as the fact it is. `referrer` carries
513
+ the referring URL with credentials masked. Neither an external referrer nor a click marker creates
514
+ campaign tags. Classify the channel on your server.
515
+
516
+ Attribution belongs to the visit: the bootstrap entry carries it, and an event batch carries events
517
+ only. Join events to their visit on the server to attribute them.
518
+
519
+ `clickIdParams` is the application's ordered list of query-parameter names to recognise. There is no
520
+ built-in list: the first configured name with a nonempty value becomes `clickId` in the touch and
521
+ bootstrap entry. Only the name is retained, never the click ID's value: a referrer from your own site
522
+ repeats the landing query, so the value of every configured marker in a referrer is masked too. An empty
523
+ list collects none; names must contain 1–200 characters, and `createTrackingClient` refuses a list that
524
+ does not. Bootstrap collects these facts even if browser storage is unavailable.
525
+
526
+ `client.attribution()` returns `firstTouch` and `currentTouch`. The first is persisted for 90 days
527
+ by default; the current is this capture's raw context, so an untagged page does not inherit an earlier
528
+ campaign. The server owns any policy for retaining the last non-direct channel. Stored first-touch
529
+ records carry a format version; unversioned attribution is discarded because its tags may be inferred.
530
+
508
531
  Two knobs match the client to your schema: `batchSize` is the contract's
509
532
  `maxEventsPerBatch` (a larger batch is a `400` the outbox can never retire),
510
533
  and `decorate` adds your `eventExtras` — a locale, an area, or the identity a
@@ -895,33 +918,55 @@ tools. Narrow it with `expose`:
895
918
 
896
919
  - `expose: ['HTTP']` — HTTP only. The endpoint never becomes a tool.
897
920
  - `expose: ['MCP', 'AGENT']` — a tool only; no HTTP route.
898
- - omit `expose` — all transports.
921
+ - `expose: []` — no transport; the endpoint stays hidden.
922
+ - omit `expose` — HTTP, MCP and AGENT; CLI is opt-in.
899
923
 
900
- For a curated or security-sensitive tool surface, opt into explicit tool
901
- exposure at the scoped factory:
924
+ Choose an application's default surface at its scoped factory. Scope and policy
925
+ are inferred in separate calls, so the returned endpoint types retain exact transports:
902
926
 
903
927
  ```ts
904
- const { defineContract } = createContractFactory<AppScope>({
905
- toolExposure: 'explicit',
928
+ const { defineContract } = createContractFactory<AppScope>().configure({
929
+ toolExposure: { default: ['HTTP', 'CLI'] },
906
930
  })
907
931
  ```
908
932
 
909
- With this policy, omitting `expose` materializes `['HTTP']` on the returned
910
- endpoint. MCP, Agent and CLI then require an explicit endpoint array. The plain
911
- factory and `defineContract` keep the default-on behaviour above.
933
+ An omitted `expose` (including `undefined`) materializes this array on the returned
934
+ endpoint before validation. An explicit endpoint array **replaces** it, never merges
935
+ with it: `['HTTP']` stays HTTP-only, `['MCP']` enables only MCP, and `[]` hides the
936
+ operation everywhere. Types, HTTP routes, typed clients, tools, CLI, OpenAPI and
937
+ transport summaries read that same materialized exposure.
938
+
939
+ HTTP client methods and URL builders require a **guaranteed** HTTP transport:
940
+ pass a literal policy array directly to `configure` so its const generic infers
941
+ the tuple. A stored array, even with `satisfies ContractFactoryConfig`, can widen
942
+ to `Transport[]` and contain no HTTP at runtime,
943
+ so it does not promise an HTTP method. Optional capabilities preserve both
944
+ possible effective exposures rather than claiming an unfiltered default tuple.
945
+
946
+ The same holds for an endpoint whose own type is broad, with or without a factory:
947
+ a value annotated as `EndpointDef` or `FactoryScopedEndpoint<Scope>` carries
948
+ `expose?: readonly Transport[]`, which may hold no HTTP, so it has no client method
949
+ or URL. A helper that builds endpoints keeps their literal type with a const generic:
950
+
951
+ ```ts
952
+ const endpoint = <const Endpoint extends FactoryScopedEndpoint<AppScope>>(value: Endpoint) => value
953
+ ```
912
954
 
913
- Consequently, `expose: ['HTTP']` is redundant inside a factory configured with
914
- `toolExposure: 'explicit'`: omit it unless the explicit spelling is useful to a
915
- local reader. Add `MCP`, `AGENT` or `CLI` only on endpoints that intentionally
916
- join those surfaces.
955
+ For a curated tool surface use `toolExposure: { default: ['HTTP'] }`, then name
956
+ tool transports only on endpoints that intentionally join them. An empty default
957
+ is legal. Each `configure` snapshots its array independently; it does not change
958
+ the base factory, sibling configurations or authored endpoints. The no-config
959
+ factory and plain `defineContract` keep the default-on behaviour above.
917
960
 
918
- Tool transports (`MCP`, `AGENT`) skip four kinds of endpoint automatically:
919
- `multipart` (a file upload is not a tool call),
920
- [`rawResponse`](./server.md#raw-response-endpoints) (its answer is bytes, which
921
- a tool result cannot carry — it would serialize to `{}`),
922
- [`responseMeta`](./server.md#typed-json-response-metadata) (outbound HTTP
923
- headers have no meaning on a tool call) and `rawBody` (HTTP-only by
924
- construction).
961
+ Six intrinsic HTTP-only forms intersect an **implicit factory default** with HTTP:
962
+ `multipart`, [`rawResponse`](./server.md#raw-response-endpoints),
963
+ [`responseMeta`](./server.md#typed-json-response-metadata), `rawBody`, streaming
964
+ responses and `HEAD`. Under `['HTTP', 'CLI']` they receive `['HTTP']`; under
965
+ `['CLI']` or `[]` they receive `[]`, not an implicitly reopened HTTP route.
966
+ An **explicit** non-HTTP exposure on any of them is a definition error, not a
967
+ silent downgrade. `safelistedBody` only changes HTTP parsing and remains
968
+ multi-transport. A factory policy applies only to contracts authored through
969
+ that factory, not to contracts another helper builds internally.
925
970
 
926
971
  ## Tool options (`tool`)
927
972
 
@@ -939,7 +984,7 @@ endpoint that can never be a tool (a raw response, a stream, `responseMeta`,
939
984
 
940
985
  `expose` stays outside the group: it chooses transports, HTTP included.
941
986
  Setting `tool` on an endpoint with no tool transport — `expose: ['HTTP']`, or an
942
- omitted `expose` under a factory with `toolExposure: 'explicit'` — throws at
987
+ omitted `expose` under a factory with a default containing no tool transport — throws at
943
988
  definition time. The pre-0.94 top-level keys (`toolName`, `toolView`, `ui`,
944
989
  `annotations`, `mcp`) are refused by name, with where each one went.
945
990
 
@@ -2236,11 +2281,24 @@ contract route does.
2236
2281
 
2237
2282
  ### Serving files & Range requests
2238
2283
 
2239
- `staticRoute` is basic on purpose — it reads the whole file into memory and has
2240
- no `Range` or conditional support; put a CDN in front, or use it for small web
2241
- assets. To serve **media** (video / audio / large downloads) that a browser must
2242
- seek and cache, use **`serveFile`** (Bun) — it streams the requested byte range
2243
- and speaks the conditional-request half of RFC 7233 / 9110:
2284
+ `staticRoute(prefix, dir, { cacheControl })` serves a directory: it checks
2285
+ containment, then answers the file through `serveFile`, so a static image or
2286
+ video carries `ETag`, `Last-Modified` and `Accept-Ranges`, revalidates to `304`
2287
+ and seeks with `206`. `cacheControl` sets `Cache-Control` on every answer; by
2288
+ default none is sent.
2289
+
2290
+ ```ts
2291
+ import { createServer, staticRoute } from 'stitchkit/server'
2292
+
2293
+ createServer({
2294
+ rawRoutes: [staticRoute('/static', './public', { cacheControl: 'public, max-age=3600' })],
2295
+ })
2296
+ ```
2297
+
2298
+ The route answers `GET`. To serve a file chosen by your own logic — through a
2299
+ contract, with `HEAD`, a download name or a custom type — call **`serveFile`**
2300
+ directly. It runs on Bun and Node, streams the requested byte range and speaks
2301
+ the conditional-request half of RFC 7233 / 9110:
2244
2302
 
2245
2303
  ```ts
2246
2304
  import { defineContract } from 'stitchkit/contract'
@@ -2290,7 +2348,7 @@ emit none.
2290
2348
  `serveFile` takes an explicit `path` and trusts it — **the caller owns
2291
2349
  containment**. For a URL-derived path use `staticRoute` (which enforces it) or
2292
2350
  `isWithinDir` first. The byte-range parser is exported on its own as
2293
- `parseByteRange(header, size)` for direct use and testing. → ADR 0023.
2351
+ `parseByteRange(header, size)` for direct use and testing. → ADR 0023, ADR 0281.
2294
2352
 
2295
2353
  ## Server primitives
2296
2354
 
@@ -4866,8 +4924,8 @@ export const users = defineContract({ prefix: 'users' }, {
4866
4924
  reshapes values inside the full schema. With `output` alone it is a slice, and
4867
4925
  the full result must fit that schema — `withToolView` refuses the endpoint at
4868
4926
  compile time when it does not.
4869
- - **Exposure.** A view needs a tool transport. With `createContractFactory({
4870
- toolExposure: 'explicit' })` an endpoint without `expose` is HTTP-only, so an
4927
+ - **Exposure.** A view needs a tool transport. With `createContractFactory<Scope>().configure({
4928
+ toolExposure: { default: ['HTTP'] } })` an endpoint without `expose` is HTTP-only, so an
4871
4929
  endpoint with a view names its tool transports in `expose`.
4872
4930
  - **What it refuses.** `defineContract` rejects a view on an endpoint with no
4873
4931
  tool transport or no full `output`, on a raw, `rawBody`, multipart,
@@ -15141,9 +15199,9 @@ Notes for a Node host:
15141
15199
  - **Socket.IO** attaches to the Node HTTP server via `serveNode({ socket })`, and
15142
15200
  on Node the default transport is `['websocket']` — set the client to match
15143
15201
  (`transports: ['websocket']`). See [realtime](./realtime.md).
15144
- - **Bun-only helpers** do not run on Node: `serveFile` (uses `Bun.file`) and the
15145
- raw WebSocket lane. `staticRoute` is runtime-neutral (`node:fs`) and works on
15146
- both, but in production prefer a CDN / the static front-end below.
15202
+ - **Bun-only helpers** do not run on Node: the raw WebSocket lane. `serveFile`
15203
+ and `staticRoute` work on both — on Node the body is a `node:fs` read stream —
15204
+ but in production prefer a CDN / the static front-end below.
15147
15205
 
15148
15206
  ### Production checklist
15149
15207
 
@@ -16937,7 +16995,45 @@ const attribution = await geo.resolve(requestIp);
16937
16995
  `resolve` returns `null` for invalid, loopback, private, link-local and otherwise
16938
16996
  non-public addresses without querying the reader. A database miss or lookup
16939
16997
  failure also returns `null`; `onError` receives operational failures. A result
16940
- may include country, region, city, postal, coordinates, timezone and ASN fields.
16998
+ is a sparse `GeoAttribution`: absent facts have no own property, not `null` or
16999
+ an own `undefined` value. City and ASN can be present independently. The default
17000
+ mapper preserves the complete City + ASN place snapshot:
17001
+
17002
+ | Fields | Meaning |
17003
+ |--------|---------|
17004
+ | `continentCode`, `continentName`, `continentGeonameId` | continent code, English name and GeoNames identifier |
17005
+ | `countryCode`, `countryName`, `countryGeonameId` | the country the database locates the address in; absent when the record locates none |
17006
+ | `registeredCountryCode`, `registeredCountryName`, `registeredCountryGeonameId` | the country the address block is registered to, a separate fact |
17007
+ | `regionCode`, `regionName`, `regionGeonameId` | first subdivision |
17008
+ | `city`, `cityGeonameId`, `postalCode` | city and postal facts; no city is required |
17009
+ | `latitude`, `longitude`, `timezone`, `accuracyRadiusKm` | coordinates, timezone and accuracy radius in kilometres; zero is a value |
17010
+ | `autonomousSystemNumber`, `autonomousSystemOrganization` | finite autonomous system number and organisation |
17011
+ | `localizedNames` | every available language under optional `continent`, `country`, `registeredCountry`, `region` and `city` dictionaries |
17012
+
17013
+ No locale is synthesised: without English names, localized dictionaries make a
17014
+ meaningful result, but `countryName`, `regionName` or `city` stays absent. The
17015
+ located country is never inferred from the registered one — an application that
17016
+ wants a country for every address chooses `countryCode ?? registeredCountryCode`
17017
+ itself, knowing which fact it used. Empty localized groups are omitted. Each field
17018
+ is validated on its own by the schema: text is nonempty, GeoNames identifiers and
17019
+ `accuracyRadiusKm` are nonnegative safe integers, latitude and longitude are in
17020
+ range. A malformed field is omitted without erasing the other valid facts. Unknown
17021
+ vendor fields are not projected.
17022
+
17023
+ A snapshot carries every language the database has: with the eight GeoLite2
17024
+ languages in all five groups it is 1–1.5 KB of JSON. An application that stores
17025
+ one per visit and needs fewer languages maps the result to its own shape.
17026
+
17027
+ `GeoAttributionSchema` is the strict, readonly Zod owner of the DTO. It lives in
17028
+ the browser-safe `stitchkit/geo/contract`, so a shared contract validates a stored
17029
+ or transported snapshot without the server-only resolver. Every field is optional
17030
+ and readonly; use `.unwrap()` before composing the underlying object schema. The
17031
+ mapper copies localized dictionaries, so later reader mutations do not change a
17032
+ snapshot. An empty or unrecognised record returns `null`.
17033
+
17034
+ An explicit `map` callback receives the raw reader result and owns its
17035
+ answer. The resolver does not apply `GeoAttributionSchema` after a custom mapper,
17036
+ strip its application fields or replace its returned object.
16941
17037
 
16942
17038
  When a reload succeeds, new lookups move to the new generation immediately.
16943
17039
  The previous reader closes only after its in-flight lookups finish.
@@ -17048,6 +17144,124 @@ charts, no merged cells and no streaming: an application that needs them takes a
17048
17144
 
17049
17145
  # Upgrading stitchkit
17050
17146
 
17147
+ ## Released migration: 0.119.0
17148
+
17149
+ ### factory-default-exposure
17150
+
17151
+ **Who must act:** code passing arguments to `createContractFactory`, depending on its fixed-string
17152
+ policy types or the renamed exposure types, calling HTTP clients of endpoints typed as the broad
17153
+ `EndpointDef` / `FactoryScopedEndpoint`, or declaring multipart endpoints with non-HTTP exposure or
17154
+ tool options.
17155
+
17156
+ Scope and default transports are inferred separately. Replace the constructor configuration:
17157
+
17158
+ ```ts
17159
+ // before
17160
+ const { defineContract } = createContractFactory<AppScope>({ toolExposure: 'explicit' })
17161
+ // after: preserve the same curated HTTP-only default
17162
+ const { defineContract } = createContractFactory<AppScope>().configure({
17163
+ toolExposure: { default: ['HTTP'] },
17164
+ })
17165
+ ```
17166
+
17167
+ To deliberately expose ordinary operations through HTTP and CLI, choose `default: ['HTTP', 'CLI']`
17168
+ and remove redundant matching endpoint arrays. Explicit arrays replace, rather than extend, the
17169
+ default. Raw responses, multipart, raw bodies, response metadata, response streams and HEAD get only
17170
+ HTTP from implicit defaults; a CLI-only or empty default leaves them hidden on every transport.
17171
+ Explicit contradictions fail at definition; remove non-HTTP transports and tool options from multipart
17172
+ endpoints rather than relying on their silent omission from tool catalogs.
17173
+
17174
+ `ExplicitToolExposureEndpoints` and `ExplicitScopedDefineContract` are renamed
17175
+ `ConfiguredToolExposureEndpoints<Endpoints, Defaults>` and `ConfiguredScopedDefineContract<Scope, Defaults>`:
17176
+ they describe materialization from any default tuple, their second generic, and HTTP-only when it is
17177
+ omitted.
17178
+ An optional or conditional endpoint override remains an alternative in the returned type. It does not
17179
+ guarantee an HTTP client method unless every alternative includes HTTP.
17180
+
17181
+ HTTP clients and URL builders no longer assume a widened `Transport[]` contains
17182
+ HTTP. Pass the literal array directly to `configure` when the HTTP surface is static;
17183
+ its const generic infers the tuple. An annotation or `satisfies ContractFactoryConfig`
17184
+ on a separately stored configuration does not preserve the tuple by itself.
17185
+ Optional capabilities likewise retain the filtered alternative in endpoint types.
17186
+
17187
+ The rule reads the endpoint's type, with or without a factory. An endpoint returned by a helper
17188
+ annotated `(): EndpointDef` or `(): FactoryScopedEndpoint<Scope>`, or holding a stored
17189
+ `readonly Transport[]`, has no HTTP client method or URL, because that type allows an exposure
17190
+ without HTTP. Keep the literal type with a const generic helper:
17191
+
17192
+ ```ts
17193
+ // before
17194
+ const read = (): FactoryScopedEndpoint<AppScope> => ({ method: 'GET', path: '/', desc: 'Read', output })
17195
+ // after
17196
+ const endpoint = <const Endpoint extends FactoryScopedEndpoint<AppScope>>(value: Endpoint) => value
17197
+ const read = () => endpoint({ method: 'GET', path: '/', desc: 'Read', output })
17198
+ ```
17199
+
17200
+ If every endpoint already has an explicit array, removing the constructor configuration alone also
17201
+ preserves exposure. [ADR 0283](../decisions/0283-factory-default-exposure-is-materialized-by-capability.md).
17202
+
17203
+ Search patterns: `createContractFactory`, `toolExposure`, `ExplicitToolExposureEndpoints`,
17204
+ `ExplicitScopedDefineContract`, `: EndpointDef`, `: FactoryScopedEndpoint`, and multipart endpoints
17205
+ with `expose` or `tool`.
17206
+
17207
+ ### geo-full-snapshot
17208
+
17209
+ **Who must act:** code comparing `stitchkit/geo` results by equality, parsing them with a strict
17210
+ schema of its own, mutating a returned `GeoAttribution`, or relying on empty labels.
17211
+
17212
+ The default mapper returns everything the City + ASN record states: continent, the registered
17213
+ country as its own fields, GeoNames identifiers, `accuracyRadiusKm` and `localizedNames` with every
17214
+ language. A record that used to give `{ city: 'Bangkok' }` now also gives
17215
+ `localizedNames: { city: { en: 'Bangkok' } }`.
17216
+
17217
+ ```ts
17218
+ // before: an application's copy of the shape
17219
+ const StoredGeo = z.strictObject({ city: z.string().optional(), countryCode: z.string().optional() })
17220
+ // after: the framework's schema, importable from shared code
17221
+ import { GeoAttributionSchema } from 'stitchkit/geo/contract'
17222
+ ```
17223
+
17224
+ `GeoAttribution` is readonly to its nested dictionaries; copy before changing a field. Empty labels
17225
+ and coordinates outside ±90° / ±180° are omitted. The located country never borrows the registered
17226
+ one; write `countryCode ?? registeredCountryCode` where a country for every address is wanted.
17227
+
17228
+ Search pattern: `stitchkit/geo`, `mapGeoIpRecord`, `GeoAttribution`, `resolve(`.
17229
+
17230
+ ### raw-tracking-attribution
17231
+
17232
+ **Who must act:** code passing `referrerMap` or `hostname` to `resolveAttribution`, importing
17233
+ `ReferrerRule` or `parseReferrer`, relying on inferred, lowercased or retained UTM, or reading `utm`
17234
+ from an event batch on the server.
17235
+
17236
+ Tracking sends raw attribution facts. Remove `referrerMap`, `ReferrerRule`, `parseReferrer` and the
17237
+ resolver's `hostname`; `TrackingPageContext` and `browserTrackingHost()` no longer require or return
17238
+ `hostname` either. Classify `referrer` and campaign tags on your server instead. Configure the
17239
+ click-marker names your application recognises:
17240
+
17241
+ ```ts
17242
+ // before
17243
+ createTrackingClient({ ...config, referrerMap: [{ pattern: /search\.example/, source: 'search', medium: 'organic' }] })
17244
+ // after
17245
+ createTrackingClient({ ...config, clickIdParams: ['gclid', 'msclkid'] })
17246
+ ```
17247
+
17248
+ Bootstrap and both touches carry `clickId`, the configured parameter's name without its value. Extend
17249
+ your bootstrap DTO or stored attribution shape if you previously copied the framework's fields. No
17250
+ marker is recognised unless configured. Real UTM retains its case and whitespace, and a tag without
17251
+ `utm_source` is no longer discarded. Keep any channel normalisation in the server classifier.
17252
+
17253
+ `currentTouch` is the current capture's context; an untagged entry has no campaign even when a prior
17254
+ one did. Implement last-non-direct retention on the server if required. First-touch storage uses a
17255
+ versioned format and resets unversioned records once, because inferred and real tags in those records
17256
+ cannot be distinguished. Without browser storage, current landing facts still reach bootstrap.
17257
+
17258
+ An event batch (`TrackEventsRequest`, the `request` schema of `createTrackingSchemas`) carries
17259
+ `buildId` and `events` only. A server that read `request.utm` attributes events through their visit:
17260
+ store the bootstrap entry's `utm`, `referrer` and `clickId` with the visit and join on `visitId`.
17261
+
17262
+ Search pattern: `referrerMap`, `ReferrerRule`, `parseReferrer`, `resolveAttribution`, `currentTouch`,
17263
+ `request.utm`, `input.utm`.
17264
+
17051
17265
  ## Released migration: 0.118.0
17052
17266
 
17053
17267
  ### telegram-due-cursor
@@ -22334,11 +22548,11 @@ from the root `stitchkit`.
22334
22548
  | Export | Kind | Summary |
22335
22549
  |--------|------|---------|
22336
22550
  | `defineContract` | function | declare a contract — [guide](../guide/contracts.md#definecontract) |
22551
+ | `ConfiguredScopedDefineContract` | _type_ | scoped authoring parameterized by the exact default transport tuple; explicit endpoint exposure replaces it |
22552
+ | `ConfiguredToolExposureEndpoints` | _type_ | endpoint map with materialized exposure; a second generic selects defaults (HTTP when omitted), preserving explicit, optional and conditional overrides |
22337
22553
  | `createContractFactory` | function | a `defineContract` whose scope — on the contract **and** on any endpoint override — is required and held to your union, retaining each concrete literal — [guide](../guide/contracts.md#scope) |
22338
- | `ContractFactoryConfig` | _type_ | optional scoped-factory policy, including explicit tool exposure |
22339
- | `ContractFactoryToolExposure` | _type_ | `'explicit'` — omitted endpoint exposure materializes as HTTP-only |
22340
- | `ExplicitScopedDefineContract` | _type_ | scoped factory authoring with explicit tool opt-in |
22341
- | `ExplicitToolExposureEndpoints` | _type_ | endpoint map after missing exposure is materialized as `['HTTP']` |
22554
+ | `ContractFactoryConfig` | _type_ | configuration passed to `createContractFactory<Scope>().configure({ toolExposure: { default: [...] } })`; the constructor itself takes no arguments |
22555
+ | `ContractFactoryToolExposure` | _type_ | `default: readonly Transport[]`; omitted endpoint exposure uses the immutable policy snapshot, intersected with intrinsic HTTP-only capabilities |
22342
22556
  | `FactoryScopedEndpoint` | _type_ | an endpoint authored through the factory — its own `scope` override is held to the same union |
22343
22557
  | `ScopedContractDef` | _type_ | a factory-defined contract whose `meta.scope` is the required concrete literal |
22344
22558
  | `ScopedDefineContract` | _type_ | the `defineContract` `createContractFactory` returns — endpoint `scope` overrides join the union |
@@ -22558,7 +22772,8 @@ Also re-exports the error helpers from `stitchkit/contract`.
22558
22772
  | `KeyedServices` | _type_ | registry result: the mount-ordered `ServiceDef[]` carrying the same services under `.byKey` |
22559
22773
  | `RegistryHandlers` | _type_ | exact backend handler registry inferred from a contract registry |
22560
22774
  | `ExactRegistryHandlers` | _type_ | fail-first handler shape that rejects extra registry and endpoint keys |
22561
- | `staticRoute` | function | a raw route that serves a directory |
22775
+ | `staticRoute` | function | a raw route that serves a contained directory through `serveFile` (`Range` / `304` / validators) |
22776
+ | `StaticRouteOptions` | _type_ | `{ cacheControl }` — what `staticRoute` adds to every file it serves |
22562
22777
  | `serveFile` | function | serve a file with `Range` / `304` / `HEAD` — [guide](../guide/server.md#serving-files--range-requests) |
22563
22778
  | `parseByteRange` | function | parse a single `Range` header → range / `unsatisfiable` / `null` |
22564
22779
  | `isWithinDir` | function | path containment — `(root, resolvedTarget) => boolean`; call it before `serveFile` on any URL-derived path |
@@ -23889,7 +24104,7 @@ runtime-tool runner, plus deliberate raw MCP adapters over the same mechanics.
23889
24104
  | `AsyncOperationOutputCapability` | _type_ | optional result/artifact schema plus handler |
23890
24105
  | `RuntimeAsyncOperationConfig` | _type_ | runtime-only descriptor configuration |
23891
24106
  | `RuntimeAsyncOperation` | _type_ | inferred generated definitions and schemas |
23892
- | `AsyncOperationContractConfig` | _type_ | canonical contract config where start returns the operation id; optional `expose` names the transports per capability, because this contract is built inside the framework and never passes through a consumer's `toolExposure: 'explicit'` factory |
24107
+ | `AsyncOperationContractConfig` | _type_ | canonical contract config where start returns the operation id; optional `expose` names the transports per capability, because this contract is built inside the framework and never passes through a consumer's configured factory |
23893
24108
  | `AsyncOperationContractWithStartOutputConfig` | _type_ | canonical contract config with an application start envelope and typed id extractor |
23894
24109
  | `DefinedAsyncOperationContract` | _type_ | generated contract, capability keys, schemas and parsed adapters |
23895
24110
  | `ContractAsyncOperationConfig` | _type_ | literal contract method binding and handlers |
@@ -24302,7 +24517,7 @@ shapes are `stitchkit/tracking/contract`, the server half is
24302
24517
  | Export | Kind | Summary |
24303
24518
  |--------|------|---------|
24304
24519
  | `createTrackingClient` | function | the client: visit lease and renewal, pending queue, synchronous identity from reserved sequences, outbox and flush lease, delivery, the string-body page-leave beacon plus a queued copy, visible time, scroll milestones, declarative clicks, heartbeats |
24305
- | `TrackingClient` / `TrackingClientConfig` | _type_ | `track` / `onNavigate` / `start` / `visitId` / `browserStreamId` / `attribution`, and the host, `buildId`, `builtin` names, `bootstrap`, `deliver`, optional `decorate` (your `eventExtras` on every minted event), `unload` / `unloadUrl`, `outbox`, `onUnauthorized`, `onVisit`, `onFailure`, `isAction`, `referrerMap`, `clickAttributes`, `hrefPolicy` (`path-only` by default; `full` retains click query/fragment before `decorate`), `scrollMilestones`, `batchSize`, periods and limits |
24520
+ | `TrackingClient` / `TrackingClientConfig` | _type_ | `track` / `onNavigate` / `start` / `visitId` / `browserStreamId` / `attribution`, and the host, `buildId`, `builtin` names, `bootstrap`, `deliver`, optional `decorate` (your `eventExtras` on every minted event), `unload` / `unloadUrl`, `outbox`, `onUnauthorized`, `onVisit`, `onFailure`, `isAction`, `clickIdParams` (ordered application-defined marker names; no values retained), `clickAttributes`, `hrefPolicy` (`path-only` by default; `full` retains click query/fragment before `decorate`), `scrollMilestones`, `batchSize`, periods and limits |
24306
24521
  | `TrackFn` / `EventsWithMetadata` / `EventsWithoutMetadata` | _type_ | `track(type)` / `track(type, metadata)` typed by the application's metadata map, and the two halves of that map |
24307
24522
  | `BuiltinTrackingEventTypes` / `CONVENTIONAL_TRACKING_EVENT_TYPES` | _type_ / const | the application's names for the events the client emits itself; the conventional `PAGE_VIEW` … `INTERACTION` set |
24308
24523
  | `browserTrackingHost` / `TrackingHost` / `TrackingPageContext` | function / _type_ | everything the client takes from a browser tab behind one interface — page context, visibility, scroll depth, events, intervals, clocks, storage — so a test or a non-tab host can supply its own |
@@ -24315,8 +24530,8 @@ shapes are `stitchkit/tracking/contract`, the server half is
24315
24530
  | `createVisibleTimeMeter` / `VisibleTimeMeter` / `VisibleInterval` / `VisibleHeartbeat` / `VisibleTimeMeterOptions` | function / _type_ | additive visible time — `checkpoint` / `cut` / `heartbeat`, each interval with its own id; a hidden heartbeat carries no interval |
24316
24531
  | `scrollDepthPercent` / `createScrollMilestones` / `ScrollMilestones` | function / _type_ | depth as a percentage of the scrollable range; milestones fired once each per page |
24317
24532
  | `createOncePerPage` / `OncePerPage` | function / _type_ | one fact per key per page within a window — the guard a twice-mounted component cannot keep itself |
24318
- | `resolveAttribution` / `ResolveAttributionInput` / `ResolvedAttribution` / `AttributionStorage` / `ReferrerRule` | function / _type_ | first-touch once, current-touch on every new UTM; the referrer → source/medium map is the application's, there is no built-in list |
24319
- | `parseUtmFromSearch` / `parseReferrer` | function | the two parsers `resolveAttribution` is built from |
24533
+ | `resolveAttribution` / `ResolveAttributionInput` / `ResolvedAttribution` / `AttributionStorage` | function / _type_ | persisted first-touch with TTL, raw current-touch on each capture; actual UTM, referrer and optional `clickId` name from `clickIdParams`; `storage: null` collects without persistence; no channel inference |
24534
+ | `parseUtmFromSearch` | function | reads only `utm_*`, preserving case and whitespace up to 200 characters per field; any tag is sufficient, no source required |
24320
24535
  | `redactTrackingText` / `redactTrackingValue` | functions | mask passwords, sign-in credentials and signed launch data in text or a value of any shape; the key stays, and navigation and campaign parameters are untouched |
24321
24536
  | `resolveTrackedClick` / `ResolveTrackedClickOptions` / `TrackedClick` / `TrackedClickAttributes` / `ClickTarget` | function / _type_ | `data-track` → click, `data-track-action` → interaction, a link to another origin → outbound; attribute names configurable; `hrefPolicy: 'full'` retains query/fragment (default `path-only` omits them), URL credentials excluded; `leavesPage` says which path to send on |
24322
24537
 
@@ -24332,7 +24547,7 @@ browser code that only sends events does not import it. → ADR 0280
24332
24547
  | `createTrackingContract` / `TrackingContractConfig` / `TrackingContractEndpoints` | function / _type_ | `bootstrap` (`POST /visit`) and `track` (`POST /events`, `safelistedBody`) from the application's event types, scope and optional extras |
24333
24548
  | `createTrackingSchemas` / `TrackingSchemas` / `TrackingSchemasConfig` | function / _type_ | every schema of one surface — `event`, `request`, `response`, `disposition`, `entry`, `bootstrap` — from `eventTypes`, optional `eventExtras`, `buildIdPattern`, `maxEventsPerBatch` |
24334
24549
  | `TrackingEventEnvelope` / `TrackingEventShape` | _type_ | `eventId`, `visitId`, `browserStreamId`, `browserSequence`, `type`, `page`, `metadata`, `clientTimestamp` |
24335
- | `TrackEventsRequest` / `TrackEventsResponse` / `TrackEventsResponseSchema` | _type_ / schema | a batch (`buildId`, `events`, `utm`) and its answer (`accepted`, `dispositions`) |
24550
+ | `TrackEventsRequest` / `TrackEventsResponse` / `TrackEventsResponseSchema` | _type_ / schema | a batch (`buildId`, `events`; attribution travels with the visit bootstrap) and its answer (`accepted`, `dispositions`) |
24336
24551
  | `TrackingDisposition` / `TrackingDispositionSchema` / `TrackingDispositionStatus` / `TrackingDispositionStatusSchema` | _type_ / schema | `accepted` / `duplicate` / `identity-invalid` / `excluded-bot` — every one terminal |
24337
24552
  | `VisitEntryContext` / `VisitEntryContextShape` / `VisitBootstrapResponse` / `VisitBootstrapResponseSchema` | _type_ / schema | what a browser reports to open a visit (lineage, landing, referrer, UTM, display, screen, `buildId`, outbox health) and the lease it gets back |
24338
24553
  | `TrackingOutboxState` / `TrackingOutboxStateSchema` | _type_ / schema | `available` / `unavailable` |
@@ -24423,7 +24638,15 @@ adapter is used.
24423
24638
  | `createGeoIpResolver` / `GeoIpResolver` / `GeoIpResolverOptions` / `GeoIpSnapshot` | function / _type_ | public-IP lookup over one stable reader generation; unavailable start, last-known-good degraded reload and cleanup are explicit states |
24424
24639
  | `createMaxMindGeoIpLoader` / `MaxMindGeoIpLoaderOptions` / `MaxMindGeoIpPaths` | function / _type_ | lazy optional-peer City+ASN loader with stable before/after file revision checks |
24425
24640
  | `GeoIpReader` / `GeoIpReaderLoader` | _type_ | peer-neutral reader/generation seam for adapters and tests |
24426
- | `GeoAttribution` / `mapGeoIpRecord` | _type_ / function | generic optional place and autonomous-system projection; application-specific fields remain outside |
24641
+ | `GeoAttribution` / `mapGeoIpRecord` | _type_ / function | strict readonly sparse City + ASN snapshot: continent, located and registered country as separate facts, GeoNames IDs, first region/city/postal, coordinates/timezone, `accuracyRadiusKm`, autonomous system and all source languages in five `localizedNames` groups; each malformed field is omitted on its own; custom resolver mappers remain app-owned — [guide](../guide/geo.md#lookups) |
24642
+
24643
+ ## `stitchkit/geo/contract`
24644
+
24645
+ The place snapshot's schema, browser-safe, for a shared contract. Brings `zod`.
24646
+
24647
+ | Export | Kind | Summary |
24648
+ |--------|------|---------|
24649
+ | `GeoAttributionSchema` / `GeoAttribution` | schema / _type_ | the strict readonly Zod owner of the snapshot `stitchkit/geo` resolves — [guide](../guide/geo.md#lookups) |
24427
24650
 
24428
24651
  ---
24429
24652
 
package/llms.txt CHANGED
@@ -9,7 +9,7 @@ Build with stitchkit: define a contract once, then `implement` it and serve it (
9
9
  Load the slice for each entrypoint your code imports — `import … from 'stitchkit/server'` → `llms/server.txt`; the root `stitchkit` import → `llms/stitchkit.txt`. A slice holds that entrypoint's API reference section and the guide pages whose home it is; every guide lives in exactly one slice, and a slice that needs another's guide names it under "Also read". Each file is at most 50.0 KB; a longer one continues in numbered parts, each naming the next at its top. Moving across versions: load `llms/upgrading.txt` and the range slices it lists between your version and the target. `llms-full.txt` inlines everything; it is far larger than a context window, so do not read it whole.
10
10
 
11
11
  ## Entrypoint slices
12
- - **stitchkit**: [llms/stitchkit.txt](llms/stitchkit.txt) 43.7 KB → [llms/stitchkit.2.txt](llms/stitchkit.2.txt) 43.1 KB
12
+ - **stitchkit**: [llms/stitchkit.txt](llms/stitchkit.txt) 43.9 KB → [llms/stitchkit.2.txt](llms/stitchkit.2.txt) 43.1 KB
13
13
  - **stitchkit/live**: [llms/live.txt](llms/live.txt) 21.2 KB
14
14
  - **stitchkit/react**: [llms/react.txt](llms/react.txt) 8.7 KB
15
15
  - **stitchkit/react/keyboard**: [llms/react-keyboard.txt](llms/react-keyboard.txt) 22.7 KB
@@ -22,9 +22,9 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
22
22
  - **stitchkit/cli**: [llms/cli.txt](llms/cli.txt) 36.5 KB → [llms/cli.2.txt](llms/cli.2.txt) 36.4 KB
23
23
  - **stitchkit/cli/publish**: [llms/cli-publish.txt](llms/cli-publish.txt) 1.5 KB
24
24
  - **stitchkit/remote**: [llms/remote.txt](llms/remote.txt) 1.0 KB
25
- - **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 31.9 KB
25
+ - **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 33.8 KB
26
26
  - **stitchkit/primitives**: [llms/primitives.txt](llms/primitives.txt) 13.2 KB
27
- - **stitchkit/server**: [llms/server.txt](llms/server.txt) 44.2 KB → [llms/server.2.txt](llms/server.2.txt) 20.5 KB → [llms/server.3.txt](llms/server.3.txt) 47.0 KB → [llms/server.4.txt](llms/server.4.txt) 38.6 KB → [llms/server.5.txt](llms/server.5.txt) 22.9 KB
27
+ - **stitchkit/server**: [llms/server.txt](llms/server.txt) 44.2 KB → [llms/server.2.txt](llms/server.2.txt) 20.9 KB → [llms/server.3.txt](llms/server.3.txt) 47.0 KB → [llms/server.4.txt](llms/server.4.txt) 38.6 KB → [llms/server.5.txt](llms/server.5.txt) 23.1 KB
28
28
  - **stitchkit/observability**: [llms/observability.txt](llms/observability.txt) 46.9 KB
29
29
  - **stitchkit/agent-runtime**: [llms/agent-runtime.txt](llms/agent-runtime.txt) 44.3 KB → [llms/agent-runtime.2.txt](llms/agent-runtime.2.txt) 46.7 KB → [llms/agent-runtime.3.txt](llms/agent-runtime.3.txt) 43.4 KB
30
30
  - **stitchkit/agent-runtime/testing**: [llms/agent-runtime-testing.txt](llms/agent-runtime-testing.txt) 2.6 KB
@@ -47,12 +47,13 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
47
47
  - **stitchkit/telegram/grammy**: [llms/telegram-grammy.txt](llms/telegram-grammy.txt) 1.2 KB
48
48
  - **stitchkit/telegram/html**: [llms/telegram-html.txt](llms/telegram-html.txt) 2.7 KB
49
49
  - **stitchkit/telegram/screens**: [llms/telegram-screens.txt](llms/telegram-screens.txt) 21.2 KB
50
- - **stitchkit/tracking**: [llms/tracking.txt](llms/tracking.txt) 20.5 KB
50
+ - **stitchkit/tracking**: [llms/tracking.txt](llms/tracking.txt) 22.3 KB
51
51
  - **stitchkit/release**: [llms/release.txt](llms/release.txt) 8.9 KB
52
52
  - **stitchkit/tracking/contract**: [llms/tracking-contract.txt](llms/tracking-contract.txt) 2.8 KB
53
53
  - **stitchkit/tracking/performance**: [llms/tracking-performance.txt](llms/tracking-performance.txt) 1.1 KB
54
54
  - **stitchkit/tracking/server**: [llms/tracking-server.txt](llms/tracking-server.txt) 2.7 KB
55
- - **stitchkit/geo**: [llms/geo.txt](llms/geo.txt) 4.1 KB
55
+ - **stitchkit/geo**: [llms/geo.txt](llms/geo.txt) 7.0 KB
56
+ - **stitchkit/geo/contract**: [llms/geo-contract.txt](llms/geo-contract.txt) 0.8 KB
56
57
  - **stitchkit/xlsx**: [llms/xlsx.txt](llms/xlsx.txt) 4.5 KB
57
58
  - **stitchkit/voice**: [llms/voice.txt](llms/voice.txt) 5.2 KB
58
59
  - **stitchkit/oauth**: [llms/oauth.txt](llms/oauth.txt) 6.7 KB
@@ -69,11 +70,11 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
69
70
 
70
71
  ## Upgrading slices
71
72
  - **Upgrading — how to move across versions**: [llms/upgrading.txt](llms/upgrading.txt) 12.7 KB
72
- - **Upgrading 0.105.0 → 0.118.0**: [llms/upgrading-0.105.0-0.118.0.txt](llms/upgrading-0.105.0-0.118.0.txt) 43.0 KB
73
- - **Upgrading 0.84.0 → 0.104.0**: [llms/upgrading-0.84.0-0.104.0.txt](llms/upgrading-0.84.0-0.104.0.txt) 46.1 KB
74
- - **Upgrading 0.63.0 → 0.83.0**: [llms/upgrading-0.63.0-0.83.0.txt](llms/upgrading-0.63.0-0.83.0.txt) 44.0 KB
75
- - **Upgrading 0.44.0 → 0.62.0**: [llms/upgrading-0.44.0-0.62.0.txt](llms/upgrading-0.44.0-0.62.0.txt) 46.5 KB
76
- - **Upgrading 0.37.0 → 0.44.0**: [llms/upgrading-0.37.0-0.44.0.txt](llms/upgrading-0.37.0-0.44.0.txt) 14.7 KB
73
+ - **Upgrading 0.106.0 → 0.119.0**: [llms/upgrading-0.106.0-0.119.0.txt](llms/upgrading-0.106.0-0.119.0.txt) 28.8 KB
74
+ - **Upgrading 0.96.0 → 0.105.0**: [llms/upgrading-0.96.0-0.105.0.txt](llms/upgrading-0.96.0-0.105.0.txt) 45.8 KB
75
+ - **Upgrading 0.69.0 → 0.95.0**: [llms/upgrading-0.69.0-0.95.0.txt](llms/upgrading-0.69.0-0.95.0.txt) 46.6 KB
76
+ - **Upgrading 0.55.0 → 0.68.0**: [llms/upgrading-0.55.0-0.68.0.txt](llms/upgrading-0.55.0-0.68.0.txt) 47.0 KB
77
+ - **Upgrading 0.37.0 → 0.53.0**: [llms/upgrading-0.37.0-0.53.0.txt](llms/upgrading-0.37.0-0.53.0.txt) 32.6 KB
77
78
 
78
79
  ## Guide (web)
79
80
  - [Getting started](https://github.com/max-listov/stitchkit/blob/master/docs/guide/getting-started.md): install, entrypoints, and a first contract → server → client app