@belte/belte 0.19.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/CHANGELOG.md +313 -0
- package/LICENSE +21 -0
- package/README.md +559 -0
- package/bin/belte.ts +183 -0
- package/package.json +110 -0
- package/src/App.svelte +31 -0
- package/src/appEntry.ts +151 -0
- package/src/assets/app.html +14 -0
- package/src/belteResolverPlugin.ts +858 -0
- package/src/build.ts +147 -0
- package/src/buildCli.ts +129 -0
- package/src/buildDisconnected.ts +122 -0
- package/src/bundleApp.ts +149 -0
- package/src/bundleDisconnectedEntry.ts +17 -0
- package/src/cliEntry.ts +25 -0
- package/src/clientBuildPlugins.ts +41 -0
- package/src/clientEntry.ts +7 -0
- package/src/compile.ts +64 -0
- package/src/controlServerWorker.ts +422 -0
- package/src/dedupeSveltePlugin.ts +66 -0
- package/src/devEntry.ts +169 -0
- package/src/discoveryEntry.ts +81 -0
- package/src/lib/browser/applyStreamedResolution.ts +33 -0
- package/src/lib/browser/cacheEntryFromSnapshot.ts +48 -0
- package/src/lib/browser/flushUnresolvedPlaceholders.ts +16 -0
- package/src/lib/browser/installStreamingPlaceholders.ts +32 -0
- package/src/lib/browser/openResolveStream.ts +42 -0
- package/src/lib/browser/page.svelte.ts +258 -0
- package/src/lib/browser/pageStreamController.ts +17 -0
- package/src/lib/browser/refetchPlaceholder.ts +12 -0
- package/src/lib/browser/remoteProxy.ts +37 -0
- package/src/lib/browser/socketChannel.ts +192 -0
- package/src/lib/browser/socketProxy.ts +57 -0
- package/src/lib/browser/startClient.ts +153 -0
- package/src/lib/browser/subscribe.ts +131 -0
- package/src/lib/browser/types/Errors.ts +9 -0
- package/src/lib/browser/types/Layouts.ts +7 -0
- package/src/lib/browser/types/Pages.ts +7 -0
- package/src/lib/browser/types/StreamingDeferred.ts +9 -0
- package/src/lib/bundle/BundleMenu.ts +11 -0
- package/src/lib/bundle/BundleMenuItem.ts +24 -0
- package/src/lib/bundle/BundleWindow.ts +36 -0
- package/src/lib/bundle/WEBVIEW_BUILD_REVISION.ts +9 -0
- package/src/lib/bundle/WEBVIEW_VERSION.ts +7 -0
- package/src/lib/bundle/bindConnectedFlag.ts +29 -0
- package/src/lib/bundle/bindRequestNavigate.ts +31 -0
- package/src/lib/bundle/buildWebviewLib.ts +111 -0
- package/src/lib/bundle/disconnected.css +9 -0
- package/src/lib/bundle/disconnected.svelte +386 -0
- package/src/lib/bundle/ensureWebviewLib.ts +20 -0
- package/src/lib/bundle/exitWithParent.ts +28 -0
- package/src/lib/bundle/infoPlist.ts +46 -0
- package/src/lib/bundle/installDownloads.ts +24 -0
- package/src/lib/bundle/installMacMenu.ts +39 -0
- package/src/lib/bundle/listenLocalControlServer.ts +19 -0
- package/src/lib/bundle/native/belteMenu.mm +422 -0
- package/src/lib/bundle/native/webview.h +4557 -0
- package/src/lib/bundle/onMenu.ts +41 -0
- package/src/lib/bundle/openWebview.ts +104 -0
- package/src/lib/bundle/pngToIcns.ts +47 -0
- package/src/lib/bundle/probeBelteServer.ts +34 -0
- package/src/lib/bundle/resolveServerBinary.ts +12 -0
- package/src/lib/bundle/resolveWebviewLib.ts +53 -0
- package/src/lib/bundle/serverBinaryFilename.ts +8 -0
- package/src/lib/bundle/signMacApp.ts +35 -0
- package/src/lib/bundle/spawnEmbeddedServer.ts +65 -0
- package/src/lib/bundle/stableLocalPort.ts +19 -0
- package/src/lib/bundle/waitForServer.ts +23 -0
- package/src/lib/bundle/webviewCachePath.ts +23 -0
- package/src/lib/bundle/webviewLibName.ts +11 -0
- package/src/lib/cli/connectToServer.ts +23 -0
- package/src/lib/cli/createClient.ts +170 -0
- package/src/lib/cli/dispatchCommand.ts +71 -0
- package/src/lib/cli/loadEnvFromBinaryDir.ts +16 -0
- package/src/lib/cli/parseArgvForRpc.ts +97 -0
- package/src/lib/cli/printHelp.ts +119 -0
- package/src/lib/cli/printSessionHelp.ts +27 -0
- package/src/lib/cli/printSessionStatus.ts +21 -0
- package/src/lib/cli/printTrimmed.ts +8 -0
- package/src/lib/cli/printValue.ts +10 -0
- package/src/lib/cli/resolveCliTarget.ts +48 -0
- package/src/lib/cli/runCli.ts +139 -0
- package/src/lib/cli/runSession.ts +105 -0
- package/src/lib/cli/startLocalInstance.ts +14 -0
- package/src/lib/cli/tokenizeLine.ts +51 -0
- package/src/lib/cli/types/CliManifest.ts +9 -0
- package/src/lib/cli/types/CliManifestEntry.ts +17 -0
- package/src/lib/cli/types/CliTarget.ts +13 -0
- package/src/lib/mcp/annotationsForMethod.ts +29 -0
- package/src/lib/mcp/createMcpResourceServer.ts +101 -0
- package/src/lib/mcp/createMcpServer.ts +42 -0
- package/src/lib/mcp/dispatchMcpRequest.ts +146 -0
- package/src/lib/mcp/mcpResourceServerSlot.ts +18 -0
- package/src/lib/mcp/mcpSurface.ts +265 -0
- package/src/lib/mcp/toolResultFromResponse.ts +66 -0
- package/src/lib/mcp/types/JsonRpcRequest.ts +12 -0
- package/src/lib/mcp/types/JsonRpcResponse.ts +20 -0
- package/src/lib/mcp/types/McpResourceContents.ts +10 -0
- package/src/lib/mcp/types/McpResourceDescriptor.ts +6 -0
- package/src/lib/mcp/types/McpResourceServer.ts +12 -0
- package/src/lib/mcp/types/McpServer.ts +9 -0
- package/src/lib/mcp/types/McpServerOptions.ts +16 -0
- package/src/lib/server/AppModule.ts +33 -0
- package/src/lib/server/DELETE.ts +9 -0
- package/src/lib/server/GET.ts +9 -0
- package/src/lib/server/HEAD.ts +9 -0
- package/src/lib/server/PATCH.ts +9 -0
- package/src/lib/server/POST.ts +9 -0
- package/src/lib/server/PUT.ts +9 -0
- package/src/lib/server/agent.ts +76 -0
- package/src/lib/server/appDataDir.ts +15 -0
- package/src/lib/server/cli/buildEnvContent.ts +19 -0
- package/src/lib/server/cli/createTarGz.ts +76 -0
- package/src/lib/server/cli/handleCliDownload.ts +153 -0
- package/src/lib/server/cli/handleCliInstall.ts +37 -0
- package/src/lib/server/cli/installScript.ts +29 -0
- package/src/lib/server/cli/maxSourceMtime.ts +26 -0
- package/src/lib/server/cookies.ts +29 -0
- package/src/lib/server/env.ts +50 -0
- package/src/lib/server/error.ts +70 -0
- package/src/lib/server/json.ts +28 -0
- package/src/lib/server/jsonl.ts +46 -0
- package/src/lib/server/prompts/definePrompt.ts +20 -0
- package/src/lib/server/prompts/promptRegistry.ts +9 -0
- package/src/lib/server/prompts/registerPrompt.ts +6 -0
- package/src/lib/server/prompts/renderPromptTemplate.ts +16 -0
- package/src/lib/server/prompts/types/Prompt.ts +13 -0
- package/src/lib/server/prompts/types/PromptOptions.ts +12 -0
- package/src/lib/server/prompts/types/PromptRegistryEntry.ts +13 -0
- package/src/lib/server/prompts/types/PromptRoutes.ts +10 -0
- package/src/lib/server/redirect.ts +42 -0
- package/src/lib/server/request.ts +18 -0
- package/src/lib/server/rpc/defineVerb.ts +133 -0
- package/src/lib/server/rpc/dispatchVerbInProcess.ts +46 -0
- package/src/lib/server/rpc/findVerbByCommandName.ts +18 -0
- package/src/lib/server/rpc/parseArgs.ts +95 -0
- package/src/lib/server/rpc/registerVerb.ts +6 -0
- package/src/lib/server/rpc/types/RemoteHandler.ts +27 -0
- package/src/lib/server/rpc/types/RemoteRoutes.ts +13 -0
- package/src/lib/server/rpc/types/TypedResponse.ts +18 -0
- package/src/lib/server/rpc/types/VerbHelper.ts +68 -0
- package/src/lib/server/rpc/types/VerbRegistryEntry.ts +29 -0
- package/src/lib/server/rpc/unprocessed.ts +14 -0
- package/src/lib/server/rpc/verbRegistry.ts +11 -0
- package/src/lib/server/runtime/DEFAULT_PORT.ts +6 -0
- package/src/lib/server/runtime/DEV_REBUILD_MESSAGE.ts +4 -0
- package/src/lib/server/runtime/DEV_RELOAD_CLIENT_SCRIPT.ts +29 -0
- package/src/lib/server/runtime/acceptsZstd.ts +8 -0
- package/src/lib/server/runtime/buildOpenApiSpec.ts +106 -0
- package/src/lib/server/runtime/cacheControlForAsset.ts +22 -0
- package/src/lib/server/runtime/containsTraversal.ts +37 -0
- package/src/lib/server/runtime/createAssetHeaderCache.ts +35 -0
- package/src/lib/server/runtime/createPublicAssetServer.ts +63 -0
- package/src/lib/server/runtime/createRouteDispatcher.ts +100 -0
- package/src/lib/server/runtime/createServer.ts +692 -0
- package/src/lib/server/runtime/devReloadResponse.ts +35 -0
- package/src/lib/server/runtime/disableIdleTimeoutForStream.ts +27 -0
- package/src/lib/server/runtime/envSchemaStore.ts +15 -0
- package/src/lib/server/runtime/findOpenPort.ts +35 -0
- package/src/lib/server/runtime/getActiveServer.ts +6 -0
- package/src/lib/server/runtime/globToPathSet.ts +29 -0
- package/src/lib/server/runtime/inProcessServer.ts +20 -0
- package/src/lib/server/runtime/internalErrorResponse.ts +25 -0
- package/src/lib/server/runtime/isCrossOriginUpgrade.ts +19 -0
- package/src/lib/server/runtime/listenOnOpenPort.ts +36 -0
- package/src/lib/server/runtime/logExposedSurfaces.ts +162 -0
- package/src/lib/server/runtime/mimeForExtension.ts +20 -0
- package/src/lib/server/runtime/parseIdleTimeout.ts +10 -0
- package/src/lib/server/runtime/parsePort.ts +11 -0
- package/src/lib/server/runtime/registryManifests.ts +66 -0
- package/src/lib/server/runtime/requestContext.ts +5 -0
- package/src/lib/server/runtime/resolveStreamResponse.ts +29 -0
- package/src/lib/server/runtime/runWithRequestScope.ts +57 -0
- package/src/lib/server/runtime/safeJsonForScript.ts +17 -0
- package/src/lib/server/runtime/serializeCacheSnapshot.ts +45 -0
- package/src/lib/server/runtime/serverSlot.ts +13 -0
- package/src/lib/server/runtime/setActiveServer.ts +6 -0
- package/src/lib/server/runtime/snapshotEntryFromCache.ts +81 -0
- package/src/lib/server/runtime/streamCacheResolutions.ts +37 -0
- package/src/lib/server/runtime/streamFromIterator.ts +86 -0
- package/src/lib/server/runtime/streamStash.ts +64 -0
- package/src/lib/server/runtime/types/Assets.ts +1 -0
- package/src/lib/server/runtime/types/RequestStore.ts +27 -0
- package/src/lib/server/runtime/withResponseDefaults.ts +24 -0
- package/src/lib/server/server.ts +32 -0
- package/src/lib/server/socket.ts +31 -0
- package/src/lib/server/sockets/createSocketDispatcher.ts +311 -0
- package/src/lib/server/sockets/defineSocket.ts +167 -0
- package/src/lib/server/sockets/lookupSocket.ts +6 -0
- package/src/lib/server/sockets/recentHistory.ts +11 -0
- package/src/lib/server/sockets/registerSocket.ts +6 -0
- package/src/lib/server/sockets/socketOperations.ts +35 -0
- package/src/lib/server/sockets/socketRegistry.ts +9 -0
- package/src/lib/server/sockets/types/Socket.ts +21 -0
- package/src/lib/server/sockets/types/SocketClientFrame.ts +18 -0
- package/src/lib/server/sockets/types/SocketOperation.ts +22 -0
- package/src/lib/server/sockets/types/SocketOptions.ts +22 -0
- package/src/lib/server/sockets/types/SocketRegistryEntry.ts +17 -0
- package/src/lib/server/sockets/types/SocketRoutes.ts +10 -0
- package/src/lib/server/sockets/types/SocketServerFrame.ts +15 -0
- package/src/lib/server/sse.ts +53 -0
- package/src/lib/shared/BELTE_PACKAGE_NAME.ts +7 -0
- package/src/lib/shared/CACHE_CONTROL_VALUES.ts +16 -0
- package/src/lib/shared/HttpError.ts +19 -0
- package/src/lib/shared/RESOLVE_STREAM_PATH.ts +7 -0
- package/src/lib/shared/STREAMING_CONTENT_TYPES.ts +11 -0
- package/src/lib/shared/activeCacheStore.ts +20 -0
- package/src/lib/shared/appDataDir.ts +34 -0
- package/src/lib/shared/belteImportName.ts +44 -0
- package/src/lib/shared/browserClientFlags.ts +10 -0
- package/src/lib/shared/buildRpcRequest.ts +70 -0
- package/src/lib/shared/bundleLayout.ts +36 -0
- package/src/lib/shared/bundled.ts +34 -0
- package/src/lib/shared/cache.ts +559 -0
- package/src/lib/shared/cacheStoreSlot.ts +16 -0
- package/src/lib/shared/canonicalJson.ts +63 -0
- package/src/lib/shared/carriesBodyArgs.ts +13 -0
- package/src/lib/shared/clearLastConnection.ts +7 -0
- package/src/lib/shared/commandNameForUrl.ts +17 -0
- package/src/lib/shared/createCacheStore.ts +75 -0
- package/src/lib/shared/createPushIterator.ts +93 -0
- package/src/lib/shared/createRemoteFunction.ts +99 -0
- package/src/lib/shared/decodeResponse.ts +47 -0
- package/src/lib/shared/detectTarget.ts +27 -0
- package/src/lib/shared/exeSuffix.ts +9 -0
- package/src/lib/shared/exitOnBuildFailure.ts +17 -0
- package/src/lib/shared/extraForwardHeaders.ts +16 -0
- package/src/lib/shared/fileStem.ts +9 -0
- package/src/lib/shared/findExportCallSite.ts +479 -0
- package/src/lib/shared/forwardHeaders.ts +41 -0
- package/src/lib/shared/getRemoteMeta.ts +5 -0
- package/src/lib/shared/globalCacheStore.ts +15 -0
- package/src/lib/shared/globalCacheStoreSlot.ts +14 -0
- package/src/lib/shared/importNamesToStrip.ts +13 -0
- package/src/lib/shared/invalidateEvent.ts +11 -0
- package/src/lib/shared/isCompileTarget.ts +15 -0
- package/src/lib/shared/isDebugEnabled.ts +23 -0
- package/src/lib/shared/isModuleNotFound.ts +16 -0
- package/src/lib/shared/isReadOnlyMethod.ts +14 -0
- package/src/lib/shared/isStreamingResponse.ts +11 -0
- package/src/lib/shared/jsonSchemaForPromptArguments.ts +29 -0
- package/src/lib/shared/jsonSchemaForSchema.ts +32 -0
- package/src/lib/shared/jsonlErrorFrame.ts +24 -0
- package/src/lib/shared/keyForRemoteCall.ts +29 -0
- package/src/lib/shared/lastConnectionPath.ts +7 -0
- package/src/lib/shared/loadEnvFile.ts +17 -0
- package/src/lib/shared/loadEnvFromDataDir.ts +15 -0
- package/src/lib/shared/loadSvelteConfig.ts +18 -0
- package/src/lib/shared/log.ts +104 -0
- package/src/lib/shared/manifestModule.ts +39 -0
- package/src/lib/shared/memoizeByKey.ts +24 -0
- package/src/lib/shared/nearestLayoutPrefix.ts +36 -0
- package/src/lib/shared/normalizeTarget.ts +10 -0
- package/src/lib/shared/pageUrlForFile.ts +14 -0
- package/src/lib/shared/parseBoundedEnvInt.ts +20 -0
- package/src/lib/shared/parseEnv.ts +30 -0
- package/src/lib/shared/parsePromptMarkdown.ts +34 -0
- package/src/lib/shared/parseRouteSegments.ts +22 -0
- package/src/lib/shared/prepareRpcModule.ts +59 -0
- package/src/lib/shared/prepareSocketModule.ts +49 -0
- package/src/lib/shared/programNameForPackage.ts +14 -0
- package/src/lib/shared/promptNameForFile.ts +10 -0
- package/src/lib/shared/queryStringFromArgs.ts +27 -0
- package/src/lib/shared/readEnvFile.ts +15 -0
- package/src/lib/shared/readLastConnection.ts +18 -0
- package/src/lib/shared/readPackageJson.ts +9 -0
- package/src/lib/shared/recordRemoteMeta.ts +5 -0
- package/src/lib/shared/remoteMetaStore.ts +16 -0
- package/src/lib/shared/resolveClientFlags.ts +20 -0
- package/src/lib/shared/responseErrorText.ts +9 -0
- package/src/lib/shared/rpcUrlForFile.ts +19 -0
- package/src/lib/shared/runningAsStandaloneBinary.ts +13 -0
- package/src/lib/shared/serializeEnv.ts +18 -0
- package/src/lib/shared/setCacheStoreResolver.ts +6 -0
- package/src/lib/shared/setGlobalCacheStoreResolver.ts +6 -0
- package/src/lib/shared/socketNameForFile.ts +11 -0
- package/src/lib/shared/sseErrorFrame.ts +29 -0
- package/src/lib/shared/streamResponse.ts +169 -0
- package/src/lib/shared/stripImport.ts +27 -0
- package/src/lib/shared/subscribableFromResponse.ts +51 -0
- package/src/lib/shared/toBunRoutePattern.ts +28 -0
- package/src/lib/shared/types/CacheEntry.ts +63 -0
- package/src/lib/shared/types/CacheInvalidation.ts +9 -0
- package/src/lib/shared/types/CacheOptions.ts +33 -0
- package/src/lib/shared/types/CacheSnapshot.ts +16 -0
- package/src/lib/shared/types/CacheSnapshotEntry.ts +15 -0
- package/src/lib/shared/types/CacheStore.ts +32 -0
- package/src/lib/shared/types/ClientFlags.ts +11 -0
- package/src/lib/shared/types/CompileTarget.ts +6 -0
- package/src/lib/shared/types/HttpVerb.ts +1 -0
- package/src/lib/shared/types/LastConnection.ts +9 -0
- package/src/lib/shared/types/PromptArgument.ts +12 -0
- package/src/lib/shared/types/RawRemoteFunction.ts +13 -0
- package/src/lib/shared/types/RemoteFunction.ts +42 -0
- package/src/lib/shared/types/StandardSchemaV1.ts +57 -0
- package/src/lib/shared/types/StreamedResolution.ts +10 -0
- package/src/lib/shared/types/StreamingPlaceholder.ts +13 -0
- package/src/lib/shared/types/Subscribable.ts +15 -0
- package/src/lib/shared/types/SvelteConfig.ts +5 -0
- package/src/lib/shared/withJsonSchema.ts +20 -0
- package/src/lib/shared/writeLastConnection.ts +13 -0
- package/src/lib/shared/writeRoutesDts.ts +67 -0
- package/src/lib/test/clearVerbRegistry.ts +11 -0
- package/src/lib/test/createTestClient.ts +78 -0
- package/src/preload.ts +20 -0
- package/src/scaffold.ts +92 -0
- package/src/serverBuildPlugins.ts +25 -0
- package/src/serverEntry.ts +94 -0
- package/src/sveltePlugin.ts +58 -0
- package/src/tailwindStylePreprocessor.ts +62 -0
- package/template/bunfig.toml +4 -0
- package/template/package.json +19 -0
- package/template/src/app.ts +23 -0
- package/template/src/browser/app.css +21 -0
- package/template/src/browser/app.html +24 -0
- package/template/src/browser/pages/about/page.svelte +5 -0
- package/template/src/browser/pages/layout.svelte +26 -0
- package/template/src/browser/pages/page.svelte +20 -0
- package/template/src/bundle/icon.png +0 -0
- package/template/src/cli/banner.txt +3 -0
- package/template/src/cli/footer.txt +1 -0
- package/template/src/server/config.ts +17 -0
- package/template/src/server/rpc/getHello.ts +35 -0
- package/template/svelte.config.js +12 -0
- package/template/tsconfig.json +18 -0
- package/tsconfig.app.json +16 -0
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Minimal ustar tarball writer. Each entry is a 512-byte header followed
|
|
3
|
+
by the content padded to a 512-byte boundary; the archive ends with two
|
|
4
|
+
512-byte zero blocks. After assembly the buffer is gzipped via
|
|
5
|
+
Bun.gzipSync — no external `tar` invocation, no extra deps.
|
|
6
|
+
|
|
7
|
+
Format constraints:
|
|
8
|
+
- File names ≤ 100 bytes (we never write longer paths).
|
|
9
|
+
- Numeric fields are zero-padded octal ASCII strings (POSIX rule).
|
|
10
|
+
- Checksum is the sum of all header bytes treating the checksum
|
|
11
|
+
field as spaces; encoded as 6 octal digits + NUL + space.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
type TarEntry = {
|
|
15
|
+
name: string
|
|
16
|
+
content: Uint8Array
|
|
17
|
+
mode?: number
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const BLOCK = 512
|
|
21
|
+
const ENC = new TextEncoder()
|
|
22
|
+
|
|
23
|
+
function writeString(buf: Uint8Array, offset: number, length: number, value: string): void {
|
|
24
|
+
const bytes = ENC.encode(value)
|
|
25
|
+
buf.set(bytes.subarray(0, Math.min(bytes.length, length)), offset)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function writeOctal(buf: Uint8Array, offset: number, length: number, value: number): void {
|
|
29
|
+
// length-1 octal digits + trailing NUL
|
|
30
|
+
const oct = value.toString(8).padStart(length - 1, '0')
|
|
31
|
+
writeString(buf, offset, length - 1, oct)
|
|
32
|
+
buf[offset + length - 1] = 0
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function buildHeader(entry: TarEntry): Uint8Array {
|
|
36
|
+
const header = new Uint8Array(BLOCK)
|
|
37
|
+
writeString(header, 0, 100, entry.name)
|
|
38
|
+
writeOctal(header, 100, 8, entry.mode ?? 0o644)
|
|
39
|
+
writeOctal(header, 108, 8, 0)
|
|
40
|
+
writeOctal(header, 116, 8, 0)
|
|
41
|
+
writeOctal(header, 124, 12, entry.content.length)
|
|
42
|
+
writeOctal(header, 136, 12, Math.floor(Date.now() / 1000))
|
|
43
|
+
header.fill(0x20, 148, 156)
|
|
44
|
+
header[156] = 0x30 // '0' = regular file
|
|
45
|
+
writeString(header, 257, 6, 'ustar\0')
|
|
46
|
+
writeString(header, 263, 2, '00')
|
|
47
|
+
// Checksum: sum of all bytes with checksum field treated as spaces.
|
|
48
|
+
let sum = 0
|
|
49
|
+
for (let index = 0; index < BLOCK; index++) {
|
|
50
|
+
sum += header[index] ?? 0
|
|
51
|
+
}
|
|
52
|
+
writeOctal(header, 148, 7, sum)
|
|
53
|
+
header[155] = 0x20 // trailing space after checksum digits
|
|
54
|
+
return header
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/*
|
|
58
|
+
Builds a gzipped tarball from the given entries and returns the bytes.
|
|
59
|
+
Sized eagerly (sum of headers + padded contents + 2 trailing blocks).
|
|
60
|
+
*/
|
|
61
|
+
export function createTarGz(entries: TarEntry[]): Uint8Array {
|
|
62
|
+
let totalSize = BLOCK * 2 // trailing zero blocks
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
totalSize += BLOCK
|
|
65
|
+
totalSize += Math.ceil(entry.content.length / BLOCK) * BLOCK
|
|
66
|
+
}
|
|
67
|
+
const tar = new Uint8Array(totalSize)
|
|
68
|
+
let offset = 0
|
|
69
|
+
for (const entry of entries) {
|
|
70
|
+
tar.set(buildHeader(entry), offset)
|
|
71
|
+
offset += BLOCK
|
|
72
|
+
tar.set(entry.content, offset)
|
|
73
|
+
offset += Math.ceil(entry.content.length / BLOCK) * BLOCK
|
|
74
|
+
}
|
|
75
|
+
return Bun.gzipSync(tar)
|
|
76
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
+
import { exeSuffix } from '../../shared/exeSuffix.ts'
|
|
3
|
+
import { isCompileTarget } from '../../shared/isCompileTarget.ts'
|
|
4
|
+
import { log } from '../../shared/log.ts'
|
|
5
|
+
import { normalizeTarget } from '../../shared/normalizeTarget.ts'
|
|
6
|
+
import { buildEnvContent } from './buildEnvContent.ts'
|
|
7
|
+
import { createTarGz } from './createTarGz.ts'
|
|
8
|
+
import { maxSourceMtime } from './maxSourceMtime.ts'
|
|
9
|
+
|
|
10
|
+
// The sibling server binary's name for a platform — `server` / `server.exe` — must
|
|
11
|
+
// match what resolveServerBinary() looks for next to the unpacked CLI binary.
|
|
12
|
+
function serverBinaryName(platform: string): string {
|
|
13
|
+
return `server${exeSuffix(normalizeTarget(platform))}`
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/*
|
|
17
|
+
Process-wide per-platform build coalescing. Two concurrent curls for
|
|
18
|
+
the same /__belte/cli/<platform> share one promise; the later requests
|
|
19
|
+
await the same one the first installed. The promise both runs the
|
|
20
|
+
freshness check AND the rebuild, so the map insertion is synchronous
|
|
21
|
+
relative to the first request's entry into the function — no window
|
|
22
|
+
between an `await` and `pendingBuilds.set` for a second concurrent
|
|
23
|
+
request to slip through and fire its own buildCli against the same
|
|
24
|
+
output paths.
|
|
25
|
+
*/
|
|
26
|
+
const pendingBuilds = new Map<string, Promise<string | undefined>>()
|
|
27
|
+
|
|
28
|
+
async function ensurePlatformBinary(
|
|
29
|
+
platform: string,
|
|
30
|
+
programName: string,
|
|
31
|
+
cwd: string,
|
|
32
|
+
): Promise<string | undefined> {
|
|
33
|
+
const existing = pendingBuilds.get(platform)
|
|
34
|
+
if (existing) {
|
|
35
|
+
return existing
|
|
36
|
+
}
|
|
37
|
+
const promise = computeBinary(platform, programName, cwd)
|
|
38
|
+
pendingBuilds.set(platform, promise)
|
|
39
|
+
/*
|
|
40
|
+
Drop the entry after settlement so a later request rebuilds if the
|
|
41
|
+
source has changed again. Identity-guard so a still-pending entry
|
|
42
|
+
installed by a follow-up request isn't evicted by ours.
|
|
43
|
+
*/
|
|
44
|
+
promise.finally(() => {
|
|
45
|
+
if (pendingBuilds.get(platform) === promise) {
|
|
46
|
+
pendingBuilds.delete(platform)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
return promise
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async function computeBinary(
|
|
53
|
+
platform: string,
|
|
54
|
+
programName: string,
|
|
55
|
+
cwd: string,
|
|
56
|
+
): Promise<string | undefined> {
|
|
57
|
+
const dir = `${cwd}/dist/cli-thin/${platform}`
|
|
58
|
+
const binaryPath = `${dir}/${programName}`
|
|
59
|
+
const serverPath = `${dir}/${serverBinaryName(platform)}`
|
|
60
|
+
/*
|
|
61
|
+
On-disk binaries are fresh when both the CLI and its sibling server exist AND
|
|
62
|
+
the CLI's mtime beats the newest rpc/socket source mtime. The mtime check
|
|
63
|
+
catches the common dev iteration where the user edits an rpc handler but didn't
|
|
64
|
+
run `belte cli` again; the server-exists check forces a rebuild for a dist
|
|
65
|
+
produced before the CLI co-shipped a server. Other source paths (project lib,
|
|
66
|
+
transitive imports) fall back to manual rebuild.
|
|
67
|
+
*/
|
|
68
|
+
const binaryFile = Bun.file(binaryPath)
|
|
69
|
+
const serverFile = Bun.file(serverPath)
|
|
70
|
+
if ((await binaryFile.exists()) && (await serverFile.exists())) {
|
|
71
|
+
const binaryMtime = (await binaryFile.stat()).mtimeMs
|
|
72
|
+
const sourceMtime = await maxSourceMtime(cwd)
|
|
73
|
+
if (binaryMtime >= sourceMtime) {
|
|
74
|
+
return binaryPath
|
|
75
|
+
}
|
|
76
|
+
log.info(`thin cli for ${platform} is stale — rebuilding`)
|
|
77
|
+
}
|
|
78
|
+
try {
|
|
79
|
+
log.info(`lazy-building cli + server for ${platform}…`)
|
|
80
|
+
// Lazy-import buildCli so the build pipeline isn't pulled into
|
|
81
|
+
// production processes that never serve a download.
|
|
82
|
+
const { buildCli } = await import('../../../buildCli.ts')
|
|
83
|
+
await buildCli({
|
|
84
|
+
cwd,
|
|
85
|
+
platforms: [normalizeTarget(platform)],
|
|
86
|
+
})
|
|
87
|
+
return (await binaryFile.exists()) && (await serverFile.exists()) ? binaryPath : undefined
|
|
88
|
+
} catch (error) {
|
|
89
|
+
log.error(error)
|
|
90
|
+
return undefined
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/*
|
|
95
|
+
Handles GET /__belte/cli/<platform> — streams a gzipped tarball
|
|
96
|
+
containing the platform-specific thin binary + a `.env` carrying
|
|
97
|
+
BELTE_APP_URL (and BELTE_APP_TOKEN if the inbound request was authenticated).
|
|
98
|
+
|
|
99
|
+
Thin binaries live at `dist/cli-thin/<platform>/<programName>`
|
|
100
|
+
(produced by `belte cli` with BELTE_APP_URL set). Missing platforms produce
|
|
101
|
+
404 — the install script reports it, doesn't try to fall back.
|
|
102
|
+
*/
|
|
103
|
+
export async function handleCliDownload(
|
|
104
|
+
request: Request,
|
|
105
|
+
platform: string,
|
|
106
|
+
programName: string,
|
|
107
|
+
cwd: string,
|
|
108
|
+
): Promise<Response> {
|
|
109
|
+
/*
|
|
110
|
+
Validate the URL-supplied platform against the known target set before any
|
|
111
|
+
filesystem access or lazy build. Without this, an arbitrary `platform`
|
|
112
|
+
segment flows into `dist/cli-thin/<platform>` paths (a traversal/oracle
|
|
113
|
+
surface) and a cache miss triggers an expensive cross-compile — so spraying
|
|
114
|
+
distinct junk strings amplifies into unbounded concurrent builds.
|
|
115
|
+
*/
|
|
116
|
+
if (!isCompileTarget(normalizeTarget(platform))) {
|
|
117
|
+
return new Response(`unknown platform: ${platform}`, {
|
|
118
|
+
status: 404,
|
|
119
|
+
headers: { 'Cache-Control': NO_STORE },
|
|
120
|
+
})
|
|
121
|
+
}
|
|
122
|
+
const binaryPath = await ensurePlatformBinary(platform, programName, cwd)
|
|
123
|
+
if (!binaryPath) {
|
|
124
|
+
return new Response(`unknown platform: ${platform}`, {
|
|
125
|
+
status: 404,
|
|
126
|
+
headers: { 'Cache-Control': NO_STORE },
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
const appUrl = new URL(request.url).origin
|
|
130
|
+
const auth = request.headers.get('authorization')
|
|
131
|
+
const bearer =
|
|
132
|
+
auth && auth.toLowerCase().startsWith('bearer ') ? auth.slice('bearer '.length) : undefined
|
|
133
|
+
const envContent = buildEnvContent(appUrl, bearer)
|
|
134
|
+
|
|
135
|
+
const serverPath = `${cwd}/dist/cli-thin/${platform}/${serverBinaryName(platform)}`
|
|
136
|
+
const [binaryBytes, serverBytes] = await Promise.all([
|
|
137
|
+
Bun.file(binaryPath).bytes(),
|
|
138
|
+
Bun.file(serverPath).bytes(),
|
|
139
|
+
])
|
|
140
|
+
// Ship the server beside the CLI so `/start` can spawn a local instance.
|
|
141
|
+
const archive = createTarGz([
|
|
142
|
+
{ name: programName, content: binaryBytes, mode: 0o755 },
|
|
143
|
+
{ name: serverBinaryName(platform), content: serverBytes, mode: 0o755 },
|
|
144
|
+
{ name: '.env', content: new TextEncoder().encode(envContent), mode: 0o644 },
|
|
145
|
+
])
|
|
146
|
+
return new Response(archive, {
|
|
147
|
+
headers: {
|
|
148
|
+
'Content-Type': 'application/gzip',
|
|
149
|
+
'Content-Disposition': `attachment; filename="${programName}-${platform}.tar.gz"`,
|
|
150
|
+
'Cache-Control': NO_STORE,
|
|
151
|
+
},
|
|
152
|
+
})
|
|
153
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import { NO_STORE } from '../../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
+
import { installScript } from './installScript.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
The request host is reflected verbatim into a shell script the user pipes
|
|
6
|
+
to `sh`, so it's constrained to the strict authority charset: letters,
|
|
7
|
+
digits, `.`, `-`, `_`, `:` (port + IPv6 separators), and IPv6 `[` `]`
|
|
8
|
+
brackets. That set excludes every character that could break out of the
|
|
9
|
+
interpolated `URL="…"` line in installScript (`"`, `$`, backtick, `\`,
|
|
10
|
+
whitespace), neutralising shell injection via a crafted Host header
|
|
11
|
+
regardless of how lenient the upstream URL parser is.
|
|
12
|
+
*/
|
|
13
|
+
const SAFE_HOST = /^[A-Za-z0-9._:[\]-]+$/
|
|
14
|
+
|
|
15
|
+
/*
|
|
16
|
+
Handles GET /__belte/cli — returns the platform-detecting shell script.
|
|
17
|
+
Authoritative URL for the tarball is derived from the inbound request
|
|
18
|
+
(so the script's curl line points at whatever host the user reached us
|
|
19
|
+
on). Program name is the bundler-emitted `belte:cli-name` value.
|
|
20
|
+
*/
|
|
21
|
+
export function handleCliInstall(request: Request, programName: string): Response {
|
|
22
|
+
const url = new URL(request.url)
|
|
23
|
+
if (!SAFE_HOST.test(url.host)) {
|
|
24
|
+
return new Response('Bad Request', {
|
|
25
|
+
status: 400,
|
|
26
|
+
headers: { 'Cache-Control': NO_STORE },
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
const appUrl = url.origin
|
|
30
|
+
const script = installScript(appUrl, programName)
|
|
31
|
+
return new Response(script, {
|
|
32
|
+
headers: {
|
|
33
|
+
'Content-Type': 'text/x-shellscript; charset=utf-8',
|
|
34
|
+
'Cache-Control': NO_STORE,
|
|
35
|
+
},
|
|
36
|
+
})
|
|
37
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/*
|
|
2
|
+
The shell script returned by `GET /__belte/cli` (no platform). Detects
|
|
3
|
+
uname OS + arch, normalises common arch aliases, then curls the right
|
|
4
|
+
platform-specific tarball and extracts it into `$BELTE_INSTALL_DIR`
|
|
5
|
+
(default `~/.local/bin`). The tarball already contains the `.env` next
|
|
6
|
+
to the binary — no separate config write step in the script.
|
|
7
|
+
|
|
8
|
+
The script is rendered server-side so `<BELTE_APP_URL>` is the request's own
|
|
9
|
+
origin and the embedded curl URL needs no escaping or quoting beyond
|
|
10
|
+
basic shell hygiene.
|
|
11
|
+
*/
|
|
12
|
+
export function installScript(appUrl: string, programName: string): string {
|
|
13
|
+
return `#!/usr/bin/env sh
|
|
14
|
+
set -e
|
|
15
|
+
OS=$(uname -s | tr '[:upper:]' '[:lower:]')
|
|
16
|
+
case "$(uname -m)" in
|
|
17
|
+
x86_64|amd64) ARCH=x64 ;;
|
|
18
|
+
aarch64|arm64) ARCH=arm64 ;;
|
|
19
|
+
*) echo "unsupported architecture: $(uname -m)" >&2 ; exit 1 ;;
|
|
20
|
+
esac
|
|
21
|
+
INSTALL_DIR="\${BELTE_INSTALL_DIR:-$HOME/.local/bin}"
|
|
22
|
+
mkdir -p "$INSTALL_DIR"
|
|
23
|
+
URL="${appUrl.replace(/\/$/, '')}/__belte/cli/\${OS}-\${ARCH}"
|
|
24
|
+
echo "installing ${programName} from $URL into $INSTALL_DIR"
|
|
25
|
+
curl -fsSL "$URL" | tar -xz -C "$INSTALL_DIR"
|
|
26
|
+
echo "installed: $INSTALL_DIR/${programName}"
|
|
27
|
+
echo "ensure $INSTALL_DIR is in your PATH"
|
|
28
|
+
`
|
|
29
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs'
|
|
2
|
+
import { Glob } from 'bun'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Returns the most-recent mtime across every rpc + socket source file in
|
|
6
|
+
the project, or 0 when both directories are absent. The lazy CLI
|
|
7
|
+
download path compares this to the binary's mtime to decide whether to
|
|
8
|
+
rebuild — covers the common dev iteration of "user edited an rpc
|
|
9
|
+
handler" without needing to scan transitively-imported modules. Globs and
|
|
10
|
+
stats run concurrently since each file is independent.
|
|
11
|
+
*/
|
|
12
|
+
export async function maxSourceMtime(cwd: string): Promise<number> {
|
|
13
|
+
const roots = [`${cwd}/src/server/rpc`, `${cwd}/src/server/sockets`].filter(existsSync)
|
|
14
|
+
const perRoot = await Promise.all(
|
|
15
|
+
roots.map(async (root) => {
|
|
16
|
+
const files = await Array.fromAsync(
|
|
17
|
+
new Glob('**/*.ts').scan({ cwd: root, onlyFiles: true }),
|
|
18
|
+
)
|
|
19
|
+
return files.map((file) => `${root}/${file}`)
|
|
20
|
+
}),
|
|
21
|
+
)
|
|
22
|
+
const mtimes = await Promise.all(
|
|
23
|
+
perRoot.flat().map(async (path) => (await Bun.file(path).stat()).mtimeMs),
|
|
24
|
+
)
|
|
25
|
+
return mtimes.reduce((newest, mtime) => Math.max(newest, mtime), 0)
|
|
26
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import { requestContext } from './runtime/requestContext.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
The cookie jar for the in-flight request. Reads parse the inbound `Cookie`
|
|
5
|
+
header; writes (`set` / `delete`) are collected and flushed to the response as
|
|
6
|
+
`Set-Cookie` headers when the handler returns (see runWithRequestScope). Backed
|
|
7
|
+
by Bun's native `CookieMap`, so it's a live `Map<string, string>` plus
|
|
8
|
+
`.set(name, value, options)` carrying the standard attributes (httpOnly, secure,
|
|
9
|
+
sameSite, maxAge, path, …) and `.delete(name)` for expiry:
|
|
10
|
+
|
|
11
|
+
const jar = cookies()
|
|
12
|
+
const session = jar.get('session') // read inbound
|
|
13
|
+
jar.set('session', token, { httpOnly: true, sameSite: 'lax' })
|
|
14
|
+
jar.delete('session') // expire on the way out
|
|
15
|
+
|
|
16
|
+
Materialized lazily on first call and cached on the request store, so a request
|
|
17
|
+
that never touches cookies parses nothing and emits no `Set-Cookie`. Throws
|
|
18
|
+
outside a request scope, like request().
|
|
19
|
+
*/
|
|
20
|
+
export function cookies(): Bun.CookieMap {
|
|
21
|
+
const store = requestContext.getStore()
|
|
22
|
+
if (!store) {
|
|
23
|
+
throw new Error(
|
|
24
|
+
'[belte] cookies() called outside a request scope — it only resolves while an SSR render or rpc handler is in flight',
|
|
25
|
+
)
|
|
26
|
+
}
|
|
27
|
+
store.cookies ??= new Bun.CookieMap(store.req.headers.get('cookie') ?? '')
|
|
28
|
+
return store.cookies
|
|
29
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { StandardSchemaV1 } from '../shared/types/StandardSchemaV1.ts'
|
|
2
|
+
import { envSchemaStore } from './runtime/envSchemaStore.ts'
|
|
3
|
+
|
|
4
|
+
/*
|
|
5
|
+
Validates the process environment against a Standard Schema and returns the
|
|
6
|
+
typed, parsed config. Built to be called at module top level so a missing or
|
|
7
|
+
malformed variable fails the boot loudly rather than surfacing as `undefined`
|
|
8
|
+
deep inside a handler:
|
|
9
|
+
|
|
10
|
+
// src/server/config.ts
|
|
11
|
+
export const config = env(
|
|
12
|
+
v.object({ DATABASE_URL: v.string(), STRIPE_KEY: v.string() }),
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
Reads `Bun.env` (the process environment plus any `.env` Bun loaded), so any
|
|
16
|
+
Standard Schema library — zod, valibot, arktype — works without an adapter,
|
|
17
|
+
same as the verb helpers. Coercion (e.g. a numeric PORT) lives in the schema.
|
|
18
|
+
|
|
19
|
+
The schema is registered (envSchemaStore) so the bundle launcher can project
|
|
20
|
+
the first-run setup form from the same declaration. When the launcher imports
|
|
21
|
+
this purely to read that schema it sets `skipValidation`, so env() registers
|
|
22
|
+
and returns without validating Bun.env — boot validation stays the server's.
|
|
23
|
+
|
|
24
|
+
Validation must be synchronous — boot can't await config — so a schema whose
|
|
25
|
+
`validate` returns a Promise throws. On failure every issue is reported at once
|
|
26
|
+
(path + message) so a misconfigured deploy shows the full list rather than one
|
|
27
|
+
variable per restart.
|
|
28
|
+
*/
|
|
29
|
+
export function env<Schema extends StandardSchemaV1>(
|
|
30
|
+
schema: Schema,
|
|
31
|
+
): StandardSchemaV1.InferOutput<Schema> {
|
|
32
|
+
envSchemaStore.schema = schema
|
|
33
|
+
if (envSchemaStore.skipValidation) {
|
|
34
|
+
return Bun.env as unknown as StandardSchemaV1.InferOutput<Schema>
|
|
35
|
+
}
|
|
36
|
+
const result = schema['~standard'].validate(Bun.env)
|
|
37
|
+
if (result instanceof Promise) {
|
|
38
|
+
throw new Error('[belte] env() schema must validate synchronously')
|
|
39
|
+
}
|
|
40
|
+
if (result.issues) {
|
|
41
|
+
const lines = result.issues.map((issue) => {
|
|
42
|
+
const path = issue.path
|
|
43
|
+
?.map((segment) => String(typeof segment === 'object' ? segment.key : segment))
|
|
44
|
+
.join('.')
|
|
45
|
+
return path ? ` ${path}: ${issue.message}` : ` ${issue.message}`
|
|
46
|
+
})
|
|
47
|
+
throw new Error(`[belte] invalid environment:\n${lines.join('\n')}`)
|
|
48
|
+
}
|
|
49
|
+
return result.value
|
|
50
|
+
}
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import { NO_STORE } from '../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
+
import type { TypedResponse } from './rpc/types/TypedResponse.ts'
|
|
3
|
+
import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Plain-text error Response — clearer than constructing a Response by
|
|
7
|
+
hand with a status and a text body, and shaped so the client's
|
|
8
|
+
HttpError carries the message verbatim (`HttpError.response.text()`
|
|
9
|
+
returns the message, no parsing).
|
|
10
|
+
|
|
11
|
+
if (!order) return error(404, 'order not found')
|
|
12
|
+
|
|
13
|
+
`message` defaults to the status's standard reason phrase when
|
|
14
|
+
omitted (e.g. `error(404)` body = 'Not Found'). The body is
|
|
15
|
+
text/plain so intermediaries don't try to render or sniff it. A final
|
|
16
|
+
`ResponseInit` adds headers (e.g. `Retry-After` on a 429); the positional
|
|
17
|
+
`status` always wins over any `init.status`.
|
|
18
|
+
|
|
19
|
+
To short-circuit a handler instead of returning, `throw new Error(...)`
|
|
20
|
+
or `throw new HttpError(error(...))` — the framework's `app.handleError`
|
|
21
|
+
hook catches thrown errors. This helper deliberately returns a Response
|
|
22
|
+
rather than throwing one so a single `return error(...)` is the
|
|
23
|
+
expected pattern, with the same control flow as `return json(...)`.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/*
|
|
27
|
+
Standard reason phrases for the statuses error() is realistically called
|
|
28
|
+
with. Maintained explicitly because Bun's `Response` does not populate
|
|
29
|
+
`statusText` from the status code, so there's no platform table to read.
|
|
30
|
+
Unlisted codes fall back to `HTTP <status>`.
|
|
31
|
+
*/
|
|
32
|
+
const STATUS_TEXT: Record<number, string> = {
|
|
33
|
+
400: 'Bad Request',
|
|
34
|
+
401: 'Unauthorized',
|
|
35
|
+
403: 'Forbidden',
|
|
36
|
+
404: 'Not Found',
|
|
37
|
+
405: 'Method Not Allowed',
|
|
38
|
+
409: 'Conflict',
|
|
39
|
+
410: 'Gone',
|
|
40
|
+
422: 'Unprocessable Content',
|
|
41
|
+
429: 'Too Many Requests',
|
|
42
|
+
500: 'Internal Server Error',
|
|
43
|
+
501: 'Not Implemented',
|
|
44
|
+
502: 'Bad Gateway',
|
|
45
|
+
503: 'Service Unavailable',
|
|
46
|
+
504: 'Gateway Timeout',
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/*
|
|
50
|
+
Body type is `never` because `error()` only travels the non-2xx path on
|
|
51
|
+
the wire — the caller's `await fn(args)` throws `HttpError` and never
|
|
52
|
+
resolves to this response's body. Returning a TypedResponse<never> lets
|
|
53
|
+
the union of branches in a handler narrow to whatever the success
|
|
54
|
+
branch carries (`TypedResponse<{user}> | TypedResponse<never>` → Return
|
|
55
|
+
= {user}).
|
|
56
|
+
*/
|
|
57
|
+
export function error(status: number, message?: string, init?: ResponseInit): TypedResponse<never> {
|
|
58
|
+
const body = message ?? STATUS_TEXT[status] ?? `HTTP ${status}`
|
|
59
|
+
return new Response(
|
|
60
|
+
body,
|
|
61
|
+
withResponseDefaults(
|
|
62
|
+
init,
|
|
63
|
+
{
|
|
64
|
+
'Content-Type': 'text/plain; charset=utf-8',
|
|
65
|
+
'Cache-Control': NO_STORE,
|
|
66
|
+
},
|
|
67
|
+
status,
|
|
68
|
+
),
|
|
69
|
+
) as TypedResponse<never>
|
|
70
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NO_STORE } from '../shared/CACHE_CONTROL_VALUES.ts'
|
|
2
|
+
import type { TypedResponse } from './rpc/types/TypedResponse.ts'
|
|
3
|
+
import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
JSON Response with rpc-friendly defaults — same shape as
|
|
7
|
+
`Response.json(data, init)`, except `Cache-Control: no-store` is set
|
|
8
|
+
unless the caller overrides it. Intermediary caches (browsers, CDNs,
|
|
9
|
+
shared proxies) shouldn't cache rpc replies by default; the framework's
|
|
10
|
+
own per-request cache handles in-process dedupe.
|
|
11
|
+
|
|
12
|
+
export const getOrder = GET<{ id: string }>(async ({ id }) =>
|
|
13
|
+
json(await db.getOrder(id)),
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
The return type carries `T` as a phantom brand so the verb helper can
|
|
17
|
+
infer the caller-facing `Return` from the handler body — no need to
|
|
18
|
+
annotate `GET<Args, Return>` just to type the response shape.
|
|
19
|
+
|
|
20
|
+
For non-default cache policy pass `init.headers`; explicit
|
|
21
|
+
`cache-control` wins over the default.
|
|
22
|
+
*/
|
|
23
|
+
export function json<T>(data: T, init?: ResponseInit): TypedResponse<T> {
|
|
24
|
+
return Response.json(
|
|
25
|
+
data,
|
|
26
|
+
withResponseDefaults(init, { 'Cache-Control': NO_STORE }),
|
|
27
|
+
) as TypedResponse<T>
|
|
28
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Wraps an AsyncIterable<Frame> in a Response whose body is JSON Lines
|
|
3
|
+
(application/jsonl) — one JSON value per line, terminated by `\n`. Used
|
|
4
|
+
inside an rpc handler to turn a generator into a streaming HTTP response
|
|
5
|
+
that `subscribe(fn.stream)(args)` consumes frame-by-frame on the client.
|
|
6
|
+
|
|
7
|
+
export const orderFeed = GET<Args>((args) =>
|
|
8
|
+
jsonl(async function* () {
|
|
9
|
+
for await (const order of db.watchOrders(args)) yield order
|
|
10
|
+
}())
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
Cancellation flows from the consumer through ReadableStream's `cancel`
|
|
14
|
+
into `iter.return()` so the handler's `for await` exits via its normal
|
|
15
|
+
control path (DB cursors, file handles, etc. get to release in finally).
|
|
16
|
+
|
|
17
|
+
Errors thrown by the generator are emitted as a final
|
|
18
|
+
`{"$error":"<message>"}` line before the stream closes. The convention
|
|
19
|
+
keeps the format JSON-safe and lets the consumer distinguish "stream
|
|
20
|
+
ended cleanly" from "handler threw" without a side-channel. The full
|
|
21
|
+
error is logged server-side via the framework's error handler — only the
|
|
22
|
+
message crosses the wire.
|
|
23
|
+
*/
|
|
24
|
+
import { NO_STORE } from '../shared/CACHE_CONTROL_VALUES.ts'
|
|
25
|
+
import { jsonlErrorFrame } from '../shared/jsonlErrorFrame.ts'
|
|
26
|
+
import type { TypedResponse } from './rpc/types/TypedResponse.ts'
|
|
27
|
+
import { streamFromIterator } from './runtime/streamFromIterator.ts'
|
|
28
|
+
import { withResponseDefaults } from './runtime/withResponseDefaults.ts'
|
|
29
|
+
|
|
30
|
+
export function jsonl<Frame>(
|
|
31
|
+
iterable: AsyncIterable<Frame>,
|
|
32
|
+
init?: ResponseInit,
|
|
33
|
+
): TypedResponse<Frame> {
|
|
34
|
+
const body = streamFromIterator(iterable, {
|
|
35
|
+
encodeFrame: (value) => `${JSON.stringify(value)}\n`,
|
|
36
|
+
encodeError: (message) => jsonlErrorFrame.encode(message),
|
|
37
|
+
})
|
|
38
|
+
return new Response(
|
|
39
|
+
body,
|
|
40
|
+
withResponseDefaults(init, {
|
|
41
|
+
'Content-Type': 'application/jsonl; charset=utf-8',
|
|
42
|
+
'Cache-Control': NO_STORE,
|
|
43
|
+
'X-Content-Type-Options': 'nosniff',
|
|
44
|
+
}),
|
|
45
|
+
) as TypedResponse<Frame>
|
|
46
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { registerPrompt } from './registerPrompt.ts'
|
|
2
|
+
import type { Prompt } from './types/Prompt.ts'
|
|
3
|
+
import type { PromptOptions } from './types/PromptOptions.ts'
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Builds a Prompt from a name + options. The resolver plugin parses every
|
|
7
|
+
`src/mcp/prompts/<file>.md` and generates a module that calls
|
|
8
|
+
`definePrompt("<name>", { description, jsonSchema, render })`, so the file
|
|
9
|
+
path becomes the prompt's identity. Registers itself so the MCP dispatcher
|
|
10
|
+
can enumerate and render it.
|
|
11
|
+
*/
|
|
12
|
+
export function definePrompt(name: string, opts: PromptOptions): Prompt {
|
|
13
|
+
const self: Prompt = {
|
|
14
|
+
name,
|
|
15
|
+
description: opts.description,
|
|
16
|
+
render: opts.render,
|
|
17
|
+
}
|
|
18
|
+
registerPrompt({ prompt: self, jsonSchema: opts.jsonSchema })
|
|
19
|
+
return self
|
|
20
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PromptRegistryEntry } from './types/PromptRegistryEntry.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Process-wide registry of every prompt declared in the app. definePrompt
|
|
5
|
+
inserts on first construction (eagerly when the registry loader walks the
|
|
6
|
+
prompts manifest at MCP boot). The MCP server reads this to build its
|
|
7
|
+
`prompts/list` + `prompts/get` responses.
|
|
8
|
+
*/
|
|
9
|
+
export const promptRegistry = new Map<string, PromptRegistryEntry>()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// `{{name}}` placeholder, surrounding whitespace tolerated, names are
|
|
2
|
+
// word chars or hyphens to match valid MCP argument identifiers.
|
|
3
|
+
const PLACEHOLDER = /\{\{\s*([\w-]+)\s*\}\}/g
|
|
4
|
+
|
|
5
|
+
/*
|
|
6
|
+
Renders a markdown prompt body by substituting each `{{name}}` placeholder
|
|
7
|
+
with the matching argument value. Missing arguments collapse to an empty
|
|
8
|
+
string — MCP only enforces `required` at the client, so an optional
|
|
9
|
+
argument the model omits should vanish from the text. Called by the
|
|
10
|
+
render closure the resolver plugin generates for every `.md` prompt.
|
|
11
|
+
*/
|
|
12
|
+
export function renderPromptTemplate(template: string, args: Record<string, string>): string {
|
|
13
|
+
return template.replace(PLACEHOLDER, (_match, key: string) =>
|
|
14
|
+
args[key] === undefined ? '' : String(args[key]),
|
|
15
|
+
)
|
|
16
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/*
|
|
2
|
+
An MCP prompt declared by a markdown file under `src/mcp/prompts/`. The
|
|
3
|
+
resolver plugin parses the file's frontmatter + body and generates a call
|
|
4
|
+
to definePrompt, stamping in the `name` from the file path; `render(args)`
|
|
5
|
+
interpolates the body's `{{name}}` placeholders into the single user
|
|
6
|
+
message returned by `prompts/get`. Prompts are MCP-only — there is no
|
|
7
|
+
client-side counterpart, so the shape carries no ClientFlags.
|
|
8
|
+
*/
|
|
9
|
+
export type Prompt = {
|
|
10
|
+
readonly name: string
|
|
11
|
+
readonly description: string | undefined
|
|
12
|
+
render(args: Record<string, string>): string
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/*
|
|
2
|
+
Options definePrompt receives for one markdown prompt. The resolver plugin
|
|
3
|
+
generates this object from the file: `description` + `jsonSchema` come from
|
|
4
|
+
the frontmatter (the schema built from the `arguments` list), and `render`
|
|
5
|
+
closes over the parsed body. All of this is server-only — prompts are never
|
|
6
|
+
imported by client code.
|
|
7
|
+
*/
|
|
8
|
+
export type PromptOptions = {
|
|
9
|
+
description?: string
|
|
10
|
+
jsonSchema?: Record<string, unknown>
|
|
11
|
+
render: (args: Record<string, string>) => string
|
|
12
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import type { Prompt } from './Prompt.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Per-prompt registry record. The MCP dispatcher enumerates this to build
|
|
5
|
+
`prompts/list` (description + arguments from the JSON Schema) and to
|
|
6
|
+
dispatch `prompts/get` (render the body with the caller's arguments).
|
|
7
|
+
jsonSchema stays off the public Prompt shape so the render closure isn't
|
|
8
|
+
burdened with metadata it never reads.
|
|
9
|
+
*/
|
|
10
|
+
export type PromptRegistryEntry = {
|
|
11
|
+
prompt: Prompt
|
|
12
|
+
jsonSchema: Record<string, unknown> | undefined
|
|
13
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { Prompt } from './Prompt.ts'
|
|
2
|
+
|
|
3
|
+
/*
|
|
4
|
+
Manifest of prompt-name → module loader. Produced by the resolver plugin
|
|
5
|
+
from each `.md` under src/mcp/prompts/. Each markdown file is transformed
|
|
6
|
+
into a module that registers one Prompt (its `.name` stamped in by the
|
|
7
|
+
generated definePrompt call) on import. The registry loader imports every
|
|
8
|
+
module once so the MCP dispatcher can enumerate the full prompt surface.
|
|
9
|
+
*/
|
|
10
|
+
export type PromptRoutes = Record<string, () => Promise<Record<string, Prompt>>>
|