@briancray/belte 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (178) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +49 -0
  3. package/bin/belte.ts +136 -0
  4. package/package.json +80 -0
  5. package/src/App.svelte +31 -0
  6. package/src/assets/app.html +14 -0
  7. package/src/belteResolverPlugin.ts +832 -0
  8. package/src/build.ts +144 -0
  9. package/src/buildCli.ts +160 -0
  10. package/src/cliEntry.ts +31 -0
  11. package/src/clientEntry.ts +7 -0
  12. package/src/compile.ts +64 -0
  13. package/src/devEntry.ts +33 -0
  14. package/src/discoveryEntry.ts +33 -0
  15. package/src/lib/browser/cache.ts +191 -0
  16. package/src/lib/browser/page.svelte.ts +215 -0
  17. package/src/lib/browser/remoteProxy.ts +44 -0
  18. package/src/lib/browser/socketChannel.ts +182 -0
  19. package/src/lib/browser/socketProxy.ts +64 -0
  20. package/src/lib/browser/startClient.ts +132 -0
  21. package/src/lib/browser/subscribe.ts +131 -0
  22. package/src/lib/browser/types/Layouts.ts +7 -0
  23. package/src/lib/browser/types/Pages.ts +7 -0
  24. package/src/lib/cli/createClient.ts +126 -0
  25. package/src/lib/cli/loadEnvFromBinaryDir.ts +44 -0
  26. package/src/lib/cli/parseArgvForRpc.ts +97 -0
  27. package/src/lib/cli/printHelp.ts +70 -0
  28. package/src/lib/cli/runCli.ts +88 -0
  29. package/src/lib/cli/types/CliManifest.ts +9 -0
  30. package/src/lib/cli/types/CliManifestEntry.ts +12 -0
  31. package/src/lib/mcp/createMcpResourceServer.ts +101 -0
  32. package/src/lib/mcp/createMcpServer.ts +40 -0
  33. package/src/lib/mcp/dispatchMcpRequest.ts +294 -0
  34. package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
  35. package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
  36. package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
  37. package/src/lib/mcp/types/McpResourceContents.ts +10 -0
  38. package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
  39. package/src/lib/mcp/types/McpResourceServer.ts +12 -0
  40. package/src/lib/mcp/types/McpServer.ts +9 -0
  41. package/src/lib/mcp/types/McpServerOptions.ts +16 -0
  42. package/src/lib/server/AppModule.ts +25 -0
  43. package/src/lib/server/DELETE.ts +9 -0
  44. package/src/lib/server/GET.ts +9 -0
  45. package/src/lib/server/HEAD.ts +9 -0
  46. package/src/lib/server/HttpError.ts +19 -0
  47. package/src/lib/server/PATCH.ts +9 -0
  48. package/src/lib/server/POST.ts +9 -0
  49. package/src/lib/server/PUT.ts +9 -0
  50. package/src/lib/server/cli/buildEnvContent.ts +18 -0
  51. package/src/lib/server/cli/createTarGz.ts +76 -0
  52. package/src/lib/server/cli/handleCliDownload.ts +124 -0
  53. package/src/lib/server/cli/handleCliInstall.ts +20 -0
  54. package/src/lib/server/cli/installScript.ts +29 -0
  55. package/src/lib/server/cli/maxSourceMtime.ts +27 -0
  56. package/src/lib/server/error.ts +56 -0
  57. package/src/lib/server/json.ts +28 -0
  58. package/src/lib/server/jsonl.ts +40 -0
  59. package/src/lib/server/prompt.ts +30 -0
  60. package/src/lib/server/prompts/definePrompt.ts +20 -0
  61. package/src/lib/server/prompts/promptRegistry.ts +9 -0
  62. package/src/lib/server/prompts/registerPrompt.ts +6 -0
  63. package/src/lib/server/prompts/types/Prompt.ts +14 -0
  64. package/src/lib/server/prompts/types/PromptMessage.ts +10 -0
  65. package/src/lib/server/prompts/types/PromptOptions.ts +17 -0
  66. package/src/lib/server/prompts/types/PromptRegistryEntry.ts +15 -0
  67. package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
  68. package/src/lib/server/redirect.ts +37 -0
  69. package/src/lib/server/request.ts +18 -0
  70. package/src/lib/server/rpc/defineVerb.ts +103 -0
  71. package/src/lib/server/rpc/parseArgs.ts +60 -0
  72. package/src/lib/server/rpc/registerVerb.ts +6 -0
  73. package/src/lib/server/rpc/types/HttpVerb.ts +1 -0
  74. package/src/lib/server/rpc/types/RawRemoteFunction.ts +13 -0
  75. package/src/lib/server/rpc/types/RemoteFunction.ts +35 -0
  76. package/src/lib/server/rpc/types/RemoteHandler.ts +22 -0
  77. package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
  78. package/src/lib/server/rpc/types/StandardSchemaV1.ts +57 -0
  79. package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
  80. package/src/lib/server/rpc/types/VerbHelper.ts +39 -0
  81. package/src/lib/server/rpc/types/VerbRegistryEntry.ts +17 -0
  82. package/src/lib/server/rpc/unprocessed.ts +14 -0
  83. package/src/lib/server/rpc/verbRegistry.ts +11 -0
  84. package/src/lib/server/runtime/buildOpenApiSpec.ts +66 -0
  85. package/src/lib/server/runtime/cacheControlForAsset.ts +17 -0
  86. package/src/lib/server/runtime/containsTraversal.ts +37 -0
  87. package/src/lib/server/runtime/createPublicAssetServer.ts +66 -0
  88. package/src/lib/server/runtime/createServer.ts +555 -0
  89. package/src/lib/server/runtime/getActiveServer.ts +6 -0
  90. package/src/lib/server/runtime/mimeForExtension.ts +20 -0
  91. package/src/lib/server/runtime/registryManifests.ts +48 -0
  92. package/src/lib/server/runtime/requestContext.ts +5 -0
  93. package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
  94. package/src/lib/server/runtime/serializeCacheSnapshot.ts +84 -0
  95. package/src/lib/server/runtime/serverSlot.ts +13 -0
  96. package/src/lib/server/runtime/setActiveServer.ts +6 -0
  97. package/src/lib/server/runtime/streamFromIterator.ts +76 -0
  98. package/src/lib/server/runtime/types/Assets.ts +1 -0
  99. package/src/lib/server/runtime/types/CompileTarget.ts +6 -0
  100. package/src/lib/server/runtime/types/RequestStore.ts +15 -0
  101. package/src/lib/server/runtime/types/SvelteConfig.ts +5 -0
  102. package/src/lib/server/server.ts +19 -0
  103. package/src/lib/server/socket.ts +31 -0
  104. package/src/lib/server/sockets/createSocketDispatcher.ts +267 -0
  105. package/src/lib/server/sockets/defineSocket.ts +160 -0
  106. package/src/lib/server/sockets/lookupSocket.ts +6 -0
  107. package/src/lib/server/sockets/registerSocket.ts +6 -0
  108. package/src/lib/server/sockets/socketRegistry.ts +9 -0
  109. package/src/lib/server/sockets/types/Socket.ts +21 -0
  110. package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
  111. package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
  112. package/src/lib/server/sockets/types/SocketRegistryEntry.ts +18 -0
  113. package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
  114. package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
  115. package/src/lib/server/sse.ts +47 -0
  116. package/src/lib/shared/activeCacheStore.ts +20 -0
  117. package/src/lib/shared/buildRpcRequest.ts +61 -0
  118. package/src/lib/shared/cacheControlValues.ts +8 -0
  119. package/src/lib/shared/cacheStoreSlot.ts +16 -0
  120. package/src/lib/shared/canonicalJson.ts +24 -0
  121. package/src/lib/shared/commandNameForUrl.ts +17 -0
  122. package/src/lib/shared/createCacheStore.ts +42 -0
  123. package/src/lib/shared/createPushIterator.ts +77 -0
  124. package/src/lib/shared/createRemoteFunction.ts +89 -0
  125. package/src/lib/shared/decodeResponse.ts +47 -0
  126. package/src/lib/shared/detectTarget.ts +27 -0
  127. package/src/lib/shared/findExportCallSite.ts +479 -0
  128. package/src/lib/shared/forwardHeaders.ts +28 -0
  129. package/src/lib/shared/getRemoteMeta.ts +5 -0
  130. package/src/lib/shared/isDebugEnabled.ts +23 -0
  131. package/src/lib/shared/jsonSchemaForSchema.ts +38 -0
  132. package/src/lib/shared/keyForRemoteCall.ts +38 -0
  133. package/src/lib/shared/loadSvelteConfig.ts +18 -0
  134. package/src/lib/shared/log.ts +104 -0
  135. package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
  136. package/src/lib/shared/normalizeTarget.ts +10 -0
  137. package/src/lib/shared/pageUrlForFile.ts +14 -0
  138. package/src/lib/shared/parseRouteSegments.ts +22 -0
  139. package/src/lib/shared/preparePromptModule.ts +36 -0
  140. package/src/lib/shared/prepareRpcModule.ts +51 -0
  141. package/src/lib/shared/prepareSocketModule.ts +37 -0
  142. package/src/lib/shared/programNameForPackage.ts +14 -0
  143. package/src/lib/shared/promptNameForFile.ts +10 -0
  144. package/src/lib/shared/recordRemoteMeta.ts +5 -0
  145. package/src/lib/shared/remoteMetaStore.ts +16 -0
  146. package/src/lib/shared/resolveClientFlags.ts +18 -0
  147. package/src/lib/shared/rpcUrlForFile.ts +19 -0
  148. package/src/lib/shared/setCacheStoreResolver.ts +6 -0
  149. package/src/lib/shared/socketNameForFile.ts +11 -0
  150. package/src/lib/shared/streamingContentTypes.ts +11 -0
  151. package/src/lib/shared/stripImport.ts +27 -0
  152. package/src/lib/shared/subscribableFromResponse.ts +333 -0
  153. package/src/lib/shared/toBunRoutePattern.ts +28 -0
  154. package/src/lib/shared/types/CacheEntry.ts +16 -0
  155. package/src/lib/shared/types/CacheOptions.ts +10 -0
  156. package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
  157. package/src/lib/shared/types/CacheStore.ts +15 -0
  158. package/src/lib/shared/types/ClientFlags.ts +11 -0
  159. package/src/lib/shared/types/Subscribable.ts +15 -0
  160. package/src/lib/shared/writeRoutesDts.ts +64 -0
  161. package/src/preload.ts +20 -0
  162. package/src/scaffold.ts +92 -0
  163. package/src/serverEntry.ts +47 -0
  164. package/src/sveltePlugin.ts +58 -0
  165. package/src/tailwindStylePreprocessor.ts +62 -0
  166. package/template/package.json +16 -0
  167. package/template/src/app.ts +23 -0
  168. package/template/src/browser/app.css +21 -0
  169. package/template/src/browser/app.html +24 -0
  170. package/template/src/browser/pages/about/page.svelte +5 -0
  171. package/template/src/browser/pages/layout.svelte +26 -0
  172. package/template/src/browser/pages/page.svelte +20 -0
  173. package/template/src/cli/banner.txt +3 -0
  174. package/template/src/cli/footer.txt +1 -0
  175. package/template/src/server/rpc/getHello.ts +33 -0
  176. package/template/svelte.config.js +12 -0
  177. package/template/tsconfig.json +18 -0
  178. package/tsconfig.app.json +16 -0
@@ -0,0 +1,103 @@
1
+ import { buildRpcRequest } from '../../shared/buildRpcRequest.ts'
2
+ import { NO_STORE } from '../../shared/cacheControlValues.ts'
3
+ import { createRemoteFunction } from '../../shared/createRemoteFunction.ts'
4
+ import { forwardHeaders } from '../../shared/forwardHeaders.ts'
5
+ import { resolveClientFlags } from '../../shared/resolveClientFlags.ts'
6
+ import type { ClientFlags } from '../../shared/types/ClientFlags.ts'
7
+ import { requestContext } from '../runtime/requestContext.ts'
8
+ import { parseArgs } from './parseArgs.ts'
9
+ import { registerVerb } from './registerVerb.ts'
10
+ import type { HttpVerb } from './types/HttpVerb.ts'
11
+ import type { RemoteFunction } from './types/RemoteFunction.ts'
12
+ import type { RemoteHandler } from './types/RemoteHandler.ts'
13
+ import type { StandardSchemaV1 } from './types/StandardSchemaV1.ts'
14
+
15
+ /*
16
+ Builds a RemoteFunction from an HTTP verb + RPC URL + handler. The bundler
17
+ rewrites every `export const VERB = handler(fn)` inside an `$rpc/**` module
18
+ so the verb (from the export name) and the URL (from the file path under
19
+ `src/server/rpc/`, with `/rpc/` prefix) are threaded into defineVerb.
20
+
21
+ The plain call (`fn(args)`) resolves to the Content-Type-decoded body;
22
+ non-2xx responses throw HttpError. `.raw(args)` returns the underlying
23
+ Response for callers that need status/headers/body streaming.
24
+ `.fetch(req)` is the dispatch hook the framework's router uses to
25
+ invoke the handler from an incoming HTTP request (with args parsed off
26
+ the Request via parseArgs).
27
+
28
+ Every raw invocation records the synthesized Request against the returned
29
+ promise so cache() can stash it on the entry without re-building.
30
+ */
31
+ export function defineVerb<Args, Return>(
32
+ method: HttpVerb,
33
+ url: string,
34
+ handler: RemoteHandler<Args, Return>,
35
+ opts?: {
36
+ schema?: StandardSchemaV1
37
+ jsonSchema?: Record<string, unknown>
38
+ clients?: Partial<ClientFlags>
39
+ },
40
+ ): RemoteFunction<Args, Return> {
41
+ const schema = opts?.schema
42
+ const jsonSchema = opts?.jsonSchema
43
+ const clients = resolveClientFlags(opts?.clients, schema !== undefined)
44
+
45
+ function buildRequest(args: Args | undefined): Request {
46
+ const store = requestContext.getStore()
47
+ const baseUrl = store ? store.url.href : 'http://localhost/'
48
+ const headers = store ? forwardHeaders(store.req.headers) : new Headers()
49
+ return buildRpcRequest({ method, url, args, baseUrl, headers })
50
+ }
51
+
52
+ /*
53
+ Handler bodies may throw synchronously (e.g. an `assert(...)` at the
54
+ top of the function). The `async function` wrapper coerces both sync
55
+ throws and returned non-promises into the Promise<Response> shape
56
+ callers expect, so an SSR caller's `await` always sees the rejection
57
+ through the cache layer's snapshot boundary instead of the error
58
+ escaping the request scope.
59
+ */
60
+ async function runHandler(args: Args | undefined): Promise<Response> {
61
+ return handler(args as Args) as unknown as Response
62
+ }
63
+
64
+ async function validateThenHandle(args: Args | undefined): Promise<Response> {
65
+ const result = await schema!['~standard'].validate(args)
66
+ if (result.issues) {
67
+ return new Response(JSON.stringify({ issues: result.issues }), {
68
+ status: 422,
69
+ headers: {
70
+ 'Content-Type': 'application/json',
71
+ 'Cache-Control': NO_STORE,
72
+ },
73
+ })
74
+ }
75
+ return runHandler(result.value as Args)
76
+ }
77
+
78
+ /*
79
+ `getRequest` is unused on the server path — handlers receive parsed
80
+ `args` directly. createRemoteFunction passes a thunk so the client
81
+ side can lazily synthesize its Request without forcing the server
82
+ to allocate one per SSR call.
83
+ */
84
+ function invoke(args: Args | undefined): Promise<Response> {
85
+ return schema ? validateThenHandle(args) : runHandler(args)
86
+ }
87
+
88
+ const remote = createRemoteFunction<Args, Return>({
89
+ method,
90
+ url,
91
+ clients,
92
+ buildRequest,
93
+ invoke,
94
+ parseArgsForFetch: (request) => parseArgs(method, request) as Promise<Args | undefined>,
95
+ })
96
+ registerVerb({
97
+ remote: remote as RemoteFunction<unknown, unknown>,
98
+ schema,
99
+ jsonSchema,
100
+ clients,
101
+ })
102
+ return remote
103
+ }
@@ -0,0 +1,60 @@
1
+ import type { HttpVerb } from './types/HttpVerb.ts'
2
+
3
+ /*
4
+ Parses + merges every source of args available for a verb-defined handler:
5
+ - body (json or form-encoded, ignored for GET/DELETE/HEAD)
6
+ - url query string
7
+
8
+ When both are present and the body is a plain object, the merge folds the
9
+ query in on top so query keys win on collision. A non-object body (array,
10
+ primitive, null) skips the merge entirely and is returned as-is — there's
11
+ no key on the body to layer the query into, and the framework's args type
12
+ is a single bag rather than a `{body, query}` envelope. Returns undefined
13
+ when no source contributes any key.
14
+ */
15
+ export async function parseArgs(method: HttpVerb, request: Request): Promise<unknown> {
16
+ /*
17
+ Skip the URL parse entirely when the raw request URL has no query —
18
+ typical POST/PUT/PATCH calls land here with a flat rpc URL and no
19
+ `?…`, so the `new URL(...)` constructor cost (which dwarfs the
20
+ indexOf check) is wasted work.
21
+ */
22
+ const queryStart = request.url.indexOf('?')
23
+ const hasQuery = queryStart !== -1
24
+ const url = hasQuery ? new URL(request.url) : undefined
25
+
26
+ let body: unknown
27
+ if (method !== 'GET' && method !== 'DELETE' && method !== 'HEAD') {
28
+ const contentType = (request.headers.get('content-type') ?? '').toLowerCase()
29
+ if (contentType.includes('application/json')) {
30
+ const text = await request.text()
31
+ if (text !== '') {
32
+ body = JSON.parse(text)
33
+ }
34
+ } else if (
35
+ contentType.includes('application/x-www-form-urlencoded') ||
36
+ contentType.includes('multipart/form-data')
37
+ ) {
38
+ const form = await request.formData()
39
+ body = Object.fromEntries(form)
40
+ }
41
+ }
42
+
43
+ if (body !== undefined && (typeof body !== 'object' || body === null || Array.isArray(body))) {
44
+ return body
45
+ }
46
+
47
+ if (!url) {
48
+ if (body === undefined) {
49
+ return undefined
50
+ }
51
+ return body
52
+ }
53
+
54
+ const bodyObject = (body ?? {}) as Record<string, unknown>
55
+ const merged = { ...bodyObject, ...Object.fromEntries(url.searchParams) }
56
+ if (Object.keys(merged).length === 0) {
57
+ return undefined
58
+ }
59
+ return merged
60
+ }
@@ -0,0 +1,6 @@
1
+ import type { VerbRegistryEntry } from './types/VerbRegistryEntry.ts'
2
+ import { verbRegistry } from './verbRegistry.ts'
3
+
4
+ export function registerVerb(entry: VerbRegistryEntry): void {
5
+ verbRegistry.set(entry.remote.url, entry)
6
+ }
@@ -0,0 +1 @@
1
+ export type HttpVerb = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'
@@ -0,0 +1,13 @@
1
+ import type { HttpVerb } from './HttpVerb.ts'
2
+
3
+ /*
4
+ Bare-response remote function — same call shape as RemoteFunction but
5
+ resolves to the underlying Response without Content-Type decode and
6
+ without throwing on non-2xx. Produced as `.raw` on every RemoteFunction
7
+ so callers that need status / headers / body streaming or want to
8
+ implement custom error handling can opt out of the decode.
9
+ */
10
+ export type RawRemoteFunction<Args> = ((args: Args) => Promise<Response>) & {
11
+ readonly method: HttpVerb
12
+ readonly url: string
13
+ }
@@ -0,0 +1,35 @@
1
+ import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
2
+ import type { Subscribable } from '../../../shared/types/Subscribable.ts'
3
+ import type { HttpVerb } from './HttpVerb.ts'
4
+ import type { RawRemoteFunction } from './RawRemoteFunction.ts'
5
+
6
+ /*
7
+ Remote function reference produced by GET/POST/... inside an `$rpc/**`
8
+ module and consumed by rpc dispatch, cache(), SSR auto-hydration, and
9
+ direct calls. Same callable signature on server and client — the bundler
10
+ swaps the implementation for browser builds.
11
+
12
+ The plain call resolves to the decoded body shape (sniffed from
13
+ Content-Type) and throws HttpError on non-2xx. `.raw` is a sibling
14
+ RemoteFunction whose call resolves to the underlying Response — same
15
+ method, same url, same args, no decode. Pass `fn.raw` to cache() to
16
+ memoise raw Responses against the same cache key as `fn` (both share one
17
+ stored entry — the decode just happens on the way out for callers of
18
+ `fn`). `.stream(args)` returns an iterable view of the Response body:
19
+ SSE / JSONL handlers yield each frame; non-streaming handlers yield the
20
+ decoded body once then complete. The result is a Subscribable, so it
21
+ can be passed to subscribe() and shared across reactive consumers.
22
+ For sustained broadcast / pub-sub use the `belte/sockets` primitive —
23
+ HTTP rpc isn't the place for long-lived multi-publisher subscriptions.
24
+ `.fetch(req)` is the framework's request-dispatch entry point — used by
25
+ the router to invoke the handler from an incoming HTTP request, not
26
+ for user code.
27
+ */
28
+ export type RemoteFunction<Args, Return> = ((args: Args) => Promise<Return>) & {
29
+ readonly method: HttpVerb
30
+ readonly url: string
31
+ readonly clients: ClientFlags
32
+ readonly raw: RawRemoteFunction<Args>
33
+ stream(args?: Args): Subscribable<Return>
34
+ fetch(request: Request): Promise<Response>
35
+ }
@@ -0,0 +1,22 @@
1
+ import type { TypedResponse } from './TypedResponse.ts'
2
+
3
+ /*
4
+ Handler signature for verb-defined remote functions. Args is `undefined` for
5
+ GETs/DELETEs with no query, JSON-shaped objects for json bodies, and
6
+ form-shaped objects for form-encoded bodies. For binary or multipart bodies
7
+ Args is `undefined` — read the raw Request via `request()` from
8
+ `belte/server` instead.
9
+
10
+ Return is the value type the call site sees after Content-Type-driven
11
+ decoding (a parsed object for JSON, a string for text/*, a Blob otherwise,
12
+ `undefined` for 204). The handler must return a Response at runtime; the
13
+ `TypedResponse<Return>` brand on `json`/`error`/`redirect`/`jsonl`/`sse`
14
+ carries the body shape through the function's inferred return type so the
15
+ verb helper can infer `Return` automatically — no need to annotate
16
+ `GET<Args, Return>` when the handler returns one of the respond helpers.
17
+ A bare `new Response(...)` is still acceptable: the brand is optional, so
18
+ untagged Responses simply fall back to `Return = unknown`.
19
+ */
20
+ export type RemoteHandler<Args, Return> = (
21
+ args: Args,
22
+ ) => TypedResponse<Return> | Promise<TypedResponse<Return>>
@@ -0,0 +1,13 @@
1
+ import type { RemoteFunction } from './RemoteFunction.ts'
2
+
3
+ /*
4
+ Manifest of RPC URL → module loader. Produced by the resolver plugin from
5
+ every `.ts` under src/server/rpc — each file maps to one URL (derived from its
6
+ path under `$rpc`, prefixed with `/rpc/`). Each module has exactly one
7
+ named export, a RemoteFunction whose `.method` and `.url` were stamped in
8
+ by the bundler rewrite.
9
+ */
10
+ export type RemoteRoutes = Record<
11
+ string,
12
+ () => Promise<Record<string, RemoteFunction<unknown, unknown>>>
13
+ >
@@ -0,0 +1,57 @@
1
+ /*
2
+ Mirror of the Standard Schema v1 spec interface (standardschema.dev). Any
3
+ library that implements the spec — zod, valibot, arktype, etc. — produces
4
+ values whose `~standard` property structurally matches this shape, so users
5
+ can pass their existing schemas to verb helpers without an adapter.
6
+
7
+ Kept inline (no `@standard-schema/spec` dep) because the spec is type-only
8
+ and tiny; adding a package for ~30 lines of interface would be churn. The
9
+ namespace pattern below is the spec's own convention — `InferInput` /
10
+ `InferOutput` ride along the same export.
11
+ */
12
+ export interface StandardSchemaV1<Input = unknown, Output = Input> {
13
+ readonly '~standard': StandardSchemaV1.Props<Input, Output>
14
+ }
15
+
16
+ // biome-ignore lint/style/useNamingConvention: matches the Standard Schema spec exactly
17
+ export namespace StandardSchemaV1 {
18
+ export interface Props<Input = unknown, Output = Input> {
19
+ readonly version: 1
20
+ readonly vendor: string
21
+ readonly validate: (value: unknown) => Result<Output> | Promise<Result<Output>>
22
+ readonly types?: Types<Input, Output> | undefined
23
+ }
24
+
25
+ export type Result<Output> = SuccessResult<Output> | FailureResult
26
+
27
+ export interface SuccessResult<Output> {
28
+ readonly value: Output
29
+ readonly issues?: undefined
30
+ }
31
+
32
+ export interface FailureResult {
33
+ readonly issues: ReadonlyArray<Issue>
34
+ }
35
+
36
+ export interface Issue {
37
+ readonly message: string
38
+ readonly path?: ReadonlyArray<PropertyKey | PathSegment> | undefined
39
+ }
40
+
41
+ export interface PathSegment {
42
+ readonly key: PropertyKey
43
+ }
44
+
45
+ export interface Types<Input = unknown, Output = Input> {
46
+ readonly input: Input
47
+ readonly output: Output
48
+ }
49
+
50
+ export type InferInput<Schema extends StandardSchemaV1> = NonNullable<
51
+ Schema['~standard']['types']
52
+ >['input']
53
+
54
+ export type InferOutput<Schema extends StandardSchemaV1> = NonNullable<
55
+ Schema['~standard']['types']
56
+ >['output']
57
+ }
@@ -0,0 +1,18 @@
1
+ /*
2
+ A `Response` tagged with the body type the framework will hand back to
3
+ callers after Content-Type-driven decoding. The tag is phantom — it
4
+ adds no runtime field, only a type-level slot so the verb helpers can
5
+ infer `Return` from the handler's return type instead of forcing every
6
+ route to annotate it via `GET<Args, Return>`.
7
+
8
+ The respond helpers (`json<T>`, `error`, `redirect`, `jsonl<F>`,
9
+ `sse<F>`) all return a `TypedResponse<T>`, so a handler ending in
10
+ `return json({ user })` exposes `{ user: ... }` as its body type; the
11
+ verb overload picks it up via `RemoteHandler<Args, Return>`.
12
+
13
+ `T` is optional on the brand so a plain `new Response(...)` (untagged)
14
+ remains assignable to `TypedResponse<unknown>`; in that case `Return`
15
+ just falls back to its `unknown` default, matching pre-existing
16
+ behaviour for handlers that build Responses by hand.
17
+ */
18
+ export type TypedResponse<T> = Response & { readonly __body?: T }
@@ -0,0 +1,39 @@
1
+ import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
2
+ import type { RemoteFunction } from './RemoteFunction.ts'
3
+ import type { RemoteHandler } from './RemoteHandler.ts'
4
+ import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
5
+
6
+ /*
7
+ Shared signature for every verb helper (GET / POST / …). Three overloads:
8
+
9
+ - `Verb(fn, { schema, clients? })` — `Args` infers from
10
+ `InferInput<Schema>`, the handler receives `InferOutput<Schema>`.
11
+ Generic order is `<Return, Schema>` so users can override `Return`
12
+ while letting `Schema` infer from `opts.schema`. `clients` controls
13
+ which surfaces (browser / mcp / cli) expose this verb.
14
+ - `Verb(fn, { clients })` — schemaless but with explicit client
15
+ targeting (e.g. server-internal RPC with `clients: { browser: false }`).
16
+ - `Verb(fn)` — bare handler. `Args` and `Return` come from the handler
17
+ type; `Return` is usually inferred via the `TypedResponse<T>` brand on
18
+ `json`/`error`/`redirect`/`jsonl`/`sse`.
19
+ */
20
+ export type VerbHelper = {
21
+ <Return = unknown, Schema extends StandardSchemaV1 = StandardSchemaV1>(
22
+ fn: RemoteHandler<StandardSchemaV1.InferOutput<Schema>, Return>,
23
+ opts: {
24
+ schema: Schema
25
+ jsonSchema?: Record<string, unknown>
26
+ clients?: Partial<ClientFlags>
27
+ },
28
+ ): RemoteFunction<StandardSchemaV1.InferInput<Schema>, Return>
29
+ <Args = undefined, Return = unknown>(
30
+ fn: RemoteHandler<Args, Return>,
31
+ opts: {
32
+ jsonSchema?: Record<string, unknown>
33
+ clients: Partial<ClientFlags>
34
+ },
35
+ ): RemoteFunction<Args, Return>
36
+ <Args = undefined, Return = unknown>(
37
+ fn: RemoteHandler<Args, Return>,
38
+ ): RemoteFunction<Args, Return>
39
+ }
@@ -0,0 +1,17 @@
1
+ import type { ClientFlags } from '../../../shared/types/ClientFlags.ts'
2
+ import type { RemoteFunction } from './RemoteFunction.ts'
3
+ import type { StandardSchemaV1 } from './StandardSchemaV1.ts'
4
+
5
+ /*
6
+ Per-verb registry record on the server side. MCP and CLI enumerate this
7
+ to discover which RPCs are advertised (clients flags) and what input
8
+ shape they expect (schema). The schema and resolved clients stay off the
9
+ public RemoteFunction shape so the browser-side proxy doesn't need to
10
+ carry server-only state.
11
+ */
12
+ export type VerbRegistryEntry = {
13
+ remote: RemoteFunction<unknown, unknown>
14
+ schema: StandardSchemaV1 | undefined
15
+ jsonSchema: Record<string, unknown> | undefined
16
+ clients: ClientFlags
17
+ }
@@ -0,0 +1,14 @@
1
+ import type { RemoteFunction } from './types/RemoteFunction.ts'
2
+
3
+ /*
4
+ Verb helpers (GET / POST / …) are placeholders — the bundler rewrites every
5
+ `export const x = GET(fn)` inside `src/server/rpc/<file>.ts` into a call to
6
+ defineVerb (server target) or remoteProxy (client target). If a call slips
7
+ through, the bundler plugin didn't process the file; throwing here surfaces
8
+ that cleanly instead of silently returning undefined.
9
+ */
10
+ export function unprocessed<Args, Return>(verb: string): RemoteFunction<Args, Return> {
11
+ throw new Error(
12
+ `[belte] \`${verb}\` was called outside an $rpc module — verb helpers are only valid as the value of \`export const <filename> = ...\` inside a file under src/server/rpc/`,
13
+ )
14
+ }
@@ -0,0 +1,11 @@
1
+ import type { VerbRegistryEntry } from './types/VerbRegistryEntry.ts'
2
+
3
+ /*
4
+ Process-wide registry of every verb-bound RPC declared in the app.
5
+ defineVerb inserts on first construction (which happens at module-load
6
+ time inside the rpc dispatcher cache or eagerly when MCP / CLI walks the
7
+ rpc manifest). MCP server reads this to build its tools list; the CLI
8
+ binary reads it to generate subcommands. The browser path never touches
9
+ this — the client stub has no schema or clients metadata to register.
10
+ */
11
+ export const verbRegistry = new Map<string, VerbRegistryEntry>()
@@ -0,0 +1,66 @@
1
+ import { commandNameForUrl } from '../../shared/commandNameForUrl.ts'
2
+ import { jsonSchemaForSchema } from '../../shared/jsonSchemaForSchema.ts'
3
+ import type { HttpVerb } from '../rpc/types/HttpVerb.ts'
4
+ import { verbRegistry } from '../rpc/verbRegistry.ts'
5
+
6
+ const BODY_METHODS = new Set<HttpVerb>(['POST', 'PUT', 'PATCH'])
7
+
8
+ /*
9
+ Turns a verb's resolved JSON Schema into OpenAPI query parameters — one
10
+ per top-level property, marked required when the schema lists it. Used
11
+ for GET/DELETE/HEAD operations, which carry their args on the query
12
+ string (mirroring buildRpcRequest).
13
+ */
14
+ function queryParameters(jsonSchema: Record<string, unknown>): Array<Record<string, unknown>> {
15
+ const properties = jsonSchema.properties as Record<string, unknown> | undefined
16
+ if (!properties) {
17
+ return []
18
+ }
19
+ const required = new Set((jsonSchema.required as string[] | undefined) ?? [])
20
+ return Object.entries(properties).map(([name, schema]) => ({
21
+ name,
22
+ in: 'query',
23
+ required: required.has(name),
24
+ schema,
25
+ }))
26
+ }
27
+
28
+ /*
29
+ Builds an OpenAPI 3.1 document from the verb registry — the HTTP surface
30
+ every rpc exposes regardless of which non-browser clients it advertises.
31
+ GET/DELETE/HEAD args become query parameters; POST/PUT/PATCH args become
32
+ a JSON request body. operationId is the folder-prefixed command name so
33
+ it lines up with the MCP tool / CLI subcommand identifiers.
34
+ */
35
+ export function buildOpenApiSpec(info: {
36
+ title: string
37
+ version: string
38
+ }): Record<string, unknown> {
39
+ const paths: Record<string, Record<string, unknown>> = {}
40
+ for (const entry of verbRegistry.values()) {
41
+ const url = entry.remote.url
42
+ const method = entry.remote.method
43
+ const jsonSchema = jsonSchemaForSchema(entry.schema, entry.jsonSchema)
44
+ const operation: Record<string, unknown> = {
45
+ operationId: commandNameForUrl(url),
46
+ responses: { '200': { description: 'OK' } },
47
+ }
48
+ if (BODY_METHODS.has(method)) {
49
+ operation.requestBody = {
50
+ content: { 'application/json': { schema: jsonSchema } },
51
+ }
52
+ } else {
53
+ const parameters = queryParameters(jsonSchema)
54
+ if (parameters.length > 0) {
55
+ operation.parameters = parameters
56
+ }
57
+ }
58
+ const path = (paths[url] ??= {})
59
+ path[method.toLowerCase()] = operation
60
+ }
61
+ return {
62
+ openapi: '3.1.0',
63
+ info: { title: info.title, version: info.version },
64
+ paths,
65
+ }
66
+ }
@@ -0,0 +1,17 @@
1
+ /*
2
+ Bun.build emits `[name]-[hash].[ext]` for chunks; hash is alnum and >=8 chars.
3
+ Source maps inherit the same name (e.g. foo-abc12345.js.map), so the suffix may be `.map`.
4
+ */
5
+ const HASHED = /-[a-z0-9]{8,}\.[a-z0-9]+(\.map)?$/i
6
+
7
+ /*
8
+ Returns the right `Cache-Control` for an asset path served under `/_app/`.
9
+ Hashed chunk filenames are content-addressed and immutable; everything else
10
+ (the entry bundle, the html shell, etc.) must revalidate every time.
11
+ */
12
+ export function cacheControlForAsset(pathname: string): string {
13
+ if (HASHED.test(pathname)) {
14
+ return 'public, max-age=31536000, immutable'
15
+ }
16
+ return 'public, max-age=0, must-revalidate'
17
+ }
@@ -0,0 +1,37 @@
1
+ /*
2
+ Inspects the raw request URL (not the parsed pathname) for path-traversal
3
+ patterns. The WHATWG URL parser decodes `%2E%2E` to `..` and then collapses
4
+ dot-segments out of the pathname during normalization, so by the time
5
+ `url.pathname` is observable any encoded traversal has been masked. The
6
+ remaining literal `..` check guards against any future URL-parser quirk
7
+ that lets a normalised path through.
8
+
9
+ Hot path early-out: if none of the suspect substrings appear in the raw
10
+ URL we never lowercase the whole string nor walk segments.
11
+ */
12
+ export function containsTraversal(rawUrl: string): boolean {
13
+ if (rawUrl.includes('\\')) {
14
+ return true
15
+ }
16
+ if (rawUrl.includes('..') && segmentContainsDotDot(rawUrl)) {
17
+ return true
18
+ }
19
+ if (rawUrl.indexOf('%') === -1) {
20
+ return false
21
+ }
22
+ const lower = rawUrl.toLowerCase()
23
+ return lower.includes('%2e%2e') || lower.includes('%2f') || lower.includes('%5c')
24
+ }
25
+
26
+ function segmentContainsDotDot(rawUrl: string): boolean {
27
+ const queryStart = rawUrl.indexOf('?')
28
+ const pathEnd = queryStart === -1 ? rawUrl.length : queryStart
29
+ const pathStart = rawUrl.indexOf('/', rawUrl.indexOf('://') + 3)
30
+ if (pathStart === -1 || pathStart >= pathEnd) {
31
+ return false
32
+ }
33
+ return rawUrl
34
+ .slice(pathStart, pathEnd)
35
+ .split('/')
36
+ .some((segment) => segment === '..')
37
+ }
@@ -0,0 +1,66 @@
1
+ import { containsTraversal } from './containsTraversal.ts'
2
+ import { mimeForExtension } from './mimeForExtension.ts'
3
+ import type { Assets } from './types/Assets.ts'
4
+
5
+ const PUBLIC_CACHE_CONTROL = 'public, max-age=3600'
6
+
7
+ /*
8
+ Serves files from the project's `public/` folder at the site root. Two
9
+ sources, picked at construction:
10
+
11
+ - `publicAssets` (standalone compile): a map of root path → zstd bytes
12
+ embedded into the binary, mirroring the `_app` asset embed.
13
+ - `publicDir` on disk (dev + `belte start`): files read straight from
14
+ `${cwd}/src/browser/public`.
15
+
16
+ Returns a server fn that resolves to `undefined` when no public file
17
+ matches the request path, so the caller falls through to its own 404 /
18
+ middleware path. The path-traversal guard mirrors serveStaticAsset's
19
+ defence against encoded `..` segments in the raw URL.
20
+ */
21
+ export function createPublicAssetServer({
22
+ publicDir,
23
+ publicAssets,
24
+ }: {
25
+ publicDir: string
26
+ publicAssets?: Assets
27
+ }): (req: Request, url: URL) => Promise<Response | undefined> {
28
+ const headerCache = new Map<string, { base: HeadersInit; zstd: HeadersInit }>()
29
+ function headersFor(pathname: string): { base: HeadersInit; zstd: HeadersInit } {
30
+ const cached = headerCache.get(pathname)
31
+ if (cached) {
32
+ return cached
33
+ }
34
+ const base: HeadersInit = {
35
+ 'Content-Type': mimeForExtension(pathname),
36
+ Vary: 'Accept-Encoding',
37
+ 'Cache-Control': PUBLIC_CACHE_CONTROL,
38
+ }
39
+ const bundle = { base, zstd: { ...base, 'Content-Encoding': 'zstd' } }
40
+ headerCache.set(pathname, bundle)
41
+ return bundle
42
+ }
43
+
44
+ return async function servePublicAsset(req, url) {
45
+ if (containsTraversal(req.url)) {
46
+ return undefined
47
+ }
48
+ const wantsZstd = (req.headers.get('accept-encoding') ?? '').toLowerCase().includes('zstd')
49
+ const { base, zstd } = headersFor(url.pathname)
50
+ if (publicAssets) {
51
+ const compressed = publicAssets[url.pathname]
52
+ if (!compressed) {
53
+ return undefined
54
+ }
55
+ if (wantsZstd) {
56
+ return new Response(compressed, { headers: zstd })
57
+ }
58
+ return new Response(Bun.zstdDecompressSync(compressed), { headers: base })
59
+ }
60
+ const file = Bun.file(publicDir + url.pathname)
61
+ if (!(await file.exists())) {
62
+ return undefined
63
+ }
64
+ return new Response(file, { headers: base })
65
+ }
66
+ }