stitchkit 0.77.0 → 0.79.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/agent-runtime-browser.js +53 -581
- package/dist/agent-runtime-coding-tools.js +6 -5
- package/dist/agent-runtime-harness.js +20 -93
- package/dist/agent-runtime-openrouter.js +1 -1
- package/dist/agent-runtime.js +65 -61
- package/dist/application/decisions.d.ts +28 -25
- package/dist/application/decisions.d.ts.map +1 -1
- package/dist/application/kernel.d.ts +2 -0
- package/dist/application/kernel.d.ts.map +1 -1
- package/dist/application/keyspace.d.ts +11 -1
- package/dist/application/keyspace.d.ts.map +1 -1
- package/dist/application/schedule.d.ts.map +1 -1
- package/dist/application/schemas.d.ts +2 -0
- package/dist/application/schemas.d.ts.map +1 -1
- package/dist/application/server-resource.d.ts.map +1 -1
- package/dist/application/watch-hub.d.ts.map +1 -1
- package/dist/application-diagnostic-journal.d.ts +21 -0
- package/dist/application-diagnostic-journal.d.ts.map +1 -0
- package/dist/application-diagnostic-journal.js +632 -0
- package/dist/application-opentelemetry.js +3 -2
- package/dist/application-schemas.js +14 -98
- package/dist/application.d.ts +1 -2
- package/dist/application.d.ts.map +1 -1
- package/dist/application.js +155 -882
- package/dist/cli.js +13 -11
- package/dist/contract/index.js +15 -10
- package/dist/files/boundary.d.ts.map +1 -1
- package/dist/files.js +1 -1
- package/dist/{index-7b188kmz.js → index-2qzskm8h.js} +15 -1
- package/dist/{index-k2zczx1g.js → index-2rwwmr7v.js} +3 -3
- package/dist/{index-8eywc9zv.js → index-3gye9wzb.js} +6 -3
- package/dist/index-4fpa9dy2.js +51 -0
- package/dist/{index-7qy2ex0m.js → index-5mtc969c.js} +2 -2
- package/dist/index-5s3zajp8.js +192 -0
- package/dist/{index-3cwck0rm.js → index-6s7n2v50.js} +60 -374
- package/dist/{index-s4c8wy8m.js → index-758zx2ha.js} +11 -90
- package/dist/{index-qyrqwr4c.js → index-77fekveh.js} +4 -1
- package/dist/{index-58jzmnn4.js → index-7ag81v2f.js} +355 -34
- package/dist/index-7gnkny4z.js +326 -0
- package/dist/index-7zbps32p.js +89 -0
- package/dist/index-85vfqd7m.js +135 -0
- package/dist/{index-m668wzyc.js → index-9s6z8vgf.js} +17 -141
- package/dist/{index-bfcpjw20.js → index-actkayzn.js} +7 -3
- package/dist/index-b9aefrqd.js +20 -0
- package/dist/index-dre2ywck.js +48 -0
- package/dist/{index-nemjkxjp.js → index-dwf1f2ap.js} +16 -12
- package/dist/{index-wwst0td5.js → index-dx9xehwt.js} +1 -1
- package/dist/{index-ezmn6ac6.js → index-dxesg10d.js} +4 -4
- package/dist/{index-aczggrty.js → index-dxjjv399.js} +2 -2
- package/dist/{index-jqtsc9mj.js → index-egaabxkz.js} +3 -3
- package/dist/{index-vc1b0b1b.js → index-evcnmsc3.js} +49 -8
- package/dist/{index-nt1mp8km.js → index-fxz0v7vg.js} +84 -84
- package/dist/index-h4pj6fta.js +87 -0
- package/dist/{index-1rxswfbv.js → index-hb5pf3s4.js} +1 -1
- package/dist/{index-t8qyvrvg.js → index-k9ay6v6d.js} +21 -272
- package/dist/index-kzfs85xp.js +9 -0
- package/dist/{index-vbf2p6me.js → index-m9kzax52.js} +91 -89
- package/dist/{index-22by16v6.js → index-mgtxfe6b.js} +1 -1
- package/dist/index-p7hgdyhb.js +22 -0
- package/dist/index-pq95qkbf.js +0 -0
- package/dist/index-t0z5p5w6.js +318 -0
- package/dist/{index-s2rchahr.js → index-vwp69871.js} +13 -11
- package/dist/index-wc4x00bn.js +103 -0
- package/dist/{index-da1aqnhb.js → index-xcetq6j3.js} +8 -6
- package/dist/{index-f6n5n7nz.js → index-y01f4223.js} +33 -126
- package/dist/{index-35z5h2ty.js → index-ywd0y6m1.js} +18 -218
- package/dist/index.js +65 -1588
- package/dist/internal/deadline.d.ts +17 -0
- package/dist/internal/deadline.d.ts.map +1 -0
- package/dist/live.js +15 -89
- package/dist/node.js +17 -15
- package/dist/observability/context.d.ts.map +1 -1
- package/dist/observability/index.js +7 -6
- package/dist/primitives.js +5 -2
- package/dist/react.js +1 -1
- package/dist/remote.js +7 -6
- package/dist/server/event-bus.d.ts.map +1 -1
- package/dist/server/index.js +26 -38
- package/dist/telegram.js +1 -1
- package/dist/testing.js +86 -97
- package/dist/tool-invoker.js +11 -9
- package/dist/tools/async-operation-contract.d.ts +61 -0
- package/dist/tools/async-operation-contract.d.ts.map +1 -0
- package/dist/tools/async-operation.d.ts +2 -47
- package/dist/tools/async-operation.d.ts.map +1 -1
- package/dist/tools/view-file-contract.d.ts +98 -0
- package/dist/tools/view-file-contract.d.ts.map +1 -0
- package/dist/tools/view-file.d.ts +4 -83
- package/dist/tools/view-file.d.ts.map +1 -1
- package/dist/tools-contract.d.ts +29 -0
- package/dist/tools-contract.d.ts.map +1 -0
- package/dist/tools-contract.js +24 -0
- package/dist/tools.d.ts +3 -3
- package/dist/tools.d.ts.map +1 -1
- package/dist/tools.js +59 -95
- package/llms-full.txt +146 -14
- package/package.json +14 -7
- package/dist/index-ksp6e2ye.js +0 -24
- package/dist/index-vj3vvpaa.js +0 -307
package/llms-full.txt
CHANGED
|
@@ -53,6 +53,7 @@ own, recorded as an ADR.
|
|
|
53
53
|
| `stitchkit/server` | server (Bun) | stable | `createServer`, `implement`, hooks, auth, Socket.IO server, server primitives |
|
|
54
54
|
| `stitchkit/node` | server (Node ≥ 22) | stable | `serveNode` + the runtime-agnostic core — the Node mirror of `/server` |
|
|
55
55
|
| `stitchkit/tools` | server | stable | `createMcpHandler`, `mountMcp`, `mountAgent`, the OAuth provider, native tools |
|
|
56
|
+
| `stitchkit/tools/contract` | browser + server | evolving | the shapes a tool surface speaks — async-operation contract, snapshot and cancel schemas, view-file input/output — without the runtime that serves them |
|
|
56
57
|
| `stitchkit/tools/invoker` | server | stable | peer-free `createToolInvoker` over the canonical contract tool runner |
|
|
57
58
|
| `stitchkit/cli` | server | stable | `createCli` — the CLI transport, light (no MCP SDK / `ai`) |
|
|
58
59
|
| `stitchkit/remote` | browser **and** server | stable | peer-free `implementRemote` for thin HTTP proxy processes |
|
|
@@ -60,9 +61,9 @@ own, recorded as an ADR.
|
|
|
60
61
|
| `stitchkit/telegram` | server (Bun or Node) | evolving | peer-free Telegram platform primitives — Mini App `initData` verification and Bot API send-failure classification |
|
|
61
62
|
| `stitchkit/observability` | server | stable | request/tool event projections — `createObservability`, trace context, sanitisation |
|
|
62
63
|
| `stitchkit/testing` | tests on Bun or Node | stable | in-process generated clients over a real Fetch handler, plus the store and managed-resource conformance kits |
|
|
63
|
-
| `stitchkit/declaration` | build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
|
|
64
|
+
| `stitchkit/declaration` | browser + build and deployment tooling (Bun or Node) | evolving | `ProjectDeclarationSchema` — the one machine-readable statement a repository makes about itself |
|
|
64
65
|
| `stitchkit/react` | browser | stable | `createCursorQuery`, `createCacheBridge` |
|
|
65
|
-
| `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the
|
|
66
|
+
| `stitchkit/agent-runtime` | server | evolving<br>_redefined in 11 of the 24 minors since 0.56.2, most recently 0.75.0_ | optional durable conversation/run loop, history, models, prompts, fencing and events |
|
|
66
67
|
| `stitchkit/agent-runtime/harness` | server | evolving | resource-aware process-local facade over the canonical Agent runtime; supervision stays outside |
|
|
67
68
|
| `stitchkit/agent-runtime/coding-tools` | server (Bun or Node) | evolving | bounded host-authorized direct file and shell tools; a root boundary, not an OS sandbox |
|
|
68
69
|
| `stitchkit/agent-runtime/openrouter` | server | evolving | isolated OpenRouter language-model adapter |
|
|
@@ -70,10 +71,11 @@ own, recorded as an ADR.
|
|
|
70
71
|
| `stitchkit/agent-runtime/sqlite/bun` | server (Bun) | evolving | durable built-in SQLite store for the agent runtime |
|
|
71
72
|
| `stitchkit/agent-runtime/sqlite/node` | server (Node ≥ 22.5) | evolving | durable built-in SQLite store for the agent runtime |
|
|
72
73
|
| `stitchkit-tui` | terminal (Bun) | evolving | optional official OpenTUI host over a caller-composed headless runtime |
|
|
73
|
-
| `stitchkit/application` | server | evolving<br>_redefined in
|
|
74
|
+
| `stitchkit/application` | browser + server | evolving<br>_redefined in 6 of the 24 minors since 0.56.2, most recently 0.79.0_ | managed resource graph, readiness, admission, schedules, subtree restart and bounded shutdown |
|
|
74
75
|
| `stitchkit/application/grammy` | server | evolving | isolated grammY polling and webhook lifecycle adapters |
|
|
75
76
|
| `stitchkit/application/opentelemetry` | server | evolving | maps application snapshots onto an injected OpenTelemetry `Meter` |
|
|
76
|
-
| `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas
|
|
77
|
+
| `stitchkit/application/schemas` | browser + server | evolving | the application's snapshot, health and shutdown schemas alone, without the kernel |
|
|
78
|
+
| `stitchkit/application/diagnostic-journal` | server | evolving | the local diagnostic journal — the one part of the kernel that spawns, locks and writes files |
|
|
77
79
|
|
|
78
80
|
Rule of thumb: browser code imports `stitchkit` and `stitchkit/react`; server
|
|
79
81
|
code adds `stitchkit/server` (or `stitchkit/node` on Node) and opts into
|
|
@@ -5509,9 +5511,26 @@ await app.restart({ resourceId: 'cache' })
|
|
|
5509
5511
|
**Refused is not failed.** An unknown id, a restart during shutdown, and a
|
|
5510
5512
|
restart before the application is ready all return `refused` with a reason and
|
|
5511
5513
|
touch nothing. `failed` means the subtree came down and the new generation did
|
|
5512
|
-
not come up — the
|
|
5513
|
-
|
|
5514
|
-
|
|
5514
|
+
not come up — and the result agrees with the snapshot: if any affected resource
|
|
5515
|
+
ends the restart in `failed`, so does the restart, including an optional one
|
|
5516
|
+
whose failure the start loop does not re-throw.
|
|
5517
|
+
|
|
5518
|
+
**The close phase is bounded**, by the application's shutdown budget or by one
|
|
5519
|
+
this call names:
|
|
5520
|
+
|
|
5521
|
+
```ts
|
|
5522
|
+
await app.restart({ resourceId: 'database', gracePeriodMs: 5_000, forceTimeoutMs: 2_000 })
|
|
5523
|
+
```
|
|
5524
|
+
|
|
5525
|
+
**A resource has to be able to start twice.** The kernel calls `start` again, so
|
|
5526
|
+
a resource holding state across its own lifetime rebuilds it there. The ones this
|
|
5527
|
+
framework ships do — a schedule re-arms, a keyspace opens a new generation and
|
|
5528
|
+
re-loads, a managed server built from a factory gets a fresh server. A managed
|
|
5529
|
+
server given a server *instance* cannot, and says so by name rather than
|
|
5530
|
+
republishing something it has shut down. Give a keyspace a
|
|
5531
|
+
`backend: () => …` factory when its backend cannot be re-opened after `close()`.
|
|
5532
|
+
|
|
5533
|
+
→ [ADR 0157](../decisions/0157-a-restartable-resource-begins-a-generation.md).
|
|
5515
5534
|
|
|
5516
5535
|
Restarts of overlapping subtrees queue behind each other rather than being
|
|
5517
5536
|
refused. Two callers asking at once is ordinary; two generations of one resource
|
|
@@ -5547,7 +5566,7 @@ const pipeline = createDecisionPipeline<{ userId: string; scope: string }>([
|
|
|
5547
5566
|
? { outcome: 'allow' }
|
|
5548
5567
|
: { outcome: 'defer' }) },
|
|
5549
5568
|
{ id: 'default', decide: () => ({ outcome: 'deny', reason: 'no policy allowed this' }) },
|
|
5550
|
-
])
|
|
5569
|
+
], { policyTimeoutMs: 2_000 })
|
|
5551
5570
|
|
|
5552
5571
|
const result = await pipeline.decide(request)
|
|
5553
5572
|
result.outcome // 'allow' | 'deny' — never 'defer'
|
|
@@ -5568,8 +5587,19 @@ answer that wrongly while looking complete.
|
|
|
5568
5587
|
**Every policy deferring raises.** `DecisionUndecidedError`, not a default —
|
|
5569
5588
|
defaulting to `allow` turns an incomplete policy set into an open door, and
|
|
5570
5589
|
defaulting to `deny` turns it into an outage whose cause reads as a legitimate
|
|
5571
|
-
refusal.
|
|
5572
|
-
|
|
5590
|
+
refusal.
|
|
5591
|
+
|
|
5592
|
+
**A policy that does not answer raises too.** A non-decision, a throw, and running
|
|
5593
|
+
past `policyTimeoutMs` are one error — `DecisionPolicyError`, naming the policy
|
|
5594
|
+
and carrying the trace so far — because they are indistinguishable downstream and
|
|
5595
|
+
handling one but not the others is how a broken policy becomes a skipped one. The
|
|
5596
|
+
chain stops there rather than falling through to whatever the next policy would
|
|
5597
|
+
have said.
|
|
5598
|
+
|
|
5599
|
+
`policyTimeoutMs` is required and has no default. Pick a number your slowest
|
|
5600
|
+
policy comfortably beats. The framework will not choose it for you: a policy that
|
|
5601
|
+
never settles hangs every caller of the operation it guards, and a default here
|
|
5602
|
+
is a number nobody chose applied to code the framework has never seen.
|
|
5573
5603
|
|
|
5574
5604
|
The same `allow`/`deny`/`defer` type is what an event topic declared
|
|
5575
5605
|
`mode: 'decision'` uses in `stitchkit/live` — one vocabulary, because a listener
|
|
@@ -5671,8 +5701,14 @@ changing its revision or status contract. → ADR 0119.
|
|
|
5671
5701
|
Use `createDiagnosticJournal` when a process needs finite, ordered local metadata evidence and the
|
|
5672
5702
|
deployment log pipeline is not the right boundary:
|
|
5673
5703
|
|
|
5704
|
+
It lives at `stitchkit/application/diagnostic-journal`, not in the main barrel:
|
|
5705
|
+
it is the one part of the kernel that spawns, locks and writes files, and while it
|
|
5706
|
+
was exported from `stitchkit/application` that single line made the whole
|
|
5707
|
+
entrypoint unusable in a browser bundle. Its schemas stay in
|
|
5708
|
+
`stitchkit/application`.
|
|
5709
|
+
|
|
5674
5710
|
```ts
|
|
5675
|
-
import { createDiagnosticJournal } from 'stitchkit/application'
|
|
5711
|
+
import { createDiagnosticJournal } from 'stitchkit/application/diagnostic-journal'
|
|
5676
5712
|
import { z } from 'zod'
|
|
5677
5713
|
|
|
5678
5714
|
const journal = await createDiagnosticJournal({
|
|
@@ -10101,6 +10137,84 @@ makes one thing your job rather than the resolver's:
|
|
|
10101
10137
|
The mechanical part is identical either way. Only the *noticing* differs, and an
|
|
10102
10138
|
exact pin moves it onto you.
|
|
10103
10139
|
|
|
10140
|
+
## Released migration: 0.79.0
|
|
10141
|
+
|
|
10142
|
+
One thing, and only if you assert on the whole snapshot.
|
|
10143
|
+
|
|
10144
|
+
```bash
|
|
10145
|
+
rg -n "getSnapshot\(\)|projectApplicationStatus|status\(\)" --glob '*.test.ts' --glob '*.spec.ts'
|
|
10146
|
+
```
|
|
10147
|
+
|
|
10148
|
+
`ApplicationSnapshot` and `ApplicationStatusProjection` each gained a
|
|
10149
|
+
`restarting` field — ids in the snapshot, a count in the projection. A reader
|
|
10150
|
+
that names the fields it wants is unaffected. A test that compares the whole
|
|
10151
|
+
object with `toEqual` fails until it adds the field:
|
|
10152
|
+
|
|
10153
|
+
```ts
|
|
10154
|
+
// before
|
|
10155
|
+
expect(body).toEqual({ id, lifecycle: 'ready', /* … */ resources: { total: 1, ready: 1, degraded: 0, failed: 0 } })
|
|
10156
|
+
// after
|
|
10157
|
+
expect(body).toEqual({ id, lifecycle: 'ready', /* … */ resources: { total: 1, ready: 1, degraded: 0, failed: 0 }, restarting: 0 })
|
|
10158
|
+
```
|
|
10159
|
+
|
|
10160
|
+
It is zero except while a restart is running, and then it names the subtree being
|
|
10161
|
+
replaced. That is the point: a snapshot taken mid-restart used to be
|
|
10162
|
+
indistinguishable from a resource that had failed on its own, so a dashboard read
|
|
10163
|
+
a planned replacement as an outage.
|
|
10164
|
+
|
|
10165
|
+
**If you watch a hub across a restart**, nothing to change, but the behaviour is
|
|
10166
|
+
different and better: closing a hub now tells its subscribers `unavailable` /
|
|
10167
|
+
`source-unavailable` instead of dropping them, so a page stops showing a value
|
|
10168
|
+
that will never update again.
|
|
10169
|
+
|
|
10170
|
+
**If you need a tool contract in a browser**, `stitchkit/tools/contract` now
|
|
10171
|
+
carries the async-operation helpers, its schemas and the view-file input/output
|
|
10172
|
+
without the runtime. `stitchkit/tools` still exports all of them.
|
|
10173
|
+
|
|
10174
|
+
## Released migration: 0.78.0
|
|
10175
|
+
|
|
10176
|
+
Two mechanical edits. Find them both:
|
|
10177
|
+
|
|
10178
|
+
```bash
|
|
10179
|
+
rg -n "createDecisionPipeline|createDiagnosticJournal" --glob '*.ts' --glob '*.tsx'
|
|
10180
|
+
```
|
|
10181
|
+
|
|
10182
|
+
**1. `createDecisionPipeline` takes a deadline.**
|
|
10183
|
+
|
|
10184
|
+
```ts
|
|
10185
|
+
// before
|
|
10186
|
+
createDecisionPipeline([policyA, policyB])
|
|
10187
|
+
// after
|
|
10188
|
+
createDecisionPipeline([policyA, policyB], { policyTimeoutMs: 2_000 })
|
|
10189
|
+
```
|
|
10190
|
+
|
|
10191
|
+
Pick a number your slowest policy comfortably beats. There is no default on
|
|
10192
|
+
purpose: a policy that never settles hangs every caller of the operation it
|
|
10193
|
+
guards, and the framework has no basis for choosing that number for you.
|
|
10194
|
+
|
|
10195
|
+
Past the deadline — and on a throw, and on a non-decision — the pipeline raises
|
|
10196
|
+
`DecisionPolicyError` naming the policy, with the trace so far. If you already
|
|
10197
|
+
catch that for a bad return value, you now catch two more cases with it.
|
|
10198
|
+
|
|
10199
|
+
**2. `createDiagnosticJournal` moved.**
|
|
10200
|
+
|
|
10201
|
+
```ts
|
|
10202
|
+
// before
|
|
10203
|
+
import { createDiagnosticJournal } from 'stitchkit/application'
|
|
10204
|
+
// after
|
|
10205
|
+
import { createDiagnosticJournal } from 'stitchkit/application/diagnostic-journal'
|
|
10206
|
+
```
|
|
10207
|
+
|
|
10208
|
+
Only the factory moved. `DiagnosticJournalConfig`, every
|
|
10209
|
+
`DiagnosticJournal*Schema` and `readDiagnosticJournalLockDiagnosis` stay where
|
|
10210
|
+
they were.
|
|
10211
|
+
|
|
10212
|
+
**If you restart resources**, nothing to change — but three things that did not
|
|
10213
|
+
work now do: a managed schedule can be restarted, a restarted keyspace accepts
|
|
10214
|
+
writes again, and a restarted managed server is actually shut down at exit. If
|
|
10215
|
+
you pass `managedServerResource` a server *instance*, a restart is now refused
|
|
10216
|
+
by name; pass a factory (`server: (context) => …`) to restart it.
|
|
10217
|
+
|
|
10104
10218
|
## Released migration: 0.77.0
|
|
10105
10219
|
|
|
10106
10220
|
Two type renames, and only if you named them. Nothing runtime moved.
|
|
@@ -13570,7 +13684,7 @@ cutovers are covered by the executable
|
|
|
13570
13684
|
|--------|------|---------|
|
|
13571
13685
|
| `createApplication` | function | compose a validated resource DAG into one startup, readiness, admission and shutdown state machine |
|
|
13572
13686
|
| `ApplicationHandle.restart` | method | replace one resource and everything that depends on it, leaving the rest of the graph running and the process epoch unchanged |
|
|
13573
|
-
| `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id |
|
|
13687
|
+
| `ApplicationRestartInputSchema` / `ApplicationRestartInput` | schema / _type_ | the resource to replace, by id, and optionally `gracePeriodMs` / `forceTimeoutMs` for this restart — the application's own shutdown budget otherwise |
|
|
13574
13688
|
| `ApplicationRestartResultSchema` / `ApplicationRestartResult` | schema / _type_ | the subtree that was actually taken down and brought back, in start order, with the outcome, the reason on anything but success, and how long it took |
|
|
13575
13689
|
| `ApplicationRestartOutcomeSchema` / `ApplicationRestartOutcome` | schema / _type_ | `restarted`, `failed` or `refused` — a refusal (unknown id, shutting down, not yet ready) is not a failure and touches nothing |
|
|
13576
13690
|
| `ApplicationResourceFailure` | _type_ | one resource failure with the cause its phase label cannot carry — delivered to `onResourceFailure` |
|
|
@@ -13721,11 +13835,12 @@ vocabulary with an event topic declared `mode: 'decision'`.
|
|
|
13721
13835
|
| Export | Kind | Summary |
|
|
13722
13836
|
|--------|------|---------|
|
|
13723
13837
|
| `createDecisionPipeline` | function | run policies in declaration order; the first terminal verdict wins and the rest do not run |
|
|
13838
|
+
| `DecisionPipelineConfig` | _type_ | `policyTimeoutMs` — how long one policy has to answer. Required, no default: a policy that never settles hangs every caller of the operation it guards |
|
|
13724
13839
|
| `PolicyDecisionSchema` / `PolicyDecision` | schema / _type_ | `allow`, `deny` with a reason, or `defer` — a deny cannot be silent |
|
|
13725
13840
|
| `DecisionPolicy` / `DecisionPipeline` | _types_ | one named voter over the caller's subject, and the pipeline it composes into |
|
|
13726
13841
|
| `DecisionResult` / `DecisionTraceEntry` | _types_ | the verdict, and the trace of what actually ran — not what was declared |
|
|
13727
13842
|
| `DecisionUndecidedError` | class | every policy deferred and no undecided outcome was declared: an unanswered question, raised rather than guessed |
|
|
13728
|
-
| `DecisionPolicyError` | class | a policy threw or
|
|
13843
|
+
| `DecisionPolicyError` | class | a policy returned a non-decision, threw, or ran past `policyTimeoutMs` — one error for all three, carrying the policy id and the trace so far |
|
|
13729
13844
|
|
|
13730
13845
|
### Keyspace and watched reads
|
|
13731
13846
|
|
|
@@ -13733,7 +13848,7 @@ vocabulary with an event topic declared `mode: 'decision'`.
|
|
|
13733
13848
|
|--------|------|---------|
|
|
13734
13849
|
| `defineKeyspace` / `keyspaceResource` | function | a named record set read synchronously from memory and written through one serialised chain; backend first, then memory, then the change event |
|
|
13735
13850
|
| `openKeyspace` / `OpenedKeyspace` | function / type | the same keyspace opened directly, for an application that owns its own lifecycle rather than declaring resources to a kernel |
|
|
13736
|
-
| `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle |
|
|
13851
|
+
| `KeyspaceBackend` / `KeyspaceDeclaration` / `KeyspaceChange` / `KeyspaceResourceConfig` / `OpenKeyspace` | _types_ | the four-method durability port, the declaration, the announced change, the resource's options and the published handle. `backend` takes a value or a **factory** — a factory is called once per generation, so a restarted keyspace gets a backend that was never closed |
|
|
13737
13852
|
| `memoryKeyspaceBackend` / `sqliteKeyspaceBackend` / `SqliteKeyspaceBackendConfig` | function / type | a disposable in-process backend, and one table with a key and a JSON payload over a caller-owned database |
|
|
13738
13853
|
| `SqliteDatabase` / `SqliteStatement` / `SqliteValue` | _types_ | the minimal synchronous SQLite boundary the framework types against |
|
|
13739
13854
|
| `createWatchHub` / `watchKey` | function | one read per question however many are asking: single-flight per key, re-read on a declared topic, publish only what changed |
|
|
@@ -13770,6 +13885,23 @@ snapshots; the adapter owns no SDK lifecycle, polling or delta state.
|
|
|
13770
13885
|
| `ApplicationTelemetryMeter` | _type_ | minimal structural `Meter.createObservableGauge` boundary compatible with `@opentelemetry/api` |
|
|
13771
13886
|
| `ApplicationOpenTelemetryCollectionError` | _type_ | isolated instrument-name/error diagnostic without product/provider attributes |
|
|
13772
13887
|
|
|
13888
|
+
## `stitchkit/application/diagnostic-journal`
|
|
13889
|
+
|
|
13890
|
+
| Export | Kind | Summary |
|
|
13891
|
+
|--------|------|---------|
|
|
13892
|
+
| `createDiagnosticJournal` | function | the bounded local journal: a file lock, a spawn to diagnose a stale one, and framed writes |
|
|
13893
|
+
|
|
13894
|
+
Its own entrypoint because of what it reaches — `node:child_process`,
|
|
13895
|
+
`node:fs`, `node:os`, `node:util` — with `promisify(execFile)` evaluated while
|
|
13896
|
+
the module initialises. Exported from `stitchkit/application`, that one line made
|
|
13897
|
+
the entire barrel unusable in a browser bundle: not by failing at the call, but
|
|
13898
|
+
by throwing during module initialisation, on every route.
|
|
13899
|
+
|
|
13900
|
+
Its **contract** stays in `stitchkit/application` — every
|
|
13901
|
+
`DiagnosticJournal*Schema`, the states and the refusal reasons touch nothing but
|
|
13902
|
+
Zod, and a client reading a journal's status has as much right to them as the
|
|
13903
|
+
server writing it.
|
|
13904
|
+
|
|
13773
13905
|
## `stitchkit/application/schemas`
|
|
13774
13906
|
|
|
13775
13907
|
The canonical application records — and nothing else. Every export here is also
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stitchkit",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.79.0",
|
|
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",
|
|
@@ -61,6 +61,10 @@
|
|
|
61
61
|
"types": "./dist/tools.d.ts",
|
|
62
62
|
"import": "./dist/tools.js"
|
|
63
63
|
},
|
|
64
|
+
"./tools/contract": {
|
|
65
|
+
"types": "./dist/tools-contract.d.ts",
|
|
66
|
+
"import": "./dist/tools-contract.js"
|
|
67
|
+
},
|
|
64
68
|
"./tools/invoker": {
|
|
65
69
|
"types": "./dist/tool-invoker.d.ts",
|
|
66
70
|
"import": "./dist/tool-invoker.js"
|
|
@@ -129,6 +133,10 @@
|
|
|
129
133
|
"types": "./dist/application-opentelemetry.d.ts",
|
|
130
134
|
"import": "./dist/application-opentelemetry.js"
|
|
131
135
|
},
|
|
136
|
+
"./application/diagnostic-journal": {
|
|
137
|
+
"types": "./dist/application-diagnostic-journal.d.ts",
|
|
138
|
+
"import": "./dist/application-diagnostic-journal.js"
|
|
139
|
+
},
|
|
132
140
|
"./application/schemas": {
|
|
133
141
|
"types": "./dist/application-schemas.d.ts",
|
|
134
142
|
"import": "./dist/application-schemas.js"
|
|
@@ -160,18 +168,17 @@
|
|
|
160
168
|
"scripts": {
|
|
161
169
|
"build:native-contained-files": "node scripts/build-contained-files-native.mjs",
|
|
162
170
|
"check": "bun x tsc --noEmit",
|
|
163
|
-
"build:
|
|
164
|
-
"build:server": "bun build src/server/index.ts src/node.ts src/tools.ts src/tool-invoker.ts src/cli.ts src/remote.ts src/files.ts src/testing.ts src/observability/index.ts src/agent-runtime.ts src/agent-runtime-harness.ts src/agent-runtime-coding-tools.ts src/agent-runtime-openrouter.ts src/agent-runtime-sqlite-bun.ts src/agent-runtime-sqlite-node.ts src/application.ts src/application-grammy.ts src/application-opentelemetry.ts src/telegram.ts --outdir dist --target node --packages external --splitting --root src",
|
|
165
|
-
"build:js": "bun run build:browser && bun run build:server && bun scripts/preserve-webpack-ignore.mjs",
|
|
171
|
+
"build:js": "bun run build:entrypoints && bun scripts/preserve-webpack-ignore.mjs",
|
|
166
172
|
"build:types": "bun x tsc -p tsconfig.build.json --emitDeclarationOnly && bun scripts/rewrite-declaration-specifiers.mjs",
|
|
167
|
-
"build": "rm -rf dist && bun run build:js && bun run build:types && bun scripts/check-browser-clean.mjs && bun scripts/check-env-live.mjs && bun scripts/check-public-types.mjs",
|
|
168
|
-
"dev": "bun run build:
|
|
173
|
+
"build": "rm -rf dist && bun run build:js && bun run build:types && bun scripts/check-browser-clean.mjs && bun scripts/check-browser-executes.mjs && bun scripts/check-env-live.mjs && bun scripts/check-public-types.mjs",
|
|
174
|
+
"dev": "bun run build:entrypoints -- --watch",
|
|
169
175
|
"prepack": "cp ../../README.md ./README.md && bun ../../scripts/gen-llms.ts && bun run build",
|
|
170
176
|
"test": "bun test",
|
|
171
177
|
"smoke:node": "node scripts/node-smoke.mjs",
|
|
172
178
|
"smoke:next-ssr": "node scripts/next-ssr-retry-smoke.mjs",
|
|
173
179
|
"consumer-lane": "bun scripts/consumer-lane/run.mjs",
|
|
174
|
-
"bench:mcp-preparation": "bun scripts/benchmark-mcp-preparation.ts"
|
|
180
|
+
"bench:mcp-preparation": "bun scripts/benchmark-mcp-preparation.ts",
|
|
181
|
+
"build:entrypoints": "bun scripts/build-entrypoints.mjs"
|
|
175
182
|
},
|
|
176
183
|
"peerDependencies": {
|
|
177
184
|
"@modelcontextprotocol/ext-apps": "^1.7.2",
|
package/dist/index-ksp6e2ye.js
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
// src/internal/typed.ts
|
|
2
|
-
function typedEntries(value) {
|
|
3
|
-
return Object.entries(value);
|
|
4
|
-
}
|
|
5
|
-
function isRecord(value) {
|
|
6
|
-
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
7
|
-
}
|
|
8
|
-
function transportResult(value) {
|
|
9
|
-
return value;
|
|
10
|
-
}
|
|
11
|
-
function mapObject(source, mapper) {
|
|
12
|
-
const result = {};
|
|
13
|
-
for (const [key, value] of typedEntries(source)) {
|
|
14
|
-
const mapped = mapper(key, value);
|
|
15
|
-
if (mapped !== undefined)
|
|
16
|
-
result[key] = mapped;
|
|
17
|
-
}
|
|
18
|
-
return result;
|
|
19
|
-
}
|
|
20
|
-
function mapObjectTypeBoundary(source, mapper) {
|
|
21
|
-
return mapObject(source, (key, value) => mapper(key, value));
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export { typedEntries, isRecord, transportResult, mapObject, mapObjectTypeBoundary };
|
package/dist/index-vj3vvpaa.js
DELETED
|
@@ -1,307 +0,0 @@
|
|
|
1
|
-
// src/contract/define.ts
|
|
2
|
-
import { z } from "zod";
|
|
3
|
-
|
|
4
|
-
// src/internal/route-pattern.ts
|
|
5
|
-
var PARAM_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
6
|
-
function joinRoutePath(...parts) {
|
|
7
|
-
const joined = parts.filter((part) => Boolean(part)).map((part) => part.replace(/^\/+|\/+$/g, "")).filter(Boolean).join("/");
|
|
8
|
-
return `/${joined}`;
|
|
9
|
-
}
|
|
10
|
-
function parseTrailingWildcard(path) {
|
|
11
|
-
const segments = path.split("/").filter(Boolean);
|
|
12
|
-
const paramNames = new Set;
|
|
13
|
-
let wildcard = null;
|
|
14
|
-
for (const [segmentIndex, segment] of segments.entries()) {
|
|
15
|
-
if (segment.startsWith(":")) {
|
|
16
|
-
const name2 = segment.slice(1);
|
|
17
|
-
if (!PARAM_IDENTIFIER.test(name2)) {
|
|
18
|
-
throw new Error(`Invalid route parameter name "${name2}" in path "${path}"`);
|
|
19
|
-
}
|
|
20
|
-
if (paramNames.has(name2)) {
|
|
21
|
-
throw new Error(`Duplicate route parameter name "${name2}" in path "${path}"`);
|
|
22
|
-
}
|
|
23
|
-
paramNames.add(name2);
|
|
24
|
-
continue;
|
|
25
|
-
}
|
|
26
|
-
if (!segment.startsWith("*")) {
|
|
27
|
-
if (segment.includes("*")) {
|
|
28
|
-
throw new Error(`Wildcard must occupy its own segment in path "${path}"`);
|
|
29
|
-
}
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
const name = segment.slice(1);
|
|
33
|
-
if (!PARAM_IDENTIFIER.test(name)) {
|
|
34
|
-
throw new Error(`Trailing wildcard in path "${path}" must be named, for example "/*filePath"`);
|
|
35
|
-
}
|
|
36
|
-
if (segmentIndex !== segments.length - 1) {
|
|
37
|
-
throw new Error(`Wildcard "*${name}" must be the final segment in path "${path}"`);
|
|
38
|
-
}
|
|
39
|
-
if (wildcard) {
|
|
40
|
-
throw new Error(`Path "${path}" contains more than one wildcard`);
|
|
41
|
-
}
|
|
42
|
-
if (paramNames.has(name)) {
|
|
43
|
-
throw new Error(`Duplicate route parameter name "${name}" in path "${path}"`);
|
|
44
|
-
}
|
|
45
|
-
wildcard = { name, segmentIndex };
|
|
46
|
-
}
|
|
47
|
-
return wildcard;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
// src/internal/safe-json.ts
|
|
51
|
-
function isUnsafeKey(key) {
|
|
52
|
-
return key === "__proto__";
|
|
53
|
-
}
|
|
54
|
-
function safeJsonParse(text) {
|
|
55
|
-
return JSON.parse(text, (key, value) => isUnsafeKey(key) ? undefined : value);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// src/contract/define.ts
|
|
59
|
-
function defineContract(meta, endpoints) {
|
|
60
|
-
const toolTransports = new Map;
|
|
61
|
-
for (const [key, ep] of Object.entries(endpoints)) {
|
|
62
|
-
const wildcard = parseTrailingWildcard(ep.path);
|
|
63
|
-
if (wildcard) {
|
|
64
|
-
if (!ep.params) {
|
|
65
|
-
throw new Error(`Contract "${meta.prefix}": endpoint "${key}" wildcard "${wildcard.name}" requires a params schema field`);
|
|
66
|
-
}
|
|
67
|
-
const paramsJson = z.toJSONSchema(ep.params, { io: "input" });
|
|
68
|
-
if (!paramsJson.properties || !(wildcard.name in paramsJson.properties)) {
|
|
69
|
-
throw new Error(`Contract "${meta.prefix}": endpoint "${key}" params schema is missing wildcard field "${wildcard.name}"`);
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
if (ep.desc.trim() === "") {
|
|
73
|
-
throw new Error(`Contract "${meta.prefix}": endpoint "${key}" has an empty desc`);
|
|
74
|
-
}
|
|
75
|
-
if (ep.maxJsonBodyBytes !== undefined && (!Number.isSafeInteger(ep.maxJsonBodyBytes) || ep.maxJsonBodyBytes <= 0)) {
|
|
76
|
-
throw new Error(`Contract "${meta.prefix}": endpoint "${key}" maxJsonBodyBytes must be a positive safe integer, received ${ep.maxJsonBodyBytes}`);
|
|
77
|
-
}
|
|
78
|
-
if (ep.multipart)
|
|
79
|
-
assertMultipartEndpoint(meta.prefix, key, ep);
|
|
80
|
-
if (ep.rawResponse)
|
|
81
|
-
assertRawEndpoint(meta.prefix, key, ep);
|
|
82
|
-
if ("stream" in ep)
|
|
83
|
-
assertStreamingResponseEndpoint(meta.prefix, key, ep);
|
|
84
|
-
if (ep.method === "HEAD")
|
|
85
|
-
assertHeadEndpoint(meta.prefix, key, ep);
|
|
86
|
-
if (ep.rawBody)
|
|
87
|
-
assertRawBodyEndpoint(meta.prefix, key, ep);
|
|
88
|
-
if ("responseMeta" in ep)
|
|
89
|
-
assertResponseMetaEndpoint(meta.prefix, key, ep);
|
|
90
|
-
if (!("toolName" in ep) || !ep.toolName)
|
|
91
|
-
continue;
|
|
92
|
-
const transports = new Set(ep.expose ? ep.expose.filter((t) => t !== "HTTP") : ["MCP", "AGENT"]);
|
|
93
|
-
if (transports.size === 0) {
|
|
94
|
-
throw new Error(`Contract "${meta.prefix}": endpoint "${key}" sets toolName "${ep.toolName}" but is not exposed on any tool transport (MCP / AGENT)`);
|
|
95
|
-
}
|
|
96
|
-
const existing = toolTransports.get(ep.toolName);
|
|
97
|
-
if (existing) {
|
|
98
|
-
for (const t of transports) {
|
|
99
|
-
if (existing.transports.has(t)) {
|
|
100
|
-
throw new Error(`Contract "${meta.prefix}": duplicate toolName "${ep.toolName}" on transport "${t}" (endpoints: "${existing.key}" and "${key}")`);
|
|
101
|
-
}
|
|
102
|
-
existing.transports.add(t);
|
|
103
|
-
}
|
|
104
|
-
} else {
|
|
105
|
-
toolTransports.set(ep.toolName, { key, transports });
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
return { meta, endpoints };
|
|
109
|
-
}
|
|
110
|
-
function assertPositiveLimit(where, name, value) {
|
|
111
|
-
if (value !== undefined && (!Number.isSafeInteger(value) || value <= 0)) {
|
|
112
|
-
throw new Error(`${where} ${name} must be a positive safe integer, received ${value}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
function assertMultipartEndpoint(prefix, key, ep) {
|
|
116
|
-
const where = `Contract "${prefix}": multipart endpoint "${key}"`;
|
|
117
|
-
if (ep.method !== "POST" && ep.method !== "PUT" && ep.method !== "PATCH") {
|
|
118
|
-
throw new Error(`${where} must use POST, PUT or PATCH`);
|
|
119
|
-
}
|
|
120
|
-
const multipart = ep.multipart;
|
|
121
|
-
if (!multipart || typeof multipart !== "object") {
|
|
122
|
-
throw new Error(`${where} must declare a multipart descriptor`);
|
|
123
|
-
}
|
|
124
|
-
assertPositiveLimit(where, "maxRequestBytes", multipart.maxRequestBytes);
|
|
125
|
-
assertPositiveLimit(where, "maxFieldBytes", multipart.maxFieldBytes);
|
|
126
|
-
const entries = Object.entries(multipart.files);
|
|
127
|
-
if (entries.length === 0)
|
|
128
|
-
throw new Error(`${where} must declare at least one file field`);
|
|
129
|
-
for (const [field, policy] of entries) {
|
|
130
|
-
if (!field || isUnsafeKey(field))
|
|
131
|
-
throw new Error(`${where} has an invalid file field name`);
|
|
132
|
-
assertPositiveLimit(`${where} field "${field}"`, "maxBytes", policy.maxBytes);
|
|
133
|
-
assertPositiveLimit(`${where} field "${field}"`, "maxFiles", policy.maxFiles);
|
|
134
|
-
if (policy.multiple !== true && policy.maxFiles !== undefined) {
|
|
135
|
-
throw new Error(`${where} field "${field}" may set maxFiles only with multiple: true`);
|
|
136
|
-
}
|
|
137
|
-
if (policy.contentTypes) {
|
|
138
|
-
if (policy.contentTypes.length === 0) {
|
|
139
|
-
throw new Error(`${where} field "${field}" contentTypes cannot be empty`);
|
|
140
|
-
}
|
|
141
|
-
for (const contentType of policy.contentTypes) {
|
|
142
|
-
if (!/^[a-z0-9!#$&^_.+-]+\/(?:[a-z0-9!#$&^_.+-]+|\*)$/i.test(contentType)) {
|
|
143
|
-
throw new Error(`${where} field "${field}" has invalid content type policy "${contentType}"`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
}
|
|
149
|
-
function assertRawEndpoint(prefix, key, ep) {
|
|
150
|
-
const where = `Contract "${prefix}": raw endpoint "${key}"`;
|
|
151
|
-
if (ep.output)
|
|
152
|
-
throw new Error(`${where} cannot declare an output schema`);
|
|
153
|
-
if ("toolName" in ep && ep.toolName)
|
|
154
|
-
throw new Error(`${where} cannot set a toolName`);
|
|
155
|
-
if ("ui" in ep && ep.ui)
|
|
156
|
-
throw new Error(`${where} cannot set MCP ui metadata`);
|
|
157
|
-
if ("annotations" in ep && ep.annotations) {
|
|
158
|
-
throw new Error(`${where} cannot set MCP annotations`);
|
|
159
|
-
}
|
|
160
|
-
const nonHttp = (ep.expose ?? []).filter((t) => t !== "HTTP");
|
|
161
|
-
if (nonHttp.length > 0) {
|
|
162
|
-
throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
|
|
163
|
-
}
|
|
164
|
-
}
|
|
165
|
-
function assertStreamingResponseEndpoint(prefix, key, ep) {
|
|
166
|
-
const where = `Contract "${prefix}": streaming endpoint "${key}"`;
|
|
167
|
-
if (!("stream" in ep) || !ep.stream || typeof ep.stream !== "object") {
|
|
168
|
-
throw new Error(`${where} must declare a stream descriptor`);
|
|
169
|
-
}
|
|
170
|
-
if (!ep.stream.item || typeof ep.stream.item.parse !== "function") {
|
|
171
|
-
throw new Error(`${where} must declare an item schema`);
|
|
172
|
-
}
|
|
173
|
-
const runtimeFormat = Reflect.get(ep.stream, "format");
|
|
174
|
-
const runtimeFinalLine = Reflect.get(ep.stream, "finalLine");
|
|
175
|
-
if (ep.stream.framing === "item" && runtimeFormat === "sse") {
|
|
176
|
-
throw new Error(`${where} item framing is supported only for ndjson`);
|
|
177
|
-
}
|
|
178
|
-
if (ep.stream.framing === "item" && ep.stream.completion !== "terminal") {
|
|
179
|
-
throw new Error(`${where} item framing requires terminal completion`);
|
|
180
|
-
}
|
|
181
|
-
if (ep.stream.completion === "terminal" && !ep.stream.terminal) {
|
|
182
|
-
throw new Error(`${where} terminal completion requires a terminal schema`);
|
|
183
|
-
}
|
|
184
|
-
if (runtimeFinalLine === "require-newline" && runtimeFormat === "sse") {
|
|
185
|
-
throw new Error(`${where} finalLine applies only to ndjson`);
|
|
186
|
-
}
|
|
187
|
-
assertPositiveLimit(where, "maxFrameBytes", ep.stream.maxFrameBytes);
|
|
188
|
-
assertPositiveLimit(where, "lifetimeMs", ep.stream.lifetimeMs);
|
|
189
|
-
assertPositiveLimit(where, "heartbeatMs", ep.stream.heartbeatMs);
|
|
190
|
-
if (ep.stream.idleTimeoutSeconds !== undefined && (!Number.isSafeInteger(ep.stream.idleTimeoutSeconds) || ep.stream.idleTimeoutSeconds < 0)) {
|
|
191
|
-
throw new Error(`${where} idleTimeoutSeconds must be a non-negative safe integer`);
|
|
192
|
-
}
|
|
193
|
-
if (ep.output)
|
|
194
|
-
throw new Error(`${where} cannot declare an output schema`);
|
|
195
|
-
if (ep.rawResponse)
|
|
196
|
-
throw new Error(`${where} cannot also be rawResponse`);
|
|
197
|
-
if (ep.multipart)
|
|
198
|
-
throw new Error(`${where} cannot be multipart`);
|
|
199
|
-
if ("toolName" in ep && ep.toolName)
|
|
200
|
-
throw new Error(`${where} cannot set a toolName`);
|
|
201
|
-
const nonHttp = (ep.expose ?? []).filter((transport) => transport !== "HTTP");
|
|
202
|
-
if (nonHttp.length > 0) {
|
|
203
|
-
throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
|
|
204
|
-
}
|
|
205
|
-
}
|
|
206
|
-
function assertHeadEndpoint(prefix, key, ep) {
|
|
207
|
-
const where = `Contract "${prefix}": HEAD endpoint "${key}"`;
|
|
208
|
-
if (!ep.rawResponse)
|
|
209
|
-
throw new Error(`${where} must declare rawResponse: true`);
|
|
210
|
-
if (ep.input)
|
|
211
|
-
throw new Error(`${where} cannot declare an input schema`);
|
|
212
|
-
if (ep.multipart)
|
|
213
|
-
throw new Error(`${where} cannot be multipart`);
|
|
214
|
-
if (ep.rawBody)
|
|
215
|
-
throw new Error(`${where} cannot retain a raw body`);
|
|
216
|
-
}
|
|
217
|
-
function assertRawBodyEndpoint(prefix, key, ep) {
|
|
218
|
-
const where = `Contract "${prefix}": rawBody endpoint "${key}"`;
|
|
219
|
-
if (!ep.input)
|
|
220
|
-
throw new Error(`${where} must declare an input schema`);
|
|
221
|
-
if (ep.multipart)
|
|
222
|
-
throw new Error(`${where} cannot be multipart`);
|
|
223
|
-
if (ep.method !== "POST" && ep.method !== "PUT" && ep.method !== "PATCH") {
|
|
224
|
-
throw new Error(`${where} must use POST, PUT or PATCH`);
|
|
225
|
-
}
|
|
226
|
-
if ("toolName" in ep && ep.toolName)
|
|
227
|
-
throw new Error(`${where} cannot set a toolName`);
|
|
228
|
-
if ("ui" in ep && ep.ui)
|
|
229
|
-
throw new Error(`${where} cannot set MCP ui metadata`);
|
|
230
|
-
if ("annotations" in ep && ep.annotations) {
|
|
231
|
-
throw new Error(`${where} cannot set MCP annotations`);
|
|
232
|
-
}
|
|
233
|
-
const nonHttp = (ep.expose ?? []).filter((transport) => transport !== "HTTP");
|
|
234
|
-
if (nonHttp.length > 0) {
|
|
235
|
-
throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
|
|
236
|
-
}
|
|
237
|
-
}
|
|
238
|
-
function assertResponseMetaEndpoint(prefix, key, ep) {
|
|
239
|
-
const where = `Contract "${prefix}": responseMeta endpoint "${key}"`;
|
|
240
|
-
if (!ep.responseMeta || typeof ep.responseMeta !== "object") {
|
|
241
|
-
throw new Error(`${where} must declare responseMeta as an object`);
|
|
242
|
-
}
|
|
243
|
-
const status = ep.responseMeta.status;
|
|
244
|
-
if (status !== undefined && (!Number.isSafeInteger(status) || status < 200 || status > 299)) {
|
|
245
|
-
throw new Error(`${where} status must be a successful 2xx integer, received ${status}`);
|
|
246
|
-
}
|
|
247
|
-
if (ep.output && (status === 204 || status === 205)) {
|
|
248
|
-
throw new Error(`${where} cannot combine output with bodyless status ${status}`);
|
|
249
|
-
}
|
|
250
|
-
if (ep.rawResponse)
|
|
251
|
-
throw new Error(`${where} cannot also be a rawResponse endpoint`);
|
|
252
|
-
if ("toolName" in ep && ep.toolName)
|
|
253
|
-
throw new Error(`${where} cannot set a toolName`);
|
|
254
|
-
if ("ui" in ep && ep.ui)
|
|
255
|
-
throw new Error(`${where} cannot set MCP ui metadata`);
|
|
256
|
-
if ("annotations" in ep && ep.annotations) {
|
|
257
|
-
throw new Error(`${where} cannot set MCP annotations`);
|
|
258
|
-
}
|
|
259
|
-
const nonHttp = (ep.expose ?? []).filter((transport) => transport !== "HTTP");
|
|
260
|
-
if (nonHttp.length > 0) {
|
|
261
|
-
throw new Error(`${where} is HTTP-only — remove ${nonHttp.join(", ")} from expose`);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
function mergeMeta(contractMeta, endpointMeta) {
|
|
265
|
-
if (!contractMeta)
|
|
266
|
-
return endpointMeta;
|
|
267
|
-
if (!endpointMeta)
|
|
268
|
-
return { ...contractMeta };
|
|
269
|
-
return { ...contractMeta, ...endpointMeta };
|
|
270
|
-
}
|
|
271
|
-
// src/contract/errors-factory.ts
|
|
272
|
-
import { z as z2 } from "zod";
|
|
273
|
-
// src/contract/pagination.ts
|
|
274
|
-
import { z as z3 } from "zod";
|
|
275
|
-
|
|
276
|
-
// src/internal/base64url.ts
|
|
277
|
-
function bytesToBase64Url(bytes) {
|
|
278
|
-
let binary = "";
|
|
279
|
-
for (const byte of bytes)
|
|
280
|
-
binary += String.fromCharCode(byte);
|
|
281
|
-
return btoa(binary).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
|
|
282
|
-
}
|
|
283
|
-
function base64UrlToBytes(segment) {
|
|
284
|
-
if (!/^[A-Za-z0-9_-]*$/.test(segment) || segment.length % 4 === 1) {
|
|
285
|
-
throw new Error("invalid base64url segment");
|
|
286
|
-
}
|
|
287
|
-
const b64 = segment.replace(/-/g, "+").replace(/_/g, "/");
|
|
288
|
-
const padded = b64.padEnd(Math.ceil(b64.length / 4) * 4, "=");
|
|
289
|
-
return Uint8Array.from(atob(padded), (c) => c.charCodeAt(0));
|
|
290
|
-
}
|
|
291
|
-
// src/contract/stream.ts
|
|
292
|
-
import { z as z4 } from "zod";
|
|
293
|
-
var DEFAULT_CONTRACT_STREAM_FRAME_BYTES = 256 * 1024;
|
|
294
|
-
var ContractStreamFrameSchema = z4.discriminatedUnion("type", [
|
|
295
|
-
z4.object({ type: z4.literal("data"), data: z4.unknown() }).strict(),
|
|
296
|
-
z4.object({
|
|
297
|
-
type: z4.literal("error"),
|
|
298
|
-
error: z4.object({
|
|
299
|
-
code: z4.string(),
|
|
300
|
-
message: z4.string().optional(),
|
|
301
|
-
details: z4.unknown().optional(),
|
|
302
|
-
hint: z4.string().optional()
|
|
303
|
-
}).strict()
|
|
304
|
-
}).strict(),
|
|
305
|
-
z4.object({ type: z4.literal("end") }).strict()
|
|
306
|
-
]);
|
|
307
|
-
export { joinRoutePath, parseTrailingWildcard, isUnsafeKey, safeJsonParse, defineContract, mergeMeta, bytesToBase64Url, base64UrlToBytes, DEFAULT_CONTRACT_STREAM_FRAME_BYTES, ContractStreamFrameSchema };
|