@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,89 @@
1
+ import type { HttpVerb } from '../server/rpc/types/HttpVerb.ts'
2
+ import type { RawRemoteFunction } from '../server/rpc/types/RawRemoteFunction.ts'
3
+ import type { RemoteFunction } from '../server/rpc/types/RemoteFunction.ts'
4
+ import { decodeResponse } from './decodeResponse.ts'
5
+ import { keyForRemoteCall } from './keyForRemoteCall.ts'
6
+ import { recordRemoteMeta } from './recordRemoteMeta.ts'
7
+ import { subscribableFromResponse } from './subscribableFromResponse.ts'
8
+ import type { ClientFlags } from './types/ClientFlags.ts'
9
+ import type { Subscribable } from './types/Subscribable.ts'
10
+
11
+ /*
12
+ Assembles the public RemoteFunction shape used identically by the
13
+ server-side defineVerb (in-process handler invocation) and the
14
+ client-side remoteProxy (network fetch). Centralising the wiring here
15
+ keeps the call/raw/stream/fetch semantics — including WeakMap meta
16
+ recording, Content-Type decode, and Subscribable derivation — in one
17
+ place so the two halves can't drift.
18
+
19
+ - `buildRequest(args)` synthesizes the Request a meta reader (cache()) or
20
+ the client invoke needs. Server uses the inbound request's URL as the
21
+ base; client uses window.location. The result is memoised inside the
22
+ per-call `getRequest` thunk so the Request is built at most once per
23
+ call regardless of how many readers pull on it.
24
+ - `invoke(args, getRequest)` actually runs the call: server defineVerb
25
+ runs the handler and ignores `getRequest`; client remoteProxy calls
26
+ `fetch(getRequest())`. The thunk lets the server skip the Request
27
+ allocation entirely on the SSR hot path — the only consumer that ever
28
+ forces it is cache(), via the meta thunk recorded below.
29
+ - `parseArgsForFetch` is optional and only set by the server, so the
30
+ framework's router can call `.fetch(inboundRequest)` and have the
31
+ handler receive parsed args. Client `remoteProxy.fetch` just
32
+ forwards the request through invoke().
33
+ */
34
+ export function createRemoteFunction<Args, Return>(opts: {
35
+ method: HttpVerb
36
+ url: string
37
+ clients: ClientFlags
38
+ buildRequest: (args: Args | undefined) => Request
39
+ invoke: (args: Args | undefined, getRequest: () => Request) => Promise<Response>
40
+ parseArgsForFetch?: (request: Request) => Promise<Args | undefined>
41
+ }): RemoteFunction<Args, Return> {
42
+ const { method, url, clients, buildRequest, invoke, parseArgsForFetch } = opts
43
+
44
+ /*
45
+ Dispatch is the one-stop entry for both the plain call (no prebuilt
46
+ Request) and the fetch path (router hands us the inbound Request as
47
+ `prebuilt`). The `getRequest` thunk lazily synthesizes — or
48
+ short-circuits to the prebuilt one — and caches the result so the
49
+ client invoke + the cache meta reader share a single Request.
50
+ */
51
+ function dispatch(args: Args | undefined, prebuilt?: Request): Promise<Response> {
52
+ let cached = prebuilt
53
+ function getRequest(): Request {
54
+ return cached ?? (cached = buildRequest(args))
55
+ }
56
+ const promise = invoke(args, getRequest)
57
+ recordRemoteMeta(promise, getRequest)
58
+ return promise
59
+ }
60
+
61
+ function rawCall(args: Args): Promise<Response> {
62
+ return dispatch(args)
63
+ }
64
+ rawCall.method = method
65
+ rawCall.url = url
66
+ const raw = rawCall as RawRemoteFunction<Args>
67
+
68
+ function callable(args: Args): Promise<Return> {
69
+ return raw(args).then(decodeResponse) as Promise<Return>
70
+ }
71
+ callable.method = method
72
+ callable.url = url
73
+ callable.clients = clients
74
+ callable.raw = raw
75
+ callable.stream = (args?: Args): Subscribable<Return> => {
76
+ return subscribableFromResponse(keyForRemoteCall(method, url, args), () =>
77
+ raw(args as Args),
78
+ )
79
+ }
80
+ callable.fetch = parseArgsForFetch
81
+ ? async (request: Request): Promise<Response> => {
82
+ const args = await parseArgsForFetch(request)
83
+ return dispatch(args, request)
84
+ }
85
+ : (request: Request): Promise<Response> => {
86
+ return dispatch(undefined, request)
87
+ }
88
+ return callable as RemoteFunction<Args, Return>
89
+ }
@@ -0,0 +1,47 @@
1
+ import { HttpError } from '../server/HttpError.ts'
2
+ import { STREAMING_CONTENT_TYPES } from './streamingContentTypes.ts'
3
+
4
+ /*
5
+ Decodes a Response into the natural body value based on Content-Type:
6
+ application/json (or `*\/+json`) → parsed JSON
7
+ text/* → string
8
+ 204 No Content / empty body → undefined
9
+ everything else → Blob
10
+
11
+ Non-2xx responses throw HttpError so the happy path never has to check
12
+ `.ok` — error handling moves into try/catch (or unhandled exception
13
+ propagation), and the success path types as Promise<Return> cleanly.
14
+
15
+ Streaming Content-Types (SSE / JSONL / NDJSON) throw a clear error
16
+ rather than silently doing the wrong thing: response.text() would hang
17
+ forever on a never-ending body and response.json() would fail mid-parse.
18
+ The error points callers at the right tools — `subscribe(fn.stream)(args)`
19
+ for a shared reactive view, or `fn.stream(args)` directly for a fresh
20
+ per-call AsyncIterable — both of which know how to consume the body
21
+ frame-by-frame.
22
+
23
+ Callers that need headers, streaming, or per-status branching should use
24
+ the `.raw(args)` escape hatch on the remote function instead — that
25
+ returns the underlying Response untouched.
26
+ */
27
+ export async function decodeResponse(response: Response): Promise<unknown> {
28
+ if (!response.ok) {
29
+ throw new HttpError(response)
30
+ }
31
+ if (response.status === 204) {
32
+ return undefined
33
+ }
34
+ const contentType = (response.headers.get('content-type') ?? '').toLowerCase()
35
+ if (STREAMING_CONTENT_TYPES.some((type) => contentType.startsWith(type))) {
36
+ throw new Error(
37
+ `[belte] response at ${response.url} is a stream (${contentType}) — use subscribe(fn.stream)(args) for a reactive view, or fn.stream(args) for per-call iteration, instead of awaiting the bare call or cache()`,
38
+ )
39
+ }
40
+ if (contentType.includes('json')) {
41
+ return response.json()
42
+ }
43
+ if (contentType.startsWith('text/')) {
44
+ return response.text()
45
+ }
46
+ return response.blob()
47
+ }
@@ -0,0 +1,27 @@
1
+ import type { CompileTarget } from '../server/runtime/types/CompileTarget.ts'
2
+
3
+ /*
4
+ Picks the Bun compile target matching the current host. Throws if the
5
+ platform/arch pair isn't one of the supported Bun standalone targets — the
6
+ CLI's `--target` flag is the escape hatch for cross-compilation.
7
+ */
8
+ const HOST_TO_TARGET: Record<string, CompileTarget> = {
9
+ 'darwin-arm64': 'bun-darwin-arm64',
10
+ 'darwin-x64': 'bun-darwin-x64',
11
+ 'linux-arm64': 'bun-linux-arm64',
12
+ 'linux-x64': 'bun-linux-x64',
13
+ 'win32-x64': 'bun-windows-x64',
14
+ }
15
+
16
+ export function detectTarget(
17
+ platform: NodeJS.Platform = process.platform,
18
+ arch: NodeJS.Architecture = process.arch,
19
+ ): CompileTarget {
20
+ const target = HOST_TO_TARGET[`${platform}-${arch}`]
21
+ if (!target) {
22
+ throw new Error(
23
+ `[belte] unsupported host platform ${platform}/${arch}. Pass --target=<bun-...> explicitly.`,
24
+ )
25
+ }
26
+ return target
27
+ }
@@ -0,0 +1,479 @@
1
+ /*
2
+ Scans a module source character-by-character — skipping strings,
3
+ templates, comments, and TypeScript generics — for an
4
+ `export const <name> = <IDENT>(...)` binding the caller cares about.
5
+ On a match returns the identifier text, the export name, and the byte
6
+ ranges of the call's open and close parens; the $rpc and $sockets
7
+ rewriters splice their runtime bindings into those ranges.
8
+
9
+ The scanner enforces a single matching export per module: a second match
10
+ throws `singleExportError` so each $rpc / $sockets file is required to
11
+ declare exactly one remote function / socket.
12
+
13
+ A regex pass would be tidier but it can't tell a `GET` mention inside a
14
+ docstring or template literal from the real call, and it can't follow
15
+ nested generics like `GET<Map<K, V>>(`.
16
+ */
17
+
18
+ export type ExportCallSite = {
19
+ ident: string
20
+ exportName: string
21
+ callStart: number
22
+ parenStart: number
23
+ parenEnd: number
24
+ }
25
+
26
+ export function findExportCallSite(
27
+ source: string,
28
+ matchIdent: (ident: string) => boolean,
29
+ singleExportError: string,
30
+ ): ExportCallSite | undefined {
31
+ let found: ExportCallSite | undefined
32
+ const len = source.length
33
+ let i = 0
34
+ while (i < len) {
35
+ const c = source[i]
36
+ const next = source[i + 1]
37
+ if (c === '/' && next === '/') {
38
+ const newline = source.indexOf('\n', i + 2)
39
+ i = newline === -1 ? len : newline + 1
40
+ continue
41
+ }
42
+ if (c === '/' && next === '*') {
43
+ const end = source.indexOf('*/', i + 2)
44
+ i = end === -1 ? len : end + 2
45
+ continue
46
+ }
47
+ if (c === '/' && isRegexContext(source, i)) {
48
+ i = skipRegex(source, i + 1)
49
+ continue
50
+ }
51
+ if (c === '"' || c === "'") {
52
+ i = skipString(source, i + 1, c)
53
+ continue
54
+ }
55
+ if (c === '`') {
56
+ i = skipTemplate(source, i + 1)
57
+ continue
58
+ }
59
+ if (isIdentStart(c) && !isIdentPart(source[i - 1])) {
60
+ let j = i + 1
61
+ while (j < len && isIdentPart(source[j])) {
62
+ j++
63
+ }
64
+ const ident = source.slice(i, j)
65
+ if (matchIdent(ident)) {
66
+ const tail = matchCallTail(source, j)
67
+ if (tail !== undefined) {
68
+ const exportName = detectExportName(source, i)
69
+ if (exportName !== undefined) {
70
+ if (found !== undefined) {
71
+ throw new Error(singleExportError)
72
+ }
73
+ const parenEnd = findCallEnd(source, tail)
74
+ if (parenEnd === undefined) {
75
+ throw new Error(`[belte] unmatched \`(\` after \`${ident}\` identifier`)
76
+ }
77
+ found = {
78
+ ident,
79
+ exportName,
80
+ callStart: i,
81
+ parenStart: tail,
82
+ parenEnd,
83
+ }
84
+ i = parenEnd + 1
85
+ continue
86
+ }
87
+ }
88
+ }
89
+ i = j
90
+ continue
91
+ }
92
+ i++
93
+ }
94
+ return found
95
+ }
96
+
97
+ function skipString(source: string, start: number, quote: string): number {
98
+ let i = start
99
+ while (i < source.length) {
100
+ const c = source[i]
101
+ if (c === '\\') {
102
+ i += 2
103
+ continue
104
+ }
105
+ if (c === quote) {
106
+ return i + 1
107
+ }
108
+ if (c === '\n') {
109
+ return i
110
+ }
111
+ i++
112
+ }
113
+ return source.length
114
+ }
115
+
116
+ function skipTemplate(source: string, start: number): number {
117
+ let i = start
118
+ while (i < source.length) {
119
+ const c = source[i]
120
+ if (c === '\\') {
121
+ i += 2
122
+ continue
123
+ }
124
+ if (c === '`') {
125
+ return i + 1
126
+ }
127
+ if (c === '$' && source[i + 1] === '{') {
128
+ i = skipTemplateExpression(source, i + 2)
129
+ continue
130
+ }
131
+ i++
132
+ }
133
+ return source.length
134
+ }
135
+
136
+ function skipTemplateExpression(source: string, start: number): number {
137
+ let depth = 1
138
+ let i = start
139
+ while (i < source.length && depth > 0) {
140
+ const c = source[i]
141
+ if (c === '{') {
142
+ depth++
143
+ i++
144
+ continue
145
+ }
146
+ if (c === '}') {
147
+ depth--
148
+ i++
149
+ continue
150
+ }
151
+ if (c === '"' || c === "'") {
152
+ i = skipString(source, i + 1, c)
153
+ continue
154
+ }
155
+ if (c === '`') {
156
+ i = skipTemplate(source, i + 1)
157
+ continue
158
+ }
159
+ if (c === '/' && source[i + 1] === '/') {
160
+ const newline = source.indexOf('\n', i + 2)
161
+ i = newline === -1 ? source.length : newline + 1
162
+ continue
163
+ }
164
+ if (c === '/' && source[i + 1] === '*') {
165
+ const end = source.indexOf('*/', i + 2)
166
+ i = end === -1 ? source.length : end + 2
167
+ continue
168
+ }
169
+ if (c === '/' && isRegexContext(source, i)) {
170
+ i = skipRegex(source, i + 1)
171
+ continue
172
+ }
173
+ i++
174
+ }
175
+ return i
176
+ }
177
+
178
+ function matchCallTail(source: string, after: number): number | undefined {
179
+ let j = after
180
+ while (j < source.length && isWhitespace(source[j])) {
181
+ j++
182
+ }
183
+ if (source[j] === '<') {
184
+ const closed = skipGenerics(source, j)
185
+ if (closed === undefined) {
186
+ return undefined
187
+ }
188
+ j = closed
189
+ while (j < source.length && isWhitespace(source[j])) {
190
+ j++
191
+ }
192
+ }
193
+ return source[j] === '(' ? j : undefined
194
+ }
195
+
196
+ /*
197
+ Returns the index immediately after the matching `>` for a generic
198
+ argument list starting at `start`. TypeScript type literals inside the
199
+ generic (`<{ a: string; b: number }>`, function types `<() => X>`,
200
+ tuples `<[A, B]>`, etc.) bring their own paired brackets and
201
+ semicolons, so track depth across `<>`, `()`, `{}`, and `[]` and only
202
+ count a closing `>` when every other bracket is balanced.
203
+ Arrow-function `=>` is treated as a single token so the `>` doesn't
204
+ prematurely close the generic.
205
+ */
206
+ function skipGenerics(source: string, start: number): number | undefined {
207
+ let angleDepth = 0
208
+ let parenDepth = 0
209
+ let braceDepth = 0
210
+ let bracketDepth = 0
211
+ let i = start
212
+ while (i < source.length) {
213
+ const c = source[i]
214
+ if (c === '"' || c === "'") {
215
+ i = skipString(source, i + 1, c)
216
+ continue
217
+ }
218
+ if (c === '`') {
219
+ i = skipTemplate(source, i + 1)
220
+ continue
221
+ }
222
+ if (c === '<') {
223
+ angleDepth++
224
+ } else if (c === '>') {
225
+ const isArrow = source[i - 1] === '='
226
+ if (!isArrow && parenDepth === 0 && braceDepth === 0 && bracketDepth === 0) {
227
+ angleDepth--
228
+ if (angleDepth === 0) {
229
+ return i + 1
230
+ }
231
+ }
232
+ } else if (c === '(') {
233
+ parenDepth++
234
+ } else if (c === ')') {
235
+ parenDepth--
236
+ } else if (c === '{') {
237
+ braceDepth++
238
+ } else if (c === '}') {
239
+ braceDepth--
240
+ } else if (c === '[') {
241
+ bracketDepth++
242
+ } else if (c === ']') {
243
+ bracketDepth--
244
+ }
245
+ i++
246
+ }
247
+ return undefined
248
+ }
249
+
250
+ /*
251
+ Walks the call body, skipping strings/templates/comments and respecting
252
+ nested `()` so brackets inside object literals or nested calls don't
253
+ throw the depth count.
254
+ */
255
+ function findCallEnd(source: string, parenStart: number): number | undefined {
256
+ let depth = 1
257
+ let i = parenStart + 1
258
+ while (i < source.length) {
259
+ const c = source[i]
260
+ if (c === '"' || c === "'") {
261
+ i = skipString(source, i + 1, c)
262
+ continue
263
+ }
264
+ if (c === '`') {
265
+ i = skipTemplate(source, i + 1)
266
+ continue
267
+ }
268
+ if (c === '/' && source[i + 1] === '/') {
269
+ const newline = source.indexOf('\n', i + 2)
270
+ i = newline === -1 ? source.length : newline + 1
271
+ continue
272
+ }
273
+ if (c === '/' && source[i + 1] === '*') {
274
+ const end = source.indexOf('*/', i + 2)
275
+ i = end === -1 ? source.length : end + 2
276
+ continue
277
+ }
278
+ if (c === '/' && isRegexContext(source, i)) {
279
+ i = skipRegex(source, i + 1)
280
+ continue
281
+ }
282
+ if (c === '(') {
283
+ depth++
284
+ } else if (c === ')') {
285
+ depth--
286
+ if (depth === 0) {
287
+ return i
288
+ }
289
+ }
290
+ i++
291
+ }
292
+ return undefined
293
+ }
294
+
295
+ /*
296
+ Looks backwards from a `<IDENT>(` callStart to confirm it was bound by
297
+ `export const <name> = ...`. Returns the identifier in `<name>` if so,
298
+ undefined otherwise — used to skip mentions of an identifier that
299
+ isn't the module's declared export.
300
+ */
301
+ function detectExportName(source: string, callStart: number): string | undefined {
302
+ let i = callStart - 1
303
+ while (i >= 0 && isWhitespace(source[i])) {
304
+ i--
305
+ }
306
+ if (source[i] !== '=') {
307
+ return undefined
308
+ }
309
+ i--
310
+ while (i >= 0 && isWhitespace(source[i])) {
311
+ i--
312
+ }
313
+ const nameEnd = i + 1
314
+ while (i >= 0 && isIdentPart(source[i])) {
315
+ i--
316
+ }
317
+ const nameStart = i + 1
318
+ if (nameStart === nameEnd) {
319
+ return undefined
320
+ }
321
+ const name = source.slice(nameStart, nameEnd)
322
+ while (i >= 0 && isWhitespace(source[i])) {
323
+ i--
324
+ }
325
+ if (!matchesBackwards(source, i, 'const')) {
326
+ return undefined
327
+ }
328
+ i -= 'const'.length
329
+ while (i >= 0 && isWhitespace(source[i])) {
330
+ i--
331
+ }
332
+ if (!matchesBackwards(source, i, 'export')) {
333
+ return undefined
334
+ }
335
+ return name
336
+ }
337
+
338
+ function matchesBackwards(source: string, end: number, keyword: string): boolean {
339
+ const start = end - keyword.length + 1
340
+ if (start < 0) {
341
+ return false
342
+ }
343
+ if (source.slice(start, end + 1) !== keyword) {
344
+ return false
345
+ }
346
+ return start === 0 || !isIdentPart(source[start - 1])
347
+ }
348
+
349
+ function isIdentStart(c: string | undefined): boolean {
350
+ if (c === undefined) {
351
+ return false
352
+ }
353
+ return (c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z') || c === '_' || c === '$'
354
+ }
355
+
356
+ function isIdentPart(c: string | undefined): boolean {
357
+ if (c === undefined) {
358
+ return false
359
+ }
360
+ return isIdentStart(c) || (c >= '0' && c <= '9')
361
+ }
362
+
363
+ function isWhitespace(c: string | undefined): boolean {
364
+ return c === ' ' || c === '\t' || c === '\n' || c === '\r'
365
+ }
366
+
367
+ /*
368
+ A `/` starts a regex literal when the prior expression context expects an
369
+ expression rather than a value — after an open delimiter, operator, or
370
+ expression-prefix keyword (return, typeof, instanceof, in, of, delete,
371
+ void, await, yield, new, throw, case, do). Otherwise `/` is division.
372
+ Without this disambiguation a regex like `/^\//` reads as `/` (division),
373
+ then `^`, `\`, `/`, `/` — and the final `//` pair fakes a line comment
374
+ that swallows the rest of the line, eating any `)` that closes the
375
+ enclosing call.
376
+ */
377
+ const REGEX_PREFIX_KEYWORDS = new Set([
378
+ 'return',
379
+ 'typeof',
380
+ 'instanceof',
381
+ 'in',
382
+ 'of',
383
+ 'delete',
384
+ 'void',
385
+ 'await',
386
+ 'yield',
387
+ 'new',
388
+ 'throw',
389
+ 'case',
390
+ 'do',
391
+ ])
392
+
393
+ const REGEX_PUNCTUATION = new Set([
394
+ '(',
395
+ '[',
396
+ '{',
397
+ ',',
398
+ ';',
399
+ ':',
400
+ '?',
401
+ '!',
402
+ '&',
403
+ '|',
404
+ '^',
405
+ '~',
406
+ '+',
407
+ '-',
408
+ '*',
409
+ '%',
410
+ '<',
411
+ '>',
412
+ '=',
413
+ '/',
414
+ ])
415
+
416
+ function isRegexContext(source: string, slashIndex: number): boolean {
417
+ let i = slashIndex - 1
418
+ while (i >= 0 && isWhitespace(source[i])) {
419
+ i--
420
+ }
421
+ if (i < 0) {
422
+ return true
423
+ }
424
+ const prev = source[i] as string
425
+ if (REGEX_PUNCTUATION.has(prev)) {
426
+ return true
427
+ }
428
+ if (isIdentPart(prev)) {
429
+ let start = i
430
+ while (start > 0 && isIdentPart(source[start - 1])) {
431
+ start--
432
+ }
433
+ return REGEX_PREFIX_KEYWORDS.has(source.slice(start, i + 1))
434
+ }
435
+ return false
436
+ }
437
+
438
+ /*
439
+ Walks past a regex literal body, respecting character classes (`[...]`
440
+ where `/` is literal) and backslash escapes, then consumes trailing
441
+ flag identifiers. Returns the index immediately after the regex. An
442
+ unterminated regex (newline before closing `/`) returns the newline
443
+ position so the outer scanner can resume normally on the next line.
444
+ */
445
+ function skipRegex(source: string, start: number): number {
446
+ let i = start
447
+ let inClass = false
448
+ while (i < source.length) {
449
+ const c = source[i]
450
+ if (c === '\\') {
451
+ i += 2
452
+ continue
453
+ }
454
+ if (c === '\n') {
455
+ return i
456
+ }
457
+ if (inClass) {
458
+ if (c === ']') {
459
+ inClass = false
460
+ }
461
+ i++
462
+ continue
463
+ }
464
+ if (c === '[') {
465
+ inClass = true
466
+ i++
467
+ continue
468
+ }
469
+ if (c === '/') {
470
+ let j = i + 1
471
+ while (j < source.length && isIdentPart(source[j])) {
472
+ j++
473
+ }
474
+ return j
475
+ }
476
+ i++
477
+ }
478
+ return source.length
479
+ }
@@ -0,0 +1,28 @@
1
+ /*
2
+ Headers belte forwards from an inbound HTTP/MCP request onto every
3
+ synthesized in-process rpc Request — cookies + bearer auth + the four
4
+ forwarding hints proxies set when terminating TLS in front of the app.
5
+ defineVerb uses this when an SSR pass calls a verb in-process; the MCP
6
+ dispatcher uses it when piping a tool invocation through verb.fetch.
7
+
8
+ Centralised so both call sites can't drift on which headers are
9
+ considered "auth/identity" context.
10
+ */
11
+ export const FORWARDED_HEADERS = [
12
+ 'cookie',
13
+ 'authorization',
14
+ 'x-forwarded-for',
15
+ 'x-forwarded-proto',
16
+ 'x-forwarded-host',
17
+ ]
18
+
19
+ export function forwardHeaders(source: Headers): Headers {
20
+ const headers = new Headers()
21
+ for (const name of FORWARDED_HEADERS) {
22
+ const value = source.get(name)
23
+ if (value) {
24
+ headers.set(name, value)
25
+ }
26
+ }
27
+ return headers
28
+ }
@@ -0,0 +1,5 @@
1
+ import { remoteMetaStore } from './remoteMetaStore.ts'
2
+
3
+ export function getRemoteMeta(promise: Promise<unknown>): Request | undefined {
4
+ return remoteMetaStore.get(promise)?.()
5
+ }
@@ -0,0 +1,23 @@
1
+ /*
2
+ Matches the conventions of the `debug` npm package.
3
+ DEBUG="belte" → enables "belte"
4
+ DEBUG="belte:*" → enables "belte" and "belte:anything"
5
+ DEBUG="*" → enables everything
6
+ DEBUG="a,belte" → comma-separated list
7
+ */
8
+ export function isDebugEnabled(name: string, env: string | undefined = process.env.DEBUG): boolean {
9
+ if (!env) {
10
+ return false
11
+ }
12
+ return env.split(',').some((raw) => {
13
+ const pattern = raw.trim()
14
+ if (pattern === '*') {
15
+ return true
16
+ }
17
+ if (pattern.endsWith(':*')) {
18
+ const prefix = pattern.slice(0, -2)
19
+ return name === prefix || name.startsWith(`${prefix}:`)
20
+ }
21
+ return pattern === name
22
+ })
23
+ }