stitchkit 0.100.3 → 0.101.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 (70) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/dist/browser/chunked-upload.d.ts +80 -0
  3. package/dist/browser/chunked-upload.d.ts.map +1 -0
  4. package/dist/browser/client-executors.d.ts.map +1 -1
  5. package/dist/browser/client.d.ts.map +1 -1
  6. package/dist/browser/http.d.ts +3 -0
  7. package/dist/browser/http.d.ts.map +1 -1
  8. package/dist/browser/upload-progress.d.ts +26 -0
  9. package/dist/browser/upload-progress.d.ts.map +1 -0
  10. package/dist/contract/client-types.d.ts +19 -0
  11. package/dist/contract/client-types.d.ts.map +1 -1
  12. package/dist/entrypoints/application.js +2 -1
  13. package/dist/entrypoints/contract.d.ts +1 -1
  14. package/dist/entrypoints/contract.d.ts.map +1 -1
  15. package/dist/entrypoints/files.d.ts +1 -0
  16. package/dist/entrypoints/files.d.ts.map +1 -1
  17. package/dist/entrypoints/files.js +230 -0
  18. package/dist/entrypoints/index.d.ts +1 -0
  19. package/dist/entrypoints/index.d.ts.map +1 -1
  20. package/dist/entrypoints/index.js +90 -2
  21. package/dist/entrypoints/observability.js +2 -1
  22. package/dist/entrypoints/react.js +1 -1
  23. package/dist/entrypoints/remote.js +2 -2
  24. package/dist/entrypoints/telegram/screens.js +1 -1
  25. package/dist/entrypoints/telegram.d.ts +8 -4
  26. package/dist/entrypoints/telegram.d.ts.map +1 -1
  27. package/dist/entrypoints/telegram.js +277 -49
  28. package/dist/entrypoints/testing.js +2 -2
  29. package/dist/entrypoints/tools.js +2 -1
  30. package/dist/entrypoints/tracking.js +1 -1
  31. package/dist/files/chunk-spool.d.ts +66 -0
  32. package/dist/files/chunk-spool.d.ts.map +1 -0
  33. package/dist/{index-aaj97r4y.js → index-65jwqhcf.js} +27 -10
  34. package/dist/{index-6k1svmjh.js → index-e05cjgtf.js} +16 -13
  35. package/dist/{index-kzz6rq0w.js → index-e6c2spsd.js} +56 -18
  36. package/dist/{index-qjeggp8w.js → index-h43cepag.js} +139 -11
  37. package/dist/index-sznqf8fx.js +20 -0
  38. package/dist/internal/secret-shapes.d.ts +21 -0
  39. package/dist/internal/secret-shapes.d.ts.map +1 -0
  40. package/dist/observability/sanitize.d.ts.map +1 -1
  41. package/dist/telegram/bot-api.d.ts +7 -4
  42. package/dist/telegram/bot-api.d.ts.map +1 -1
  43. package/dist/telegram/init-data.d.ts +3 -44
  44. package/dist/telegram/init-data.d.ts.map +1 -1
  45. package/dist/telegram/operator-channel.d.ts +1 -18
  46. package/dist/telegram/operator-channel.d.ts.map +1 -1
  47. package/dist/telegram/operator-dedupe.d.ts +44 -0
  48. package/dist/telegram/operator-dedupe.d.ts.map +1 -0
  49. package/dist/telegram/send-failure.d.ts +6 -0
  50. package/dist/telegram/send-failure.d.ts.map +1 -1
  51. package/dist/telegram/update-intake.d.ts +5 -1
  52. package/dist/telegram/update-intake.d.ts.map +1 -1
  53. package/dist/telegram/update-store-check.d.ts +22 -0
  54. package/dist/telegram/update-store-check.d.ts.map +1 -0
  55. package/dist/telegram/update-store-postgres.d.ts +33 -0
  56. package/dist/telegram/update-store-postgres.d.ts.map +1 -0
  57. package/dist/telegram/user.d.ts +51 -0
  58. package/dist/telegram/user.d.ts.map +1 -0
  59. package/llms/files.txt +3 -0
  60. package/llms/observability.txt +13 -1
  61. package/llms/server.4.txt +16 -0
  62. package/llms/stitchkit.2.txt +157 -2
  63. package/llms/stitchkit.3.txt +5 -2
  64. package/llms/stitchkit.txt +3 -3
  65. package/llms/telegram.txt +91 -13
  66. package/llms/{upgrading-0.74.0-0.100.0.txt → upgrading-0.74.0-0.101.0.txt} +39 -3
  67. package/llms/upgrading.txt +1 -1
  68. package/llms-full.txt +324 -21
  69. package/llms.txt +6 -6
  70. package/package.json +1 -1
package/llms-full.txt CHANGED
@@ -70,12 +70,12 @@ own, recorded as an ADR.
70
70
  | `stitchkit/geo` | server (Bun or Node) | evolving | managed GeoIP reader generations, last-known-good reload and the optional MaxMind adapter |
71
71
  | `stitchkit/oauth` | browser **and** server | evolving | provider-neutral Authorization Code + PKCE request and one-shot callback transaction mechanics |
72
72
  | `stitchkit/google` | server (Bun or Node) | evolving | optional Google code exchange and verified OIDC identity adapter |
73
- | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 45 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
73
+ | `stitchkit/observability` | server | stable<br>_redefined in 2 of the 46 minors since 0.56.2, most recently 0.92.0_ | request/tool event projections — `createObservability`, trace context, sanitisation |
74
74
  | `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the managed-resource conformance kit |
75
75
  | `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
76
76
  | `stitchkit/react` | browser + server rendering | stable | `createCursorQuery`, `createCacheBridge`, QueryClient and `ApiError` retry policy |
77
77
  | `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) |
78
- | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 22 of the 45 minors since 0.56.2, most recently 0.97.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
78
+ | `stitchkit/agent-runtime` | server | evolving<br>_redefined in 22 of the 46 minors since 0.56.2, most recently 0.97.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
79
79
  | `stitchkit/agent-runtime/sandbox` | server | evolving | optional Linux Bubblewrap sessions, durable workspaces and a host HTTP credential gateway; see [sandbox guide](sandbox.md) |
80
80
  | `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 |
81
81
  | `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
@@ -85,7 +85,7 @@ own, recorded as an ADR.
85
85
  | `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
86
86
  | `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
87
87
  | `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
88
- | `stitchkit/application` | browser + server | evolving<br>_redefined in 10 of the 45 minors since 0.56.2, most recently 0.100.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
88
+ | `stitchkit/application` | browser + server | evolving<br>_redefined in 10 of the 46 minors since 0.56.2, most recently 0.100.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
89
89
  | `stitchkit/application/grammy` | server | evolving | a grammY bot as application resources — configuration, then polling with updates admitted by batch — and the webhook adapter |
90
90
  | `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
91
91
  | `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
@@ -2768,8 +2768,8 @@ non-flat field in a `GET` input, and a missing or invalid multipart file. → AD
2768
2768
  0148.
2769
2769
 
2770
2770
  Abort and timeout do not emit `network_error` and are not retried. The same
2771
- options work for query, JSON, multipart and raw-response calls. Stitchkit does
2772
- not expose upload progress: Fetch has no portable upload-progress primitive.
2771
+ options work for query, JSON, multipart and raw-response calls. A body call
2772
+ also takes `onUploadProgress` — see [Upload progress](#upload-progress).
2773
2773
 
2774
2774
  An injected transport failure is normalized to `UNKNOWN_ERROR` while its exact
2775
2775
  object remains available as `ApiError.cause`. This is how a bounded adapter can
@@ -2784,6 +2784,51 @@ For an endpoint without contract arguments, pass only the options object:
2784
2784
  await api.health.withOptions({ signal: controller.signal })
2785
2785
  ```
2786
2786
 
2787
+ ### Upload progress
2788
+
2789
+ A call with a body can report it leaving:
2790
+
2791
+ ```ts
2792
+ await api.recordings.upload.withOptions(
2793
+ { file, title: 'Take 3' },
2794
+ {
2795
+ signal,
2796
+ onUploadProgress: ({ sentBytes, totalBytes, attempt }) => {
2797
+ bar.value = sentBytes / totalBytes
2798
+ },
2799
+ },
2800
+ )
2801
+ ```
2802
+
2803
+ - `sentBytes` counts bytes of the encoded body handed to the network — not
2804
+ bytes the server accepted. `totalBytes` is the encoded length, multipart
2805
+ boundaries included. The first report is `0`, values only grow, and the last
2806
+ one of an attempt equals `totalBytes`.
2807
+ - A transport retry (`HttpClientConfig.retry`) counts again from `0` under the
2808
+ next `attempt`.
2809
+ - Without the option nothing changes: the request takes the path it always did.
2810
+ - A `GET`, `HEAD`, `DELETE` or streaming endpoint refuses the option with a
2811
+ `TypeError` at the call — it has no body to count, or a live response the
2812
+ browser route would buffer. A listener that throws does not fail the upload.
2813
+
2814
+ Fetch has no upload event, so the count comes from one of two routes, chosen
2815
+ per call:
2816
+
2817
+ | Where the client runs | Route | What counts |
2818
+ |---|---|---|
2819
+ | Bun, Node, SSR, an injected `fetch`, a unix socket | the body is encoded once and sent as a stream of 64 KiB pieces with an exact `content-length` | a piece counts when the transport pulls it |
2820
+ | a browser on its own fetch | `XMLHttpRequest`, with the same headers, credentials, errors, cancellation and timeout | `xhr.upload` progress events |
2821
+
2822
+ The browser route exists because a streaming request body is not portable:
2823
+ Chromium sends one only over HTTP/2 and Safari not at all. Two consequences of
2824
+ XHR: `credentials: 'omit'` is refused (XHR cannot drop same-origin cookies),
2825
+ and the response is read whole before the call resolves. An injected `fetch`
2826
+ always takes the stream route — it owns the I/O, and a transport that buffers
2827
+ the stream before sending reports the whole body at once.
2828
+
2829
+ A counted body is encoded in memory once. A file larger than one request
2830
+ belongs in [chunked uploads](#chunked-uploads), whose parts are sized for it.
2831
+
2787
2832
  ### Injected delivery adapters
2788
2833
 
2789
2834
  `ClientFetch` is the narrow composition seam for application-owned delivery.
@@ -2881,6 +2926,116 @@ Every method now requires `tenantId`, and the callback sees it as a `string`.
2881
2926
  The batch form delegates to the same single-contract client runtime, including
2882
2927
  HTTP exposure filtering, multipart, raw responses and output validation.
2883
2928
 
2929
+ ## Chunked uploads
2930
+
2931
+ A file larger than one request goes as three calls of the application's own
2932
+ contract — open, parts, finish — driven by `uploadInChunks` on the client and
2933
+ held by `createChunkSpool` (`stitchkit/files`) on the server. The contract stays
2934
+ the application's: its fields, its limits, what happens to the finished file.
2935
+
2936
+ ```ts
2937
+ // shared: one part size for both halves
2938
+ export const CHUNK_BYTES = 4 * 1024 * 1024
2939
+
2940
+ // contract
2941
+ init: {
2942
+ method: 'POST', path: '/uploads', desc: 'Open an upload',
2943
+ input: z.object({ uploadId: z.string(), totalBytes: z.number(), chunkCount: z.number(), name: z.string() }),
2944
+ output: z.object({ finished: Accepted.optional() }),
2945
+ },
2946
+ chunk: {
2947
+ method: 'PUT', path: '/uploads/:uploadId/chunks/:index', desc: 'Send one part',
2948
+ params: z.object({ uploadId: z.string(), index: z.coerce.number() }),
2949
+ multipart: { files: { bytes: {} } },
2950
+ output: z.object({ state: z.enum(['stored', 'repeated']) }),
2951
+ },
2952
+ finalize: {
2953
+ method: 'POST', path: '/uploads/:uploadId/finalize', desc: 'Assemble the file',
2954
+ params: z.object({ uploadId: z.string() }),
2955
+ output: Accepted,
2956
+ },
2957
+ ```
2958
+
2959
+ ```ts
2960
+ // server
2961
+ import { createChunkSpool } from 'stitchkit/files'
2962
+
2963
+ const spool = createChunkSpool({
2964
+ directory: env.UPLOAD_SPOOL_DIR,
2965
+ chunkBytes: CHUNK_BYTES,
2966
+ maxFileBytes: 1024 * 1024 * 1024,
2967
+ meta: z.object({ name: z.string() }),
2968
+ })
2969
+
2970
+ implement(contract, {
2971
+ init: async ({ input, ctx }) => {
2972
+ const done = await findFinished(input.uploadId) // the app's own record
2973
+ if (done) return { finished: done }
2974
+ await spool.open({ owner: ctx.user.id, ...input, meta: { name: input.name } })
2975
+ return {}
2976
+ },
2977
+ chunk: async ({ params, files, ctx }) => ({
2978
+ state: await spool.put({ owner: ctx.user.id, ...params, bytes: files.bytes }),
2979
+ }),
2980
+ finalize: async ({ params, ctx }) => {
2981
+ const key = { owner: ctx.user.id, uploadId: params.uploadId }
2982
+ const file = await spool.assemble(key) // file.stream(), file.chunkPaths, file.meta
2983
+ const accepted = await store(file)
2984
+ await spool.discard(key)
2985
+ return accepted
2986
+ },
2987
+ })
2988
+ ```
2989
+
2990
+ ```ts
2991
+ // client — Bun, Node or a browser
2992
+ import { uploadInChunks } from 'stitchkit'
2993
+
2994
+ const accepted = await uploadInChunks({
2995
+ file,
2996
+ chunkBytes: CHUNK_BYTES,
2997
+ signal,
2998
+ onProgress: ({ sentBytes, totalBytes }) => draw(sentBytes / totalBytes),
2999
+ init: async (start, options) => {
3000
+ const opened = await api.uploads.init.withOptions({ ...start, name: file.name }, options)
3001
+ return opened.finished ? { finished: opened.finished } : undefined
3002
+ },
3003
+ chunk: (part, options) => api.uploads.chunk.withOptions(part, options),
3004
+ finalize: (upload, options) => api.uploads.finalize.withOptions(upload, options),
3005
+ })
3006
+ ```
3007
+
3008
+ The rules the two halves share:
3009
+
3010
+ - **The client mints the upload id** (default `crypto.randomUUID()`), so a
3011
+ repeated `init` is the same upload. The spool keys it by `owner` too — the
3012
+ same id from another owner is another upload.
3013
+ - **A part is idempotent.** The spool keeps a size+sha256 receipt per index:
3014
+ the same bytes again answer `'repeated'`, other bytes `UPLOAD_CONFLICT`
3015
+ (409). So the driver repeats `init` and a part on a failure that may pass — no
3016
+ answer (network, timeout), `429`, `5xx` — up to `retries` times (default 3)
3017
+ with a doubling pause from `retryDelayMs` (default 1 s). A refusal is final.
3018
+ - **`finalize` is never repeated.** After it the application owns the file and
3019
+ the parts are gone; a lost answer is recovered by `init` returning
3020
+ `{ finished }`, which the driver hands back without sending a byte.
3021
+ - **Cancellation is checked between parts**, so a cancelled upload never
3022
+ reaches `finalize`.
3023
+ - **Every part but the last is exactly `chunkBytes`** and `chunkCount` must add
3024
+ up; the spool refuses anything else with a 4xx `AppError`
3025
+ (`ChunkSpoolErrorCode`), which a handler can let through as the client's
3026
+ typed error.
3027
+ - **Progress counts file bytes**, inside a part too — the driver passes
3028
+ `onUploadProgress` into the part call, so the [upload
3029
+ progress](#upload-progress) route of the client applies. A repeated part
3030
+ counts again from its start.
3031
+ - The first writer of a part wins across processes: its data file is named by
3032
+ its hash and its receipt is published by an exclusive link. `sweep()` removes
3033
+ uploads untouched for `staleAfterMs` (default 24 h); run it on a schedule.
3034
+
3035
+ Part size is a latency decision: every part costs a round trip, so on a
3036
+ 200 ms link a smaller part buys smoother progress at the price of a slower
3037
+ upload.
3038
+
2884
3039
  ## Contract URL builders
2885
3040
 
2886
3041
  Browser-native consumers such as `<img src>`, downloads and navigation need a
@@ -10799,6 +10954,11 @@ Three things it does that a hand-written check usually does not:
10799
10954
  - `raw` keeps every signed pair, so a field Telegram adds after this release
10800
10955
  does not need a release to reach.
10801
10956
 
10957
+ The user arrives `camelCase` — `firstName`, `languageCode` — as `TelegramUser`.
10958
+ A bot meets the same person as grammY's `ctx.from`, in Telegram's `snake_case`;
10959
+ `parseTelegramUser(ctx.from)` gives the same `TelegramUser`, so one
10960
+ `ensureAccount(user)` serves the Mini App and the chat.
10961
+
10802
10962
  `maxAgeSeconds` is optional and there is no default: only the application knows
10803
10963
  how long its own session is worth. Omitting it means the string never expires,
10804
10964
  which is a decision rather than an oversight.
@@ -10828,6 +10988,17 @@ and our payload is wrong, which is the case a list of substrings quietly counts
10828
10988
  against the user. An unrecognised refusal leaves the recipient reachable: losing
10829
10989
  a working subscriber forever costs more than one wasted send.
10830
10990
 
10991
+ `TELEGRAM_SEND_FAILURE_REASONS` lists every reason. A table keyed by
10992
+ `TelegramSendFailureReason` — error codes, the text a person reads — fails to
10993
+ compile when a release adds one, instead of falling through to a default:
10994
+
10995
+ ```ts
10996
+ const codes: Record<TelegramSendFailureReason, string> = {
10997
+ 'blocked-by-user': 'E_BLOCKED',
10998
+ /* … every reason … */
10999
+ }
11000
+ ```
11001
+
10831
11002
  The rest of what a bot is assembled from — its resources, journal, operator
10832
11003
  channel, broadcasts and local Bot API files — is in [Telegram bots](telegram.md).
10833
11004
 
@@ -11894,6 +12065,18 @@ A payload goes into an audit row only after `sanitizePayload`:
11894
12065
  `tokenizer`) survive. This applies wherever a key exists — an object field and
11895
12066
  a `Map` entry alike, at any depth. A `Set` member has no key, so nothing there
11896
12067
  is masked by name;
12068
+ - **secrets are masked by their shape** in every string, whatever key holds
12069
+ it — the secret half of a Telegram bot token (`123456:[redacted]`: the bot's
12070
+ id stays, it tells two bots apart), the password in
12071
+ `postgresql://user:password@host` (user, host and path stay), and the value
12072
+ of a query parameter named as a secret (`?access_token=`, `?api-key=`,
12073
+ `?key=`, `?sig=`). A URL in an error message or a request log line is the
12074
+ usual way a token reaches a journal;
12075
+ - **an error keeps what it says about itself** — its own fields (`error_code`,
12076
+ `parameters`, a provider's `status`) are written beside `name`, `message`,
12077
+ `stack` and `cause`, masked by the same rules; a field holding a class
12078
+ instance other than an error (grammY's `BotError.ctx`) is named, not written,
12079
+ so one error cannot push its line past the bound and lose the stack;
11897
12080
  - **binary blobs** (`Uint8Array`, `Blob`, `FormData`) collapse to metadata —
11898
12081
  never the bytes;
11899
12082
  - the result is **capped** — anything over the byte limit becomes a preview.
@@ -13277,16 +13460,21 @@ output, in pino's shape — numeric `level` (`debug` 20, `info` 30, `warn` 40,
13277
13460
  `error` 50), `time` in epoch milliseconds, `msg` — so `pino-pretty` reads it on
13278
13461
  a terminal and a supervisor matching `"level":50` sees errors. Unlike pino, an
13279
13462
  `Error` under *any* key is written with its name, message, stack and cause; an
13280
- error logged as `{ error }` does not become `"error":{}`.
13463
+ error logged as `{ error }` does not become `"error":{}`, and its own fields —
13464
+ a `TelegramBotApiError`'s `error_code` and `parameters.retry_after` — are on the
13465
+ line beside them.
13466
+
13467
+ A bot token is masked wherever it appears — a request URL in an error message,
13468
+ a local Bot API file path — with no configuration: `123456:[redacted]` keeps the
13469
+ bot's id, which is public and is what tells two bots apart in one journal. So is
13470
+ the password in an address and a query parameter named as a secret (`?token=`).
13281
13471
 
13282
13472
  ```ts
13283
13473
  import { createJsonLogger } from 'stitchkit/observability'
13284
- import { TELEGRAM_BOT_TOKEN_PATTERN } from 'stitchkit/telegram'
13285
13474
 
13286
13475
  export const logger = createJsonLogger({
13287
13476
  level: env.LOG_LEVEL,
13288
13477
  fields: { service: 'my-bot' },
13289
- sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN],
13290
13478
  })
13291
13479
 
13292
13480
  logger.error('Application startup failed', { error })
@@ -13302,11 +13490,13 @@ query values inside any URL variable (`DATABASE_URL`), and each URL-encoded:
13302
13490
  import { createJsonLogger, secretValuesFromEnv } from 'stitchkit/observability'
13303
13491
 
13304
13492
  export const logger = createJsonLogger({
13305
- sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN],
13306
13493
  sensitiveValues: secretValuesFromEnv(process.env),
13307
13494
  })
13308
13495
  ```
13309
13496
 
13497
+ A string of your own — a message for a person, a report — masks bot tokens with
13498
+ `redactTelegramBotToken(text)` from `stitchkit/telegram`.
13499
+
13310
13500
  A key added to the environment is masked from its first line; a URL's user name
13311
13501
  is left alone, since it names a role that appears in ordinary text.
13312
13502
 
@@ -13363,7 +13553,21 @@ const operators = createTelegramOperatorChannel<'errors'>({
13363
13553
  })
13364
13554
  ```
13365
13555
 
13366
- Held-back messages reach `onDropped` as `repeated` or `over-budget`. What to post stays the bot's. On the way down, drain it within the grace period
13556
+ Held-back messages reach `onDropped` as `repeated` or `over-budget`.
13557
+
13558
+ A bot that already sends its alerts itself — its own bot instance, its own retry
13559
+ — takes the window alone instead of switching the channel off with parameters:
13560
+
13561
+ ```ts
13562
+ import { createTelegramOperatorDedupe } from 'stitchkit/telegram'
13563
+
13564
+ const admit = createTelegramOperatorDedupe<'errors'>({ windowMs: 10 * 60_000 })
13565
+
13566
+ const verdict = admit(text, 'errors')
13567
+ if (verdict.send) await sendAlert(verdict.text) // the text carries the held-back counts
13568
+ ```
13569
+
13570
+ What to post stays the bot's. On the way down, drain it within the grace period
13367
13571
  and close it: `drain: (context) => operators.drain(context.signal)`,
13368
13572
  `close: () => operators.close()` in a resource.
13369
13573
 
@@ -13484,6 +13688,14 @@ itself be a secret — and says what to pass: set the takeover for one start and
13484
13688
  remove it. `checkTelegramWebhook` on a timer notices a webhook that moved
13485
13689
  elsewhere, which this process cannot see any other way.
13486
13690
 
13691
+ **Moving to it from a claim of your own.** A bot that tagged its webhook itself
13692
+ holds an address this one does not recognise, so the first claim is refused —
13693
+ `other-secret` when the address is the same, `owned-elsewhere` otherwise — and
13694
+ names its own host. Start once with `takeoverFrom` set to that host, then remove
13695
+ it. The tag is derived from the secret with a fixed context, so it is the same
13696
+ across stitchkit releases: that takeover is needed once per bot, not per
13697
+ upgrade. A secret rotation is a new owner, and takes a takeover too.
13698
+
13487
13699
  **The update is recorded before Telegram is answered.** A handler that runs
13488
13700
  longer than Telegram waits — a large file from a local Bot API server — gets the
13489
13701
  same update again while it still works, and a process that answers and then
@@ -13498,10 +13710,55 @@ fix (a refused message, a blocked user) is abandoned; `maxAttempts` (5) ends
13498
13710
  the rest. Delivery is at least once: a handler that finished but could not be
13499
13711
  recorded as finished runs again.
13500
13712
 
13713
+ With grammY the handler is `bot.handleUpdate`, which wraps a middleware's error
13714
+ in a `BotError`; the intake unwraps it, so `retry`, `onFailure` and the error
13715
+ stored with the update are what the middleware threw — a `retry` that returns
13716
+ `false` for the bot's own terminal error sees that error.
13717
+
13501
13718
  The store is an interface of six atomic steps (`TelegramUpdateStore`), so the
13502
- bot's own database can hold it; `sqliteTelegramUpdateStore` takes a
13503
- `bun:sqlite` or `node:sqlite` handle as it is, and `memoryTelegramUpdateStore`
13504
- is for tests.
13719
+ bot's own database holds it:
13720
+
13721
+ | Store | Takes |
13722
+ |---|---|
13723
+ | `sqliteTelegramUpdateStore` | a `bun:sqlite` `Database` or `node:sqlite` `DatabaseSync` as it is |
13724
+ | `postgresTelegramUpdateStore` | one `query(text, parameters)` over the client the bot already holds |
13725
+ | `memoryTelegramUpdateStore` | nothing — tests, or a bot that accepts losing unhandled updates to a restart |
13726
+
13727
+ ```ts
13728
+ import { postgresTelegramUpdateStore } from 'stitchkit/telegram'
13729
+
13730
+ // Bun
13731
+ postgresTelegramUpdateStore({ query: (text, parameters) => sql.unsafe(text, [...parameters]) })
13732
+ // Prisma
13733
+ postgresTelegramUpdateStore({
13734
+ query: (text, parameters) => prisma.$queryRawUnsafe(text, ...parameters),
13735
+ createTable: false, // the migration below owns the table
13736
+ })
13737
+ // pg
13738
+ postgresTelegramUpdateStore({
13739
+ query: (text, parameters) => pool.query(text, [...parameters]).then((result) => result.rows),
13740
+ })
13741
+ ```
13742
+
13743
+ Each rule is one conditional statement, so a claim is atomic across every
13744
+ process sharing the table. By default the table is created on first use; where
13745
+ migrations own the schema, `postgresTelegramUpdateStoreSchema(table)` is the
13746
+ statement to put in one.
13747
+
13748
+ A store of your own — over an ORM, another database — is put through the same
13749
+ rules the shipped ones are, concurrent claims included:
13750
+
13751
+ ```ts
13752
+ import { checkTelegramUpdateStore } from 'stitchkit/telegram'
13753
+
13754
+ test('our update store keeps the rules', async () => {
13755
+ expect(await checkTelegramUpdateStore(() => ourStore(freshTable()))).toEqual([])
13756
+ })
13757
+ ```
13758
+
13759
+ A store that reads and then writes in two statements passes every test with one
13760
+ process and runs one update twice in production; the check races eight claims
13761
+ against each other to catch it.
13505
13762
 
13506
13763
  ## Files from a local Bot API server
13507
13764
 
@@ -13950,6 +14207,42 @@ analytics policy belong to the application.
13950
14207
 
13951
14208
  # Upgrading stitchkit
13952
14209
 
14210
+ ## Released migration: 0.101.0
14211
+
14212
+ ### `TelegramInitDataUser` is `TelegramUser`
14213
+
14214
+ **Who must act:** code that names the type `TelegramInitDataUser` from
14215
+ `stitchkit/telegram`. The record is the same; its name now says it is any
14216
+ Telegram user, since `parseTelegramUser(ctx.from)` returns it for a Bot API user
14217
+ too.
14218
+
14219
+ ```ts
14220
+ // Before
14221
+ import type { TelegramInitDataUser } from 'stitchkit/telegram'
14222
+ function ensureAccount(user: TelegramInitDataUser) {}
14223
+ // After
14224
+ import type { TelegramUser } from 'stitchkit/telegram'
14225
+ function ensureAccount(user: TelegramUser) {}
14226
+ ```
14227
+
14228
+ ### `TELEGRAM_BOT_TOKEN_PATTERN` matches the secret half only
14229
+
14230
+ **Who must act:** code that uses `TELEGRAM_BOT_TOKEN_PATTERN` to find or cut a
14231
+ whole token. It now matches what follows `<bot id>:`, so a masking replace keeps
14232
+ the bot's id — `123456:[redacted]`. Masking needs nothing any more: `redact` and
14233
+ every logger on it mask bot tokens by shape; remove the pattern from
14234
+ `sensitiveUrlPatterns`, and use `redactTelegramBotToken(text)` for a string of
14235
+ your own.
14236
+
14237
+ ```ts
14238
+ // Before
14239
+ createJsonLogger({ sensitiveUrlPatterns: [TELEGRAM_BOT_TOKEN_PATTERN] })
14240
+ text.replace(new RegExp(TELEGRAM_BOT_TOKEN_PATTERN.source, 'g'), '[redacted]')
14241
+ // After
14242
+ createJsonLogger({})
14243
+ redactTelegramBotToken(text)
14244
+ ```
14245
+
13953
14246
  ## Released migration: 0.100.0
13954
14247
 
13955
14248
  ### watch capacity and session/live/delivery adoption
@@ -17886,7 +18179,10 @@ The browser-and-server entrypoint. Re-exports everything from
17886
18179
  | `UrlBuilderConfig` | _type_ | explicit `{ baseUrl }` source for a URL builder |
17887
18180
  | `ClientConfig` | _type_ | config for `createClient`'s bare-fetch mode (2nd arg, no `HttpClient`) |
17888
18181
  | `ClientFetch` | _type_ | injectable Fetch-compatible transport used by framework and application-owned adapters |
17889
- | `ClientRequestOptions` | _type_ | per-call `{ signal?: AbortSignal }` passed through an endpoint callable's `.withOptions(...)`; caller abort is distinct from timeout — [guide](../guide/client.md#per-call-cancellation) |
18182
+ | `ClientRequestOptions` | _type_ | per-call `{ signal?, onUploadProgress? }` passed through an endpoint callable's `.withOptions(...)`; caller abort is distinct from timeout — [guide](../guide/client.md#per-call-cancellation) |
18183
+ | `UploadProgress` | _type_ | `{ sentBytes, totalBytes, attempt }` heard by `onUploadProgress`: bytes of the encoded body handed to the network, first `0`, last `totalBytes`, counted again on each transport retry — [guide](../guide/client.md#upload-progress) |
18184
+ | `uploadInChunks` | function | send a file larger than one request as `init` → parts → `finalize` through three calls the app owns; client-minted id, repeats of a part on a failure that may pass, `finalize` never repeated, cancel checked between parts, progress in file bytes — [guide](../guide/client.md#chunked-uploads) |
18185
+ | `ChunkedUploadConfig` / `ChunkedUploadStart` / `ChunkedUploadPart` / `ChunkedUploadProgress` | _type_ | the driver's file, part size, retries and three calls; what `init` declares; one part; progress `{ sentBytes, totalBytes, index, chunkCount, attempt }` |
17890
18186
  | `ContractClientConfig` | _type_ | per-tenant / resource-scoped client config — dynamic `pathPrefix` + `stripPrefixKeys` ([guide](../guide/client.md#contractclientconfig--per-tenant--resource-scoped-clients)) |
17891
18187
  | `contractEndpointMatchers` | function | compile exact pathname matchers for selected HTTP contract operations and expected-401 policy |
17892
18188
  | `PathPrefixArgs` | _type_ | required string-valued keys exposed to a typed dynamic `pathPrefix` callback |
@@ -17899,7 +18195,7 @@ The browser-and-server entrypoint. Re-exports everything from
17899
18195
  | `ConfiguredHttpClient` | _type_ | a framework-created `HttpClient` carrying its readonly `baseUrl` for URL builders |
17900
18196
  | `HttpClientConfig` | _type_ | config for `createHttpClient`; retry `limit` counts retries after the initial attempt (default 2 = at most 3 GET attempts), with `statusCodes: []` by default; `fetch` installs an explicit transport and is mutually exclusive with the legacy Bun-only `unix` option — [details](../guide/client.md#createhttpclient) |
17901
18197
  | `UnauthorizedMatcher` | _type_ | exact `(pathname) => boolean` policy accepted by `suppressUnauthorizedFor` |
17902
- | `RequestOptions` | _type_ | per-call options — params, timeout, response type |
18198
+ | `RequestOptions` | _type_ | per-call options — params, timeout, response type, `onUploadProgress` |
17903
18199
  | `HeaderProvider` | _type_ | static or per-request headers |
17904
18200
  | `ApiEvent` | _type_ | a client event — `unauthorized` / `network_error` / `logout` |
17905
18201
  | `ApiEventListener` | _type_ | an `ApiEvent` handler |
@@ -19328,7 +19624,7 @@ audit event. See the [Observability guide](../guide/observability.md).
19328
19624
  | Export | Kind | Summary |
19329
19625
  |--------|------|---------|
19330
19626
  | `sanitizePayload` | function | redact secrets and cap size — [guide](../guide/observability.md#sanitisation) |
19331
- | `redact` | function | mask secret-named keys, drop binary blobs |
19627
+ | `redact` | function | mask secret-named keys and secrets recognised by shape in every string — the secret half of a bot token, a password in an address, a query parameter named as a secret; drop binary blobs; an error keeps its own fields (`error_code`, `parameters`), a class instance on one is named, not written |
19332
19628
  | `truncatePreview` | function | cap a value by serialised size |
19333
19629
  | `measureSize` | function | item count + byte size of a result |
19334
19630
  | `JsonValue` | _type_ | a JSON-serialisable value |
@@ -19743,6 +20039,9 @@ available from `stitchkit/contract`.
19743
20039
  | `writeFileAtomic` / `writeFileAtomicSync` / `WriteFileAtomicOptions` | function / _type_ | replace a file atomically: a random staging name created exclusively (never through a planted link), the mode set on the descriptor before the file is visible (default `0o600`, not masked by the umask), `fsync`, rename; a failure leaves the target and no staging file. The asynchronous form keeps the event loop running |
19744
20040
  | `withExclusiveLock` / `ExclusiveLockOptions` / `ExclusiveLock` / `ExclusiveLockOwner` | function / _type_ | run work under an exclusive lock between processes — a file recording its owner (pid, host, machine identity, time and nullable kernel process identity); waits up to `timeoutMs` (default 10 s) and stops on `signal`; a dead owner on this machine is taken over, a live, slow or foreign one never is; an ownerless lock only after `ownerlessGraceMs` |
19745
20041
  | `ExclusiveLockError` | class | `LOCK_TIMEOUT` naming the resource and its holder, or `LOCK_ABORTED` with the signal's reason |
20042
+ | `createChunkSpool` | function | the server half of a chunked upload: parts on disk under `owner` + client-minted `uploadId`, each with a size+sha256 receipt; idempotent `open` and `put`, the first writer of a part wins across processes, `assemble` in order once every part is in, `sweep` of untouched uploads — [guide](../guide/client.md#chunked-uploads) |
20043
+ | `ChunkSpool` / `ChunkSpoolConfig` / `ChunkSpoolKey` / `ChunkSpoolOpen` / `ChunkSpoolPart` / `ChunkSpoolAssembly` | _type_ | the spool, its directory, part size, file limit, meta schema and age; an upload's key; its declaration; one part; the assembled parts with a concatenating `stream()` |
20044
+ | `ChunkSpoolErrorCode` | _type_ | the `AppError` codes a spool refuses with, each with its 4xx status: `UPLOAD_INVALID`, `UPLOAD_TOO_LARGE`, `UPLOAD_NOT_FOUND`, `UPLOAD_CONFLICT`, `UPLOAD_CHUNK_OUT_OF_RANGE`, `UPLOAD_CHUNK_SIZE`, `UPLOAD_INCOMPLETE` |
19746
20045
 
19747
20046
  ---
19748
20047
 
@@ -19758,23 +20057,24 @@ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
19758
20057
  | `verifyTelegramInitData` | function | verify a Mini App `initData` signature against the bot token in constant time, before reading anything out of it |
19759
20058
  | `VerifyTelegramInitDataOptions` | _type_ | raw `initData`, bot token, optional `maxAgeSeconds` bound and injected clock |
19760
20059
  | `TelegramInitDataVerification` | _type_ | `{ valid: true, data }` or a refusal carrying its reason |
19761
- | `TelegramInitData` | _type_ | verified `user`/`receiver`, `authDate`, `ageSeconds`, `queryId`, `startParam`, `chatType`, `chatInstance` and every signed pair in `raw` |
19762
- | `TelegramInitDataUser` | _type_ | camelCase user record inferred from Telegram's signed `user` payload |
20060
+ | `TelegramInitData` | _type_ | verified `user`/`receiver` as `TelegramUser`, `authDate`, `ageSeconds`, `queryId`, `startParam`, `chatType`, `chatInstance` and every signed pair in `raw` |
20061
+ | `parseTelegramUser` / `TelegramUser` | function / _type_ | Telegram's `snake_case` user — grammY's `ctx.from`, a Bot API `User`, a Mini App `user` — as the one camelCase record initData returns too; `undefined` when it is not a user |
19763
20062
  | `TelegramInitDataRefusal` | _type_ | `missing-hash` / `signature-mismatch` / `malformed` / `expired` — an expired string is not a forged one |
19764
20063
  | `classifyTelegramSendFailure` | function | name a refused Bot API send and separate "retry this send" from "stop addressing this recipient" |
19765
20064
  | `TelegramSendFailure` | _type_ | reason, `status`, Telegram-stated `retryAfterSeconds`, `retryable`, `recipientUnreachable` and which evidence produced the answer |
19766
- | `TelegramSendFailureReason` | _type_ | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown` |
20065
+ | `TelegramSendFailureReason` / `TELEGRAM_SEND_FAILURE_REASONS` | _type_ / constant | `blocked-by-user` / `user-deactivated` / `chat-not-found` / `not-started` / `rate-limited` / `message-invalid` / `server-error` / `unknown`, and the same as a list; a `Record<TelegramSendFailureReason, …>` over them fails to compile when a reason is added |
19767
20066
  | `runTelegramBroadcast` | function | a resumable broadcast by `name` under a state `directory`: audience written once, an append-only journal, pacing at `ratePerSecond` (25), 429 waits `retry_after`, unreachable recipients never addressed again, a `message-invalid` or unreachable Telegram halts without charging the recipient, a send in flight at a crash is `uncertain` and not repeated, `dryRun`, one runner per name |
19768
20067
  | `TelegramBroadcastConfig` / `TelegramBroadcastSend` / `TelegramBroadcastReport` / `TelegramBroadcastRunOutcome` / `TelegramBroadcastOutcome` / `TelegramBroadcastRecipient` | _type_ | `name`, `directory`, `recipients()`, `send({ recipient, attempt })`, `maxAttempts`, `signal`, `onProgress`; counts `delivered` / `unreachable` / `failed` / `uncertain` / `pending` and `finished` / `stopped` / `halted` / `dry-run` |
19769
20068
  | `telegramBroadcastSender` / `TelegramBroadcastSenderConfig` / `TelegramBroadcastMessage` | function / _type_ | the standard `send`: `sendMessage` with a text or `copyMessage` of a prepared message, through the Bot API |
19770
20069
  | `createTelegramOperatorChannel` | function | the operator chat, apart from the journal: `post(text, topic?)` returns at once and never throws; paced sends into forum topics, 429 honoured, oldest dropped on overflow, `drain(signal)` and `close()` for the way down |
19771
20070
  | `TelegramOperatorChannelConfig` / `TelegramOperatorChannel` / `TelegramOperatorMessage` / `TelegramOperatorDrop` / `TelegramChatId` | _type_ | `chatId`, `topics`, `send`, `maxQueued`, `minIntervalMs`, `maxAttempts`, `onDropped`, `sensitivePatterns` and `sensitiveValues` (a bot token is always masked), `dedupe`; a drop is `overflow` / `refused` / `attempts` / `closed` / `repeated` / `over-budget` with its classification |
20071
+ | `createTelegramOperatorDedupe` / `TelegramOperatorDedupeVerdict` | function / _type_ | the channel's dedupe window alone, for a sender with its own bot and retry: `(text, topic?)` → `{ send: true, text }` with the held-back counts, or `{ send: false, reason: 'repeated' \| 'over-budget' }` |
19772
20072
  | `TelegramOperatorDedupe` | _type_ | `windowMs` (10 min): one message per `fingerprint` per window, the next one sent carries `repeatedLine(count)`; `maxPerWindow` (20) across fingerprints, the overflow counted onto the next one sent with `overBudgetLine(count)`; `now` |
19773
20073
  | `telegramOperatorSender` / `TelegramOperatorSenderConfig` | function / _type_ | the standard `send`: `sendMessage` into the chat and topic, plain text unless `parseMode`; with `'HTML'` the markup is cleaned and cut by what it shows, so the channel's cut never leaves a tag open |
19774
20074
  | `createTelegramLocalFiles` | function | files the local Bot API server wrote under `<root>/<token>/`: `resolve(file_path)` and `remove(file_path)` only inside the bot's directory, judged on real paths so a link is judged by where it leads; `check()` for readiness. Recommended root variable: `BOT_API_FILES_ROOT` |
19775
20075
  | `TelegramLocalFilesConfig` / `TelegramLocalFiles` / `TelegramLocalFilesCheck` / `TelegramLocalFileError` / `TelegramLocalFileRefusal` | _type_ / class | `root`, `token`; refusals `root-not-absolute` / `bot-directory-unavailable` / `outside-bot-directory` / `missing` / `not-a-file`, never carrying the path or the token |
19776
20076
  | `callTelegramBotApi` / `TelegramBotApiCall` / `TelegramBotApiError` | function / _type_ / class | one Bot API method over `fetch`; a refusal keeps `error_code`, `description` and `parameters` for the classifier, and no error names the URL |
19777
- | `TELEGRAM_BOT_TOKEN_PATTERN` | constant | the shape of a bot token, for a logger's `sensitiveUrlPatterns` |
20077
+ | `TELEGRAM_BOT_TOKEN_PATTERN` / `redactTelegramBotToken` | constant / function | the secret half of a bot token — the bot's id before the colon is not matched; `redact` and every logger on it mask it already, `redactTelegramBotToken(text)` masks every token in a string to `123456:[redacted]` |
19778
20078
  | `claimTelegramWebhook` | function | set the webhook only where it already points here, or by naming the host it is taken from; confirm Telegram holds it; pending updates kept unless `dropPendingUpdates` |
19779
20079
  | `ClaimTelegramWebhookConfig` / `TelegramWebhookConfig` / `TelegramWebhookClaim` | _type_ | `token`, `url`, `secret`, `apiRoot`, `fetch`, `signal`; `takeoverFrom` (a host, or `'none'`), `allowedUpdates`, `maxConnections`, `dropPendingUpdates`; the claim's tagged `url`, `takenFrom`, `pendingUpdates` |
19780
20080
  | `TelegramWebhookClaimError` / `TelegramWebhookRefusal` / `TELEGRAM_WEBHOOK_NONE` | class / _type_ / constant | `owned-elsewhere` / `other-secret` / `not-confirmed` with `ownerHost` — a host, never a path; `'none'` names no webhook |
@@ -19782,10 +20082,13 @@ injected grammY bot. → ADR 0143, ADR 0201 — [guide](../guide/telegram.md)
19782
20082
  | `checkTelegramWebhook` / `TelegramWebhookState` | function / _type_ | whether the webhook still points here: `owned`, `ownerHost`, `pendingUpdates`, Telegram's `lastError` |
19783
20083
  | `receiveTelegramWebhook` / `ReceiveTelegramWebhookOptions` / `TelegramUpdateAcceptance` | function / _type_ | one webhook `Request`: 403 without the secret (compared in constant time), 405 for another method, 400 for no update, 200 once `accept` recorded it |
19784
20084
  | `createTelegramUpdateIntake` | function | webhook updates recorded before Telegram is answered and handled after: chat order kept, other chats alongside up to `maxConcurrent`, a renewed lease per attempt, a sweep that takes over what a restart or a dead process left, retries, `maxAttempts`, finished updates remembered `retainMs` to refuse repeats; at least once |
19785
- | `TelegramUpdateIntakeConfig` / `TelegramUpdateIntake` / `TelegramUpdateEnvelope` / `TelegramUpdateFailure` / `TelegramUpdateStoreStep` | _type_ | `store`, `handle`, `retry`, `maxAttempts` (5), `leaseMs` (120 000), `pendingGraceMs` (60 000), `sweepEveryMs` (30 000), `retainMs` (24 h), `maxConcurrent` (32), `onFailure`, `onStoreError`, `now`; `accept(body)`, `start()`, `sweep()`, `idle()`, `close()` |
20085
+ | `TelegramUpdateIntakeConfig` / `TelegramUpdateIntake` / `TelegramUpdateEnvelope` / `TelegramUpdateFailure` / `TelegramUpdateStoreStep` | _type_ | `store`, `handle` (a grammY `BotError` is unwrapped: `retry`, `onFailure` and the stored error see what the middleware threw), `retry`, `maxAttempts` (5), `leaseMs` (120 000), `pendingGraceMs` (60 000), `sweepEveryMs` (30 000), `retainMs` (24 h), `maxConcurrent` (32), `onFailure`, `onStoreError`, `now`; `accept(body)`, `start()`, `sweep()`, `idle()`, `close()` |
19786
20086
  | `TelegramUpdateStore` / `StoredTelegramUpdate` / `TelegramUpdateState` / `TelegramUpdateSettlement` / `TelegramUpdateClaimOptions` / `TelegramUpdateDueQuery` | _type_ | the store an intake runs on: `add`, `claim`, `renew`, `settle`, `due`, `prune`, each atomic on one record — a claim takes `pending`, a due `failed` or a lapsed `processing`; renew and settle only by the attempt holding it |
19787
20087
  | `memoryTelegramUpdateStore` | function | the store in this process's memory — tests, or a bot that accepts losing unhandled updates to a restart |
19788
20088
  | `sqliteTelegramUpdateStore` / `SqliteTelegramUpdateStoreConfig` / `TelegramSqliteDatabase` | function / _type_ | the store in SQLite over the handle the application opened — `bun:sqlite` `Database` or `node:sqlite` `DatabaseSync`; `table` (default `telegram_updates`) created when missing |
20089
+ | `postgresTelegramUpdateStore` / `PostgresTelegramUpdateStoreConfig` / `TelegramPostgresQuery` | function / _type_ | the store in Postgres over the client the application holds, as one `query(text, parameters)` returning rows — Bun `sql.unsafe`, Prisma `$queryRawUnsafe`, `pg` `pool.query`; each rule one conditional statement; `table`, `createTable` (default `true`) |
20090
+ | `postgresTelegramUpdateStoreSchema` | function | the table and its index as a statement for a migration, safe to run twice |
20091
+ | `checkTelegramUpdateStore` | function | put any store through every rule, each on a fresh store from `make`, concurrent claims included; resolves with the violations, empty when it keeps them |
19789
20092
 
19790
20093
  ---
19791
20094
 
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) 16.5 KB → [llms/stitchkit.2.txt](llms/stitchkit.2.txt) 37.2 KB → [llms/stitchkit.3.txt](llms/stitchkit.3.txt) 16.1 KB
12
+ - **stitchkit**: [llms/stitchkit.txt](llms/stitchkit.txt) 16.5 KB → [llms/stitchkit.2.txt](llms/stitchkit.2.txt) 43.6 KB → [llms/stitchkit.3.txt](llms/stitchkit.3.txt) 16.9 KB
13
13
  - **stitchkit/live**: [llms/live.txt](llms/live.txt) 21.6 KB
14
14
  - **stitchkit/react**: [llms/react.txt](llms/react.txt) 9.0 KB
15
15
  - **stitchkit/react/keyboard**: [llms/react-keyboard.txt](llms/react-keyboard.txt) 9.4 KB
@@ -21,8 +21,8 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
21
21
  - **stitchkit/remote**: [llms/remote.txt](llms/remote.txt) 1.0 KB
22
22
  - **stitchkit/contract**: [llms/contract.txt](llms/contract.txt) 31.9 KB
23
23
  - **stitchkit/primitives**: [llms/primitives.txt](llms/primitives.txt) 9.3 KB
24
- - **stitchkit/server**: [llms/server.txt](llms/server.txt) 45.3 KB → [llms/server.2.txt](llms/server.2.txt) 14.6 KB → [llms/server.3.txt](llms/server.3.txt) 45.6 KB → [llms/server.4.txt](llms/server.4.txt) 37.5 KB → [llms/server.5.txt](llms/server.5.txt) 21.8 KB
25
- - **stitchkit/observability**: [llms/observability.txt](llms/observability.txt) 43.7 KB
24
+ - **stitchkit/server**: [llms/server.txt](llms/server.txt) 45.3 KB → [llms/server.2.txt](llms/server.2.txt) 14.6 KB → [llms/server.3.txt](llms/server.3.txt) 45.6 KB → [llms/server.4.txt](llms/server.4.txt) 38.1 KB → [llms/server.5.txt](llms/server.5.txt) 21.8 KB
25
+ - **stitchkit/observability**: [llms/observability.txt](llms/observability.txt) 44.8 KB
26
26
  - **stitchkit/agent-runtime**: [llms/agent-runtime.txt](llms/agent-runtime.txt) 36.8 KB → [llms/agent-runtime.2.txt](llms/agent-runtime.2.txt) 46.7 KB → [llms/agent-runtime.3.txt](llms/agent-runtime.3.txt) 34.3 KB
27
27
  - **stitchkit/agent-runtime/testing**: [llms/agent-runtime-testing.txt](llms/agent-runtime-testing.txt) 2.2 KB
28
28
  - **stitchkit/agent-runtime/harness**: [llms/agent-runtime-harness.txt](llms/agent-runtime-harness.txt) 4.1 KB
@@ -39,8 +39,8 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
39
39
  - **stitchkit/application/schemas**: [llms/application-schemas.txt](llms/application-schemas.txt) 1.8 KB
40
40
  - **stitchkit/testing**: [llms/testing.txt](llms/testing.txt) 30.8 KB
41
41
  - **stitchkit/agent-runtime/sandbox**: [llms/agent-runtime-sandbox.txt](llms/agent-runtime-sandbox.txt) 8.1 KB
42
- - **stitchkit/files**: [llms/files.txt](llms/files.txt) 2.9 KB
43
- - **stitchkit/telegram**: [llms/telegram.txt](llms/telegram.txt) 22.2 KB
42
+ - **stitchkit/files**: [llms/files.txt](llms/files.txt) 3.8 KB
43
+ - **stitchkit/telegram**: [llms/telegram.txt](llms/telegram.txt) 26.9 KB
44
44
  - **stitchkit/telegram/html**: [llms/telegram-html.txt](llms/telegram-html.txt) 2.7 KB
45
45
  - **stitchkit/telegram/screens**: [llms/telegram-screens.txt](llms/telegram-screens.txt) 21.2 KB
46
46
  - **stitchkit/tracking**: [llms/tracking.txt](llms/tracking.txt) 16.8 KB
@@ -55,7 +55,7 @@ Load the slice for each entrypoint your code imports — `import … from 'stitc
55
55
 
56
56
  ## Upgrading slices
57
57
  - **Upgrading — how to move across versions**: [llms/upgrading.txt](llms/upgrading.txt) 10.3 KB
58
- - **Upgrading 0.74.0 → 0.100.0**: [llms/upgrading-0.74.0-0.100.0.txt](llms/upgrading-0.74.0-0.100.0.txt) 45.1 KB
58
+ - **Upgrading 0.74.0 → 0.101.0**: [llms/upgrading-0.74.0-0.101.0.txt](llms/upgrading-0.74.0-0.101.0.txt) 46.3 KB
59
59
  - **Upgrading 0.61.0 → 0.73.0**: [llms/upgrading-0.61.0-0.73.0.txt](llms/upgrading-0.61.0-0.73.0.txt) 41.2 KB
60
60
  - **Upgrading 0.44.0 → 0.60.0**: [llms/upgrading-0.44.0-0.60.0.txt](llms/upgrading-0.44.0-0.60.0.txt) 43.9 KB
61
61
  - **Upgrading 0.37.0 → 0.37.0**: [llms/upgrading-0.37.0.txt](llms/upgrading-0.37.0.txt) 6.2 KB
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "stitchkit",
3
- "version": "0.100.3",
3
+ "version": "0.101.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",