devflare 1.0.0-next.35 → 1.0.0-next.37
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/LLM.md +2 -0
- package/README.md +5 -4
- package/dist/bridge/client.d.ts +1 -1
- package/dist/bridge/gateway-runtime.d.ts +1 -1
- package/dist/bridge/gateway-runtime.d.ts.map +1 -1
- package/dist/bridge/v2/control-messages.d.ts +1 -6
- package/dist/bridge/v2/control-messages.d.ts.map +1 -1
- package/dist/bridge/v2/wire.d.ts +4 -4
- package/dist/bridge/v2/wire.d.ts.map +1 -1
- package/dist/browser.js +1 -1
- package/dist/cli/index.js +1 -1
- package/dist/{dev-mkhjdwh7.js → dev-xz2qxw0m.js} +70 -5
- package/package.json +2 -2
package/LLM.md
CHANGED
|
@@ -2227,6 +2227,7 @@ export default defineConfig({
|
|
|
2227
2227
|
queue: 'src/queue.ts',
|
|
2228
2228
|
scheduled: 'src/scheduled.ts',
|
|
2229
2229
|
email: 'src/email.ts',
|
|
2230
|
+
tail: 'src/tail.ts',
|
|
2230
2231
|
durableObjects: 'src/do/**/*.ts',
|
|
2231
2232
|
entrypoints: 'src/ep/**/*.ts',
|
|
2232
2233
|
routes: {
|
|
@@ -2566,6 +2567,7 @@ export default defineConfig({
|
|
|
2566
2567
|
queue: 'src/queue.ts',
|
|
2567
2568
|
scheduled: 'src/scheduled.ts',
|
|
2568
2569
|
email: 'src/email.ts',
|
|
2570
|
+
tail: 'src/tail.ts',
|
|
2569
2571
|
routes: false
|
|
2570
2572
|
},
|
|
2571
2573
|
triggers: {
|
package/README.md
CHANGED
|
@@ -98,18 +98,18 @@ bun test tests/worker.test.ts
|
|
|
98
98
|
| Need | Open |
|
|
99
99
|
| --- | --- |
|
|
100
100
|
| Add route files | `/docs/first-route-tree`, then `/docs/http-routing` |
|
|
101
|
-
| Add one binding | `/docs/first-bindings`, then `/docs/
|
|
101
|
+
| Add one binding | `/docs/first-bindings`, then `/docs/storage-bindings` |
|
|
102
102
|
| Pick a test helper | `/docs/test-helper-reference` |
|
|
103
103
|
| Deploy safely | `/docs/deploy-command-recipes` |
|
|
104
104
|
| Check support stance | `/docs/feature-index` |
|
|
105
|
-
| Copy a larger example |
|
|
105
|
+
| Copy a larger example | `cases/README.md` |
|
|
106
106
|
|
|
107
107
|
## Package Entrypoints
|
|
108
108
|
|
|
109
109
|
| Import | Use |
|
|
110
110
|
| --- | --- |
|
|
111
|
-
| `devflare` | Node-side utilities: `defineConfig`, `preview`, `loadConfig`, `loadResolvedConfig`, `compileConfig`, `stringifyConfig`, `configSchema`, `ref()`, `workerName`, `env`, `durableObject`, `getDurableObjectOptions`, `runCli`, `parseArgs` |
|
|
112
|
-
| `devflare/config` |
|
|
111
|
+
| `devflare` | Node-side utilities: `defineConfig`, `preview`, `loadConfig`, `loadResolvedConfig`, `compileConfig`, `stringifyConfig`, `configSchema`, `ConfigNotFoundError`, `ConfigValidationError`, `ConfigResourceResolutionError`, `ref()`, `workerName`, `env`, `vars`, `durableObject`, `getDurableObjectOptions`, `runCli`, `parseArgs` |
|
|
112
|
+
| `devflare/config` | Lightweight config-authoring entry (no CLI/bridge/test barrel): `defineConfig`, `env`, `preview`, `ref`, plus the config types. The full Node-side compiler and normalizer helpers live on the bare devflare import instead. |
|
|
113
113
|
| `devflare/runtime` | Worker-safe runtime helpers: `env`, `ctx`, `event`, `locals`, `sequence`, `defineFetchHandler`, `defineQueueHandler`, `defineScheduledHandler`, `markResolveStyle`, `markWorkerStyle`, `createResolveFetch`, `invokeFetchHandler`, `invokeFetchModule`, `matchFetchRoute`, `invokeRouteModules`, `createRouteResolve`, event creators and getters |
|
|
114
114
|
| `devflare/test` | Testing helpers: `createTestContext`, `env`, `cf`, `worker`, `queue`, `scheduled`, `email`, `tail`, `shouldSkip`, `createOfflineEnv`, `createOfflineBindings`, `describeOfflineSupport`, `getOfflineSupportMatrix`, `containers`, `detectContainerEngine`, `getContainerSkipReason`, `stopActiveContainers`, `createMockEnv`, `createMockKV`, `createMockD1`, `createMockR2`, `createMockQueue`, `createMockRateLimit`, `createMockVersionMetadata`, `createMockHyperdrive`, `createMockWorkerLoader`, `createMockMTLSCertificate`, `createMockDispatchNamespace`, `createMockWorkflow`, `createMockPipeline`, `createMockImagesBinding`, `createMockMediaBinding`, `createMockArtifacts`, `createMockAISearchInstance`, `createMockAISearchNamespace`, `createMockTestContext`, `withTestContext`, `resolveServiceBindings`, `resolveDOBindings`, `clearBundleCache` |
|
|
115
115
|
| `devflare/vite` | Vite integration: `devflarePlugin`, `getCloudflareConfig`, `getDevflareConfigs`, `getPluginContext`, `hasInlineViteConfig`, `resolveEffectiveViteProject`, `resolveViteUserConfig`, `writeGeneratedViteConfig` |
|
|
@@ -122,6 +122,7 @@ Runtime import rule of thumb:
|
|
|
122
122
|
- Use `devflare/config` in config files.
|
|
123
123
|
- Use `devflare/runtime` in Worker code.
|
|
124
124
|
- Use `devflare/test` in tests.
|
|
125
|
+
- Use `devflare/vite` in a Vite project; it re-exports Vite's `Plugin`/`ConfigEnv`/`UserConfig` types, so type-checking it needs `vite` (an optional peer) installed.
|
|
125
126
|
- Use bare `devflare` for Node-side package tooling and the unified env proxy only when that is intentional.
|
|
126
127
|
|
|
127
128
|
The authoritative frozen-entrypoint list and the stability guarantee on it live
|
package/dist/bridge/client.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
type WebSocketConstructor = new (url: string) => WebSocket;
|
|
2
2
|
export declare function resolveBridgeWebSocketConstructor(runtimeWebSocket?: unknown): Promise<WebSocketConstructor>;
|
|
3
3
|
export interface BridgeClientOptions {
|
|
4
|
-
/** Bridge WebSocket URL (default: ws://localhost:
|
|
4
|
+
/** Bridge WebSocket URL (default: ws://localhost:8787) */
|
|
5
5
|
url?: string;
|
|
6
6
|
/** Auto-reconnect on disconnect */
|
|
7
7
|
autoReconnect?: boolean;
|
|
@@ -4,5 +4,5 @@
|
|
|
4
4
|
* module. All symbols are declared with `function`/`const` so they are
|
|
5
5
|
* hoisted in both embedding sites.
|
|
6
6
|
*/
|
|
7
|
-
export declare const GATEWAY_RUNTIME_JS = "\nconst RAW_EMAIL = 'EmailMessage::raw'\n\nfunction arrayBufferToBase64(buffer) {\n\tconst bytes = new Uint8Array(buffer)\n\tlet binary = ''\n\tfor (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i])\n\treturn btoa(binary)\n}\n\nfunction base64ToArrayBuffer(base64) {\n\tconst binary = atob(base64)\n\tconst bytes = new Uint8Array(binary.length)\n\tfor (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)\n\treturn bytes.buffer\n}\n\nfunction serializeR2Object(obj) {\n\tif (!obj) return null\n\treturn {\n\t\t__type: 'R2Object',\n\t\tkey: obj.key,\n\t\tversion: obj.version,\n\t\tsize: obj.size,\n\t\tetag: obj.etag,\n\t\thttpEtag: obj.httpEtag,\n\t\tchecksums: obj.checksums,\n\t\tuploaded: obj.uploaded?.toISOString(),\n\t\thttpMetadata: obj.httpMetadata,\n\t\tcustomMetadata: obj.customMetadata,\n\t\trange: obj.range,\n\t\tstorageClass: obj.storageClass\n\t}\n}\n\nfunction serializeR2ObjectBody(obj, bodyData) {\n\tif (!obj) return null\n\treturn {\n\t\t__type: 'R2ObjectBody',\n\t\tkey: obj.key,\n\t\tversion: obj.version,\n\t\tsize: obj.size,\n\t\tetag: obj.etag,\n\t\thttpEtag: obj.httpEtag,\n\t\tchecksums: obj.checksums,\n\t\tuploaded: obj.uploaded?.toISOString(),\n\t\thttpMetadata: obj.httpMetadata,\n\t\tcustomMetadata: obj.customMetadata,\n\t\trange: obj.range,\n\t\tstorageClass: obj.storageClass,\n\t\tbodyData\n\t}\n}\n\nfunction serializeR2Objects(result) {\n\tif (!result) return null\n\treturn {\n\t\tobjects: result.objects.map(serializeR2Object),\n\t\ttruncated: result.truncated,\n\t\tcursor: result.cursor,\n\t\tdelimitedPrefixes: result.delimitedPrefixes\n\t}\n}\n\nasync function serializeResponse(response) {\n\tlet body = null\n\tif (response.body) {\n\t\tconst bytes = await response.arrayBuffer()\n\t\tif (bytes.byteLength > 0) {\n\t\t\tbody = { type: 'bytes', data: arrayBufferToBase64(bytes) }\n\t\t}\n\t}\n\treturn {\n\t\tstatus: response.status,\n\t\tstatusText: response.statusText,\n\t\theaders: [...response.headers.entries()],\n\t\tbody\n\t}\n}\n\nfunction deserializeRequest(serializedReq) {\n\treturn new Request(serializedReq.url, {\n\t\tmethod: serializedReq.method,\n\t\theaders: serializedReq.headers,\n\t\tbody: serializedReq.body?.type === 'bytes'\n\t\t\t? base64ToArrayBuffer(serializedReq.body.data)\n\t\t\t: undefined,\n\t\tredirect: serializedReq.redirect\n\t})\n}\n\nfunction createEmailMessageRaw(raw) {\n\tif (typeof raw === 'string' || raw instanceof ReadableStream) {\n\t\treturn raw\n\t}\n\tif (raw instanceof ArrayBuffer || raw instanceof Uint8Array) {\n\t\treturn new Response(raw).body\n\t}\n\tthrow new Error('Unsupported EmailMessage raw payload')\n}\n\nfunction isDurableObjectNamespace(binding) {\n\treturn !!binding\n\t\t&& typeof binding.idFromName === 'function'\n\t\t&& typeof binding.idFromString === 'function'\n\t\t&& typeof binding.newUniqueId === 'function'\n}\n\n/**\n * Resolve a (possibly jurisdiction-scoped) DurableObjectNamespace.\n * Forwards the jurisdiction faithfully when the binding supports it\n * (real Cloudflare), and degrades to the plain binding otherwise\n * (miniflare/workerd local emulation, which does not pin jurisdictions).\n */\nfunction resolveDoNamespace(binding, jurisdiction) {\n\tif (jurisdiction && typeof binding.jurisdiction === 'function') {\n\t\treturn binding.jurisdiction(jurisdiction)\n\t}\n\treturn binding\n}\n\n/**\n * Execute an RPC method against the gateway's bindings.\n *\n * Method format: \"binding.operation\". Operations must be namespaced by\n * binding kind (e.g. \"kv.get\", \"r2.head\", \"d1.stmt.first\", \"do.fetch\",\n * \"service.fetch\", \"queue.send\", \"email.send\", \"ai.run\"). Bare verbs and the legacy\n * \"stmt.*\" / \"stub.*\" sub-prefixes were removed in B3-final and now throw.\n * Method vocabulary must stay in sync with the canonical server in\n * src/bridge/server.ts.\n */\nasync function executeRpcMethod(method, params, env, _ctx) {\n\tconst parts = method.split('.')\n\tif (parts.length < 2) throw new Error('Invalid method format: ' + method)\n\n\tconst bindingName = parts[0]\n\tconst operation = parts.slice(1).join('.')\n\tconst binding = env[bindingName]\n\n\tif (!binding) throw new Error('Binding not found: ' + bindingName)\n\n\tconst isNamespaced =\n\t\toperation.indexOf('kv.') === 0 ||\n\t\toperation.indexOf('r2.') === 0 ||\n\t\toperation.indexOf('d1.') === 0 ||\n\t\toperation.indexOf('do.') === 0 ||\n\t\toperation.indexOf('service.') === 0 ||\n\t\toperation.indexOf('queue.') === 0 ||\n\t\toperation.indexOf('email.') === 0 ||\n\t\toperation.indexOf('ai.') === 0 ||\n\t\toperation.indexOf('workflow.') === 0 ||\n\t\toperation.indexOf('var.') === 0\n\tif (!isNamespaced) {\n\t\tthrow new Error(createUnsupportedBridgeOperationErrorMessage(bindingName, operation))\n\t}\n\n\t// KV\n\tif (operation === 'kv.get') return binding.get(params[0], params[1])\n\tif (operation === 'kv.put') return binding.put(params[0], params[1], params[2])\n\tif (operation === 'kv.delete') return binding.delete(params[0])\n\tif (operation === 'kv.list') return binding.list(params[0])\n\tif (operation === 'kv.getWithMetadata') return binding.getWithMetadata(params[0], params[1])\n\n\t// DO get (returns DOStub reference)\n\tif (operation === 'do.get') {\n\t\treturn { __type: 'DOStub', binding: bindingName, id: params[0] }\n\t}\n\n\t// R2\n\tif (operation === 'r2.head') return serializeR2Object(await binding.head(params[0]))\n\tif (operation === 'r2.get') {\n\t\tconst obj = await binding.get(params[0], params[1])\n\t\tif (!obj) return null\n\t\tconst body = await obj.arrayBuffer()\n\t\treturn serializeR2ObjectBody(obj, arrayBufferToBase64(body))\n\t}\n\tif (operation === 'r2.put') {\n\t\tlet value = params[1]\n\t\tif (value && typeof value === 'object') {\n\t\t\tif (value.__type === 'ArrayBuffer' || value.__type === 'Uint8Array') {\n\t\t\t\tvalue = base64ToArrayBuffer(value.data)\n\t\t\t}\n\t\t}\n\t\treturn serializeR2Object(await binding.put(params[0], value, params[2]))\n\t}\n\tif (operation === 'r2.delete') return binding.delete(params[0])\n\tif (operation === 'r2.list') return serializeR2Objects(await binding.list(params[0]))\n\n\t// D1\n\tif (operation === 'd1.exec') return binding.exec(params[0])\n\tif (operation === 'd1.batch') {\n\t\tconst statements = params[0].map((s) => binding.prepare(s.sql).bind(...(s.bindings || [])))\n\t\treturn binding.batch(statements)\n\t}\n\tif (operation.indexOf('d1.stmt.') === 0) {\n\t\tconst mode = operation.split('.')[2]\n\t\tconst [sql, ...rest] = params\n\t\tlet bindings = rest\n\t\tlet extraParam\n\t\tif (mode === 'first' || mode === 'raw') {\n\t\t\textraParam = rest[rest.length - 1]\n\t\t\tbindings = rest.slice(0, -1)\n\t\t}\n\t\tlet stmt = binding.prepare(sql)\n\t\tif (bindings.length > 0) stmt = stmt.bind(...bindings)\n\t\tif (mode === 'first') {\n\t\t\tif (typeof extraParam === 'string' && extraParam.length > 0) return stmt.first(extraParam)\n\t\t\treturn stmt.first()\n\t\t}\n\t\tif (mode === 'all') return stmt.all()\n\t\tif (mode === 'run') return stmt.run()\n\t\tif (mode === 'raw') return stmt.raw(extraParam)\n\t\tthrow new Error('Unknown stmt mode: ' + mode)\n\t}\n\n\t// Durable Objects\n\tif (operation === 'do.idFromName') {\n\t\tconst ns = resolveDoNamespace(binding, params[1])\n\t\tconst id = ns.idFromName(params[0])\n\t\treturn { __type: 'DOId', hex: id.toString() }\n\t}\n\tif (operation === 'do.idFromString') {\n\t\tconst id = binding.idFromString(params[0])\n\t\treturn { __type: 'DOId', hex: id.toString() }\n\t}\n\tif (operation === 'do.newUniqueId') {\n\t\tconst ns = resolveDoNamespace(binding, params[1])\n\t\tconst id = ns.newUniqueId(params[0])\n\t\treturn { __type: 'DOId', hex: id.toString() }\n\t}\n\tif (operation === 'do.fetch') {\n\t\tconst [, serializedId, serializedReq] = params\n\t\tconst id = binding.idFromString(serializedId.hex)\n\t\tconst stub = binding.get(id)\n\t\tconst response = await stub.fetch(new Request(serializedReq.url, {\n\t\t\tmethod: serializedReq.method,\n\t\t\theaders: serializedReq.headers,\n\t\t\tbody: serializedReq.body?.type === 'bytes'\n\t\t\t\t? base64ToArrayBuffer(serializedReq.body.data)\n\t\t\t\t: undefined\n\t\t}))\n\t\treturn serializeResponse(response)\n\t}\n\tif (operation === 'do.rpc') {\n\t\tconst [, serializedId, methodName, args] = params\n\t\tconst id = binding.idFromString(serializedId.hex)\n\t\tconst stub = binding.get(id)\n\t\tconst response = await stub.fetch(new Request('http://do/_rpc', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ method: methodName, params: args })\n\t\t}))\n\t\tconst result = await response.json()\n\t\tif (!result.ok) throw new Error(result.error?.message || 'RPC failed')\n\t\treturn result.result\n\t}\n\n\t// Service Bindings\n\tif (operation === 'service.fetch') {\n\t\tif (!binding || typeof binding.fetch !== 'function') {\n\t\t\tthrow new Error('Service binding ' + bindingName + ' does not support fetch()')\n\t\t}\n\t\tconst response = await binding.fetch(deserializeRequest(params[0]))\n\t\treturn serializeResponse(response)\n\t}\n\tif (operation === 'service.rpc') {\n\t\tconst methodName = params[0]\n\t\tif (typeof methodName !== 'string') {\n\t\t\tthrow new Error('Service binding ' + bindingName + ' RPC method name must be a string')\n\t\t}\n\t\tconst args = Array.isArray(params[1]) ? params[1] : []\n\t\tconst method = binding && binding[methodName]\n\t\tif (typeof method !== 'function') {\n\t\t\tthrow new Error('Service binding ' + bindingName + ' does not support ' + methodName + '()')\n\t\t}\n\t\treturn method.apply(binding, args)\n\t}\n\n\t// Queues\n\tif (operation === 'queue.send') return binding.send(params[0], params[1])\n\tif (operation === 'queue.sendBatch') return binding.sendBatch(params[0], params[1])\n\n\t// Send Email\n\tif (operation === 'email.send') {\n\t\tif (binding && typeof binding.send === 'function') {\n\t\t\tconst message = params[0]\n\t\t\tif (message && typeof message === 'object' && 'from' in message && 'to' in message && 'raw' in message) {\n\t\t\t\treturn binding.send({\n\t\t\t\t\tfrom: message.from,\n\t\t\t\t\tto: message.to,\n\t\t\t\t\t[RAW_EMAIL]: createEmailMessageRaw(message.raw)\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn binding.send(message)\n\t\t}\n\t\treturn { ok: true, simulated: true }\n\t}\n\n\t// Workflows\n\tif (operation === 'workflow.create') {\n\t\treturn serializeWorkflowInstance(await binding.create(params[0]))\n\t}\n\tif (operation === 'workflow.get') {\n\t\treturn serializeWorkflowInstance(await binding.get(params[0]))\n\t}\n\tif (operation === 'workflow.status') {\n\t\treturn (await binding.get(params[0])).status()\n\t}\n\tif (operation === 'workflow.pause') {\n\t\treturn (await binding.get(params[0])).pause()\n\t}\n\tif (operation === 'workflow.resume') {\n\t\treturn (await binding.get(params[0])).resume()\n\t}\n\tif (operation === 'workflow.terminate') {\n\t\treturn (await binding.get(params[0])).terminate()\n\t}\n\tif (operation === 'workflow.restart') {\n\t\treturn (await binding.get(params[0])).restart()\n\t}\n\tif (operation === 'workflow.sendEvent') {\n\t\treturn (await binding.get(params[0])).sendEvent(params[1])\n\t}\n\n\t// AI / generic run()\n\tif (operation === 'ai.run') {\n\t\tif (typeof binding.run !== 'function') {\n\t\t\tthrow new Error('Binding ' + bindingName + ' does not support run(): ' + method)\n\t\t}\n\t\treturn binding.run(params[0], params[1])\n\t}\n\n\tthrow new Error('Unknown operation: ' + method)\n}\n\nfunction createUnsupportedBridgeOperationErrorMessage(bindingName, operation) {\n\tconst base = \"[devflare][bridge] Unsupported bridge operation '\" + operation + \"' for binding '\" + bindingName + \"'.\"\n\tif (operation === 'fetch') {\n\t\treturn base + ' Devflare could not dispatch fetch() for this binding through the local bridge. '\n\t\t\t+ 'Expected Cloudflare API: env.' + bindingName + '.fetch(request). '\n\t\t\t+ 'If this came from SvelteKit platform.env, make sure the binding is declared as a service binding; '\n\t\t\t+ 'this is a Devflare local bridge issue when service bindings fall back to a bare fetch operation.'\n\t}\n\tif (operation === 'toString') {\n\t\treturn base + ' A platform.env value was coerced to a string through the bridge. '\n\t\t\t+ 'For SvelteKit local dev, declared vars should be plain string values and missing env names should read as undefined.'\n\t}\n\treturn base + ' Bare verbs and the legacy stmt.*/stub.* sub-prefixes are not supported; '\n\t\t+ 'use the namespaced form (e.g. kv.get, r2.put, d1.stmt.first, do.fetch, service.fetch).'\n}\n\nfunction serializeWorkflowInstance(instance) {\n\treturn {\n\t\t__type: 'WorkflowInstance',\n\t\tid: instance.id\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket bridge (shared with src/bridge/server.ts in shape)\n// ---------------------------------------------------------------------------\n// NOTE: wsProxies is intentionally created per handleBridgeWebSocket call so\n// state never leaks across connections or across gateway-script regenerations.\n\nasync function handleBridgeRpcCall(msg, ws, env, ctx) {\n\ttry {\n\t\tconst result = await executeRpcMethod(msg.method, msg.params, env, ctx)\n\t\tws.send(JSON.stringify({ t: 'rpc.ok', id: msg.id, result }))\n\t} catch (error) {\n\t\tws.send(JSON.stringify({\n\t\t\tt: 'rpc.err',\n\t\t\tid: msg.id,\n\t\t\terror: {\n\t\t\t\tcode: error?.code || 'INTERNAL_ERROR',\n\t\t\t\tmessage: error?.message || String(error)\n\t\t\t}\n\t\t}))\n\t}\n}\n\nasync function handleBridgeWsOpen(msg, ws, env, wsProxies) {\n\ttry {\n\t\tconst binding = env[msg.target.binding]\n\t\tconst id = binding.idFromString(msg.target.id)\n\t\tconst stub = binding.get(id)\n\n\t\tconst headers = new Headers(msg.target.headers || [])\n\t\theaders.set('Upgrade', 'websocket')\n\n\t\tconst response = await stub.fetch(new Request(msg.target.url, { method: 'GET', headers }))\n\t\tconst doWs = response.webSocket\n\n\t\tif (!doWs) {\n\t\t\tws.send(JSON.stringify({\n\t\t\t\tt: 'rpc.err',\n\t\t\t\tid: 'ws_' + msg.wid,\n\t\t\t\terror: { code: 'WS_FAILED', message: 'No WebSocket returned' }\n\t\t\t}))\n\t\t\treturn\n\t\t}\n\n\t\tdoWs.accept()\n\t\twsProxies.set(msg.wid, { doWs })\n\n\t\tdoWs.addEventListener('message', (event) => {\n\t\t\tconst isText = typeof event.data === 'string'\n\t\t\tconst data = isText ? event.data : arrayBufferToBase64(event.data)\n\t\t\tws.send(JSON.stringify({ t: 'ws.data', wid: msg.wid, data, isText }))\n\t\t})\n\n\t\tdoWs.addEventListener('close', (event) => {\n\t\t\tws.send(JSON.stringify({ t: 'ws.close', wid: msg.wid, code: event.code, reason: event.reason }))\n\t\t\twsProxies.delete(msg.wid)\n\t\t})\n\n\t\tws.send(JSON.stringify({ t: 'ws.opened', wid: msg.wid }))\n\t} catch (error) {\n\t\tws.send(JSON.stringify({\n\t\t\tt: 'rpc.err',\n\t\t\tid: 'ws_' + msg.wid,\n\t\t\terror: { code: 'WS_FAILED', message: error.message }\n\t\t}))\n\t}\n}\n\nfunction handleBridgeWsClose(msg, wsProxies) {\n\tconst proxy = wsProxies.get(msg.wid)\n\tif (proxy) {\n\t\tproxy.doWs.close(msg.code, msg.reason)\n\t\twsProxies.delete(msg.wid)\n\t}\n}\n\nasync function handleBridgeJsonMessage(data, ws, env, ctx, wsProxies) {\n\tconst msg = JSON.parse(data)\n\tswitch (msg.t) {\n\t\tcase 'hello':\n\t\t\t// v2 handshake \u2014 acknowledge with welcome echoing the negotiated\n\t\t\t// capability intersection. Capabilities advertised by the gateway\n\t\t\t// are kept in sync with src/bridge/client.ts (BRIDGE_CLIENT_CAPABILITIES).\n\t\t\tws.send(JSON.stringify({\n\t\t\t\tt: 'welcome',\n\t\t\t\tprotocolVersion: 2,\n\t\t\t\tcapabilities: ['streams', 'ws-relay', 'http-transfer']\n\t\t\t\t\t.filter((c) => Array.isArray(msg.capabilities) && msg.capabilities.includes(c))\n\t\t\t\t\t.sort()\n\t\t\t}))\n\t\t\tbreak\n\t\tcase 'rpc.call':\n\t\t\tawait handleBridgeRpcCall(msg, ws, env, ctx)\n\t\t\tbreak\n\t\tcase 'ws.open':\n\t\t\tawait handleBridgeWsOpen(msg, ws, env, wsProxies)\n\t\t\tbreak\n\t\tcase 'ws.close':\n\t\t\thandleBridgeWsClose(msg, wsProxies)\n\t\t\tbreak\n\t}\n}\n\nfunction handleBridgeWebSocket(request, env, ctx) {\n\tconst { 0: client, 1: server } = new WebSocketPair()\n\tserver.accept()\n\n\t// Per-connection state: recreated for every bridge client so reloads and\n\t// concurrent clients never share WS proxy entries.\n\tconst wsProxies = new Map()\n\n\tserver.addEventListener('message', async (event) => {\n\t\ttry {\n\t\t\tif (typeof event.data === 'string') {\n\t\t\t\tawait handleBridgeJsonMessage(event.data, server, env, ctx, wsProxies)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('[Gateway] Error:', error)\n\t\t}\n\t})\n\n\tserver.addEventListener('close', () => {\n\t\tfor (const proxy of wsProxies.values()) {\n\t\t\t// Best-effort cleanup: the DO-side WS may already be closed or in an\n\t\t\t// invalid state; any throw here would abort sibling closes. Surface\n\t\t\t// the swallowed error when DEVFLARE_DEBUG_BRIDGE is enabled.\n\t\t\ttry { proxy.doWs.close() } catch (error) {\n\t\t\t\tif (globalThis.DEVFLARE_DEBUG_BRIDGE) {\n\t\t\t\t\tconsole.warn('[devflare:bridge] proxy.doWs.close() failed', error)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\twsProxies.clear()\n\t})\n\n\treturn new Response(null, { status: 101, webSocket: client })\n}\n\n// ---------------------------------------------------------------------------\n// HTTP transfer for R2 bodies (shared with src/bridge/server.ts in shape)\n// ---------------------------------------------------------------------------\n\nasync function handleHttpTransfer(request, env, url) {\n\tconst transferIdEncoded = url.pathname.split('/').pop()\n\tconst transferId = decodeURIComponent(transferIdEncoded || '')\n\tconst [binding, ...keyParts] = transferId.split(':')\n\tconst key = keyParts.join(':')\n\tconst bucket = env[binding]\n\n\tif (!bucket) return new Response('Bucket not found: ' + binding, { status: 404 })\n\n\tif (request.method === 'PUT' || request.method === 'POST') {\n\t\tconst result = await bucket.put(key, request.body)\n\t\treturn new Response(JSON.stringify(serializeR2Object(result)), {\n\t\t\theaders: { 'Content-Type': 'application/json' }\n\t\t})\n\t}\n\n\tif (request.method === 'GET') {\n\t\tconst object = await bucket.get(key)\n\t\tif (!object) return new Response('Not found', { status: 404 })\n\t\treturn new Response(object.body, {\n\t\t\theaders: {\n\t\t\t\t'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',\n\t\t\t\t'Content-Length': String(object.size)\n\t\t\t}\n\t\t})\n\t}\n\n\treturn new Response('Method not allowed', { status: 405 })\n}\n";
|
|
7
|
+
export declare const GATEWAY_RUNTIME_JS = "\nconst RAW_EMAIL = 'EmailMessage::raw'\n\n// Inline body cap for proxied DO/service-fetch responses. Matches\n// HTTP_TRANSFER_THRESHOLD in wire.ts (512 KB). A larger body would exceed\n// workerd's ~1 MB WebSocket message limit once base64-encoded into the rpc.ok\n// frame, so it is rejected with a clear error rather than silently truncated.\nconst HTTP_TRANSFER_THRESHOLD = 512 * 1024\n\nfunction arrayBufferToBase64(buffer) {\n\tconst bytes = new Uint8Array(buffer)\n\tlet binary = ''\n\tfor (let i = 0; i < bytes.byteLength; i++) binary += String.fromCharCode(bytes[i])\n\treturn btoa(binary)\n}\n\nfunction base64ToArrayBuffer(base64) {\n\tconst binary = atob(base64)\n\tconst bytes = new Uint8Array(binary.length)\n\tfor (let i = 0; i < binary.length; i++) bytes[i] = binary.charCodeAt(i)\n\treturn bytes.buffer\n}\n\n// Binary WsData frame codec (10-byte header: kind:u8, id:u32 LE, seq:u32 LE,\n// flags:u8 + payload). MUST match wire.ts encodeBinaryFrame/decodeBinaryFrame so\n// the bridge client and this gateway agree on the WS-data wire format\n// (BinaryKind.WsData = 2, BinaryFlags.TEXT = 2).\nfunction encodeWsDataFrame(wid, flags, payload) {\n\tconst frame = new Uint8Array(10 + payload.byteLength)\n\tconst view = new DataView(frame.buffer)\n\tview.setUint8(0, 2)\n\tview.setUint32(1, wid, true)\n\tview.setUint32(5, 0, true)\n\tview.setUint8(9, flags)\n\tframe.set(payload, 10)\n\treturn frame\n}\n\nfunction decodeWsDataFrame(buffer) {\n\tconst bytes = new Uint8Array(buffer)\n\tif (bytes.byteLength < 10) return null\n\tconst view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)\n\treturn {\n\t\tkind: view.getUint8(0),\n\t\tid: view.getUint32(1, true),\n\t\tflags: view.getUint8(9),\n\t\tpayload: bytes.subarray(10)\n\t}\n}\n\nfunction serializeR2Object(obj) {\n\tif (!obj) return null\n\treturn {\n\t\t__type: 'R2Object',\n\t\tkey: obj.key,\n\t\tversion: obj.version,\n\t\tsize: obj.size,\n\t\tetag: obj.etag,\n\t\thttpEtag: obj.httpEtag,\n\t\tchecksums: obj.checksums,\n\t\tuploaded: obj.uploaded?.toISOString(),\n\t\thttpMetadata: obj.httpMetadata,\n\t\tcustomMetadata: obj.customMetadata,\n\t\trange: obj.range,\n\t\tstorageClass: obj.storageClass\n\t}\n}\n\nfunction serializeR2ObjectBody(obj, bodyData) {\n\tif (!obj) return null\n\treturn {\n\t\t__type: 'R2ObjectBody',\n\t\tkey: obj.key,\n\t\tversion: obj.version,\n\t\tsize: obj.size,\n\t\tetag: obj.etag,\n\t\thttpEtag: obj.httpEtag,\n\t\tchecksums: obj.checksums,\n\t\tuploaded: obj.uploaded?.toISOString(),\n\t\thttpMetadata: obj.httpMetadata,\n\t\tcustomMetadata: obj.customMetadata,\n\t\trange: obj.range,\n\t\tstorageClass: obj.storageClass,\n\t\tbodyData\n\t}\n}\n\nfunction serializeR2Objects(result) {\n\tif (!result) return null\n\treturn {\n\t\tobjects: result.objects.map(serializeR2Object),\n\t\ttruncated: result.truncated,\n\t\tcursor: result.cursor,\n\t\tdelimitedPrefixes: result.delimitedPrefixes\n\t}\n}\n\nasync function serializeResponse(response) {\n\tlet body = null\n\tif (response.body) {\n\t\tconst bytes = await response.arrayBuffer()\n\t\tif (bytes.byteLength > HTTP_TRANSFER_THRESHOLD) {\n\t\t\tthrow new Error(\n\t\t\t\t'[devflare][bridge] Response body (' + bytes.byteLength + ' bytes) exceeds the '\n\t\t\t\t+ HTTP_TRANSFER_THRESHOLD + '-byte inline limit. DO and service-binding fetch responses '\n\t\t\t\t+ 'are delivered inline over the bridge WebSocket and cannot be streamed locally; keep '\n\t\t\t\t+ 'proxied response bodies under the limit (large R2 objects are exempt \u2014 read them through '\n\t\t\t\t+ 'the R2 binding, which uses the HTTP transfer side-channel).'\n\t\t\t)\n\t\t}\n\t\tif (bytes.byteLength > 0) {\n\t\t\tbody = { type: 'bytes', data: arrayBufferToBase64(bytes) }\n\t\t}\n\t}\n\treturn {\n\t\tstatus: response.status,\n\t\tstatusText: response.statusText,\n\t\theaders: [...response.headers.entries()],\n\t\tbody\n\t}\n}\n\nfunction deserializeRequest(serializedReq) {\n\treturn new Request(serializedReq.url, {\n\t\tmethod: serializedReq.method,\n\t\theaders: serializedReq.headers,\n\t\tbody: serializedReq.body?.type === 'bytes'\n\t\t\t? base64ToArrayBuffer(serializedReq.body.data)\n\t\t\t: undefined,\n\t\tredirect: serializedReq.redirect\n\t})\n}\n\nfunction createEmailMessageRaw(raw) {\n\tif (typeof raw === 'string' || raw instanceof ReadableStream) {\n\t\treturn raw\n\t}\n\tif (raw instanceof ArrayBuffer || raw instanceof Uint8Array) {\n\t\treturn new Response(raw).body\n\t}\n\tthrow new Error('Unsupported EmailMessage raw payload')\n}\n\nfunction isDurableObjectNamespace(binding) {\n\treturn !!binding\n\t\t&& typeof binding.idFromName === 'function'\n\t\t&& typeof binding.idFromString === 'function'\n\t\t&& typeof binding.newUniqueId === 'function'\n}\n\n/**\n * Resolve a (possibly jurisdiction-scoped) DurableObjectNamespace.\n * Forwards the jurisdiction faithfully when the binding supports it\n * (real Cloudflare), and degrades to the plain binding otherwise\n * (miniflare/workerd local emulation, which does not pin jurisdictions).\n */\nfunction resolveDoNamespace(binding, jurisdiction) {\n\tif (jurisdiction && typeof binding.jurisdiction === 'function') {\n\t\treturn binding.jurisdiction(jurisdiction)\n\t}\n\treturn binding\n}\n\n/**\n * Execute an RPC method against the gateway's bindings.\n *\n * Method format: \"binding.operation\". Operations must be namespaced by\n * binding kind (e.g. \"kv.get\", \"r2.head\", \"d1.stmt.first\", \"do.fetch\",\n * \"service.fetch\", \"queue.send\", \"email.send\", \"ai.run\"). Bare verbs and the legacy\n * \"stmt.*\" / \"stub.*\" sub-prefixes were removed in B3-final and now throw.\n * Method vocabulary must stay in sync with the canonical server in\n * src/bridge/server.ts.\n */\nasync function executeRpcMethod(method, params, env, _ctx) {\n\tconst parts = method.split('.')\n\tif (parts.length < 2) throw new Error('Invalid method format: ' + method)\n\n\tconst bindingName = parts[0]\n\tconst operation = parts.slice(1).join('.')\n\tconst binding = env[bindingName]\n\n\tif (!binding) throw new Error('Binding not found: ' + bindingName)\n\n\tconst isNamespaced =\n\t\toperation.indexOf('kv.') === 0 ||\n\t\toperation.indexOf('r2.') === 0 ||\n\t\toperation.indexOf('d1.') === 0 ||\n\t\toperation.indexOf('do.') === 0 ||\n\t\toperation.indexOf('service.') === 0 ||\n\t\toperation.indexOf('queue.') === 0 ||\n\t\toperation.indexOf('email.') === 0 ||\n\t\toperation.indexOf('ai.') === 0 ||\n\t\toperation.indexOf('workflow.') === 0 ||\n\t\toperation.indexOf('var.') === 0\n\tif (!isNamespaced) {\n\t\tthrow new Error(createUnsupportedBridgeOperationErrorMessage(bindingName, operation))\n\t}\n\n\t// KV\n\tif (operation === 'kv.get') return binding.get(params[0], params[1])\n\tif (operation === 'kv.put') return binding.put(params[0], params[1], params[2])\n\tif (operation === 'kv.delete') return binding.delete(params[0])\n\tif (operation === 'kv.list') return binding.list(params[0])\n\tif (operation === 'kv.getWithMetadata') return binding.getWithMetadata(params[0], params[1])\n\n\t// DO get (returns DOStub reference)\n\tif (operation === 'do.get') {\n\t\treturn { __type: 'DOStub', binding: bindingName, id: params[0] }\n\t}\n\n\t// R2\n\tif (operation === 'r2.head') return serializeR2Object(await binding.head(params[0]))\n\tif (operation === 'r2.get') {\n\t\tconst obj = await binding.get(params[0], params[1])\n\t\tif (!obj) return null\n\t\tconst body = await obj.arrayBuffer()\n\t\treturn serializeR2ObjectBody(obj, arrayBufferToBase64(body))\n\t}\n\tif (operation === 'r2.put') {\n\t\tlet value = params[1]\n\t\tif (value && typeof value === 'object') {\n\t\t\tif (value.__type === 'ArrayBuffer' || value.__type === 'Uint8Array') {\n\t\t\t\tvalue = base64ToArrayBuffer(value.data)\n\t\t\t}\n\t\t}\n\t\treturn serializeR2Object(await binding.put(params[0], value, params[2]))\n\t}\n\tif (operation === 'r2.delete') return binding.delete(params[0])\n\tif (operation === 'r2.list') return serializeR2Objects(await binding.list(params[0]))\n\n\t// D1\n\tif (operation === 'd1.exec') return binding.exec(params[0])\n\tif (operation === 'd1.batch') {\n\t\tconst statements = params[0].map((s) => binding.prepare(s.sql).bind(...(s.bindings || [])))\n\t\treturn binding.batch(statements)\n\t}\n\tif (operation.indexOf('d1.stmt.') === 0) {\n\t\tconst mode = operation.split('.')[2]\n\t\tconst [sql, ...rest] = params\n\t\tlet bindings = rest\n\t\tlet extraParam\n\t\tif (mode === 'first' || mode === 'raw') {\n\t\t\textraParam = rest[rest.length - 1]\n\t\t\tbindings = rest.slice(0, -1)\n\t\t}\n\t\tlet stmt = binding.prepare(sql)\n\t\tif (bindings.length > 0) stmt = stmt.bind(...bindings)\n\t\tif (mode === 'first') {\n\t\t\tif (typeof extraParam === 'string' && extraParam.length > 0) return stmt.first(extraParam)\n\t\t\treturn stmt.first()\n\t\t}\n\t\tif (mode === 'all') return stmt.all()\n\t\tif (mode === 'run') return stmt.run()\n\t\tif (mode === 'raw') return stmt.raw(extraParam)\n\t\tthrow new Error('Unknown stmt mode: ' + mode)\n\t}\n\n\t// Durable Objects\n\tif (operation === 'do.idFromName') {\n\t\tconst ns = resolveDoNamespace(binding, params[1])\n\t\tconst id = ns.idFromName(params[0])\n\t\treturn { __type: 'DOId', hex: id.toString() }\n\t}\n\tif (operation === 'do.idFromString') {\n\t\tconst id = binding.idFromString(params[0])\n\t\treturn { __type: 'DOId', hex: id.toString() }\n\t}\n\tif (operation === 'do.newUniqueId') {\n\t\tconst ns = resolveDoNamespace(binding, params[1])\n\t\tconst id = ns.newUniqueId(params[0])\n\t\treturn { __type: 'DOId', hex: id.toString() }\n\t}\n\tif (operation === 'do.fetch') {\n\t\tconst [, serializedId, serializedReq] = params\n\t\tconst id = binding.idFromString(serializedId.hex)\n\t\tconst stub = binding.get(id)\n\t\tconst response = await stub.fetch(new Request(serializedReq.url, {\n\t\t\tmethod: serializedReq.method,\n\t\t\theaders: serializedReq.headers,\n\t\t\tbody: serializedReq.body?.type === 'bytes'\n\t\t\t\t? base64ToArrayBuffer(serializedReq.body.data)\n\t\t\t\t: undefined\n\t\t}))\n\t\treturn serializeResponse(response)\n\t}\n\tif (operation === 'do.rpc') {\n\t\tconst [, serializedId, methodName, args] = params\n\t\tconst id = binding.idFromString(serializedId.hex)\n\t\tconst stub = binding.get(id)\n\t\tconst response = await stub.fetch(new Request('http://do/_rpc', {\n\t\t\tmethod: 'POST',\n\t\t\theaders: { 'Content-Type': 'application/json' },\n\t\t\tbody: JSON.stringify({ method: methodName, params: args })\n\t\t}))\n\t\tconst result = await response.json()\n\t\tif (!result.ok) throw new Error(result.error?.message || 'RPC failed')\n\t\treturn result.result\n\t}\n\n\t// Service Bindings\n\tif (operation === 'service.fetch') {\n\t\tif (!binding || typeof binding.fetch !== 'function') {\n\t\t\tthrow new Error('Service binding ' + bindingName + ' does not support fetch()')\n\t\t}\n\t\tconst response = await binding.fetch(deserializeRequest(params[0]))\n\t\treturn serializeResponse(response)\n\t}\n\tif (operation === 'service.rpc') {\n\t\tconst methodName = params[0]\n\t\tif (typeof methodName !== 'string') {\n\t\t\tthrow new Error('Service binding ' + bindingName + ' RPC method name must be a string')\n\t\t}\n\t\tconst args = Array.isArray(params[1]) ? params[1] : []\n\t\tconst method = binding && binding[methodName]\n\t\tif (typeof method !== 'function') {\n\t\t\tthrow new Error('Service binding ' + bindingName + ' does not support ' + methodName + '()')\n\t\t}\n\t\treturn method.apply(binding, args)\n\t}\n\n\t// Queues\n\tif (operation === 'queue.send') return binding.send(params[0], params[1])\n\tif (operation === 'queue.sendBatch') return binding.sendBatch(params[0], params[1])\n\n\t// Send Email\n\tif (operation === 'email.send') {\n\t\tif (binding && typeof binding.send === 'function') {\n\t\t\tconst message = params[0]\n\t\t\tif (message && typeof message === 'object' && 'from' in message && 'to' in message && 'raw' in message) {\n\t\t\t\treturn binding.send({\n\t\t\t\t\tfrom: message.from,\n\t\t\t\t\tto: message.to,\n\t\t\t\t\t[RAW_EMAIL]: createEmailMessageRaw(message.raw)\n\t\t\t\t})\n\t\t\t}\n\t\t\treturn binding.send(message)\n\t\t}\n\t\treturn { ok: true, simulated: true }\n\t}\n\n\t// Workflows\n\tif (operation === 'workflow.create') {\n\t\treturn serializeWorkflowInstance(await binding.create(params[0]))\n\t}\n\tif (operation === 'workflow.get') {\n\t\treturn serializeWorkflowInstance(await binding.get(params[0]))\n\t}\n\tif (operation === 'workflow.status') {\n\t\treturn (await binding.get(params[0])).status()\n\t}\n\tif (operation === 'workflow.pause') {\n\t\treturn (await binding.get(params[0])).pause()\n\t}\n\tif (operation === 'workflow.resume') {\n\t\treturn (await binding.get(params[0])).resume()\n\t}\n\tif (operation === 'workflow.terminate') {\n\t\treturn (await binding.get(params[0])).terminate()\n\t}\n\tif (operation === 'workflow.restart') {\n\t\treturn (await binding.get(params[0])).restart()\n\t}\n\tif (operation === 'workflow.sendEvent') {\n\t\treturn (await binding.get(params[0])).sendEvent(params[1])\n\t}\n\n\t// AI / generic run()\n\tif (operation === 'ai.run') {\n\t\tif (typeof binding.run !== 'function') {\n\t\t\tthrow new Error('Binding ' + bindingName + ' does not support run(): ' + method)\n\t\t}\n\t\treturn binding.run(params[0], params[1])\n\t}\n\n\tthrow new Error('Unknown operation: ' + method)\n}\n\nfunction createUnsupportedBridgeOperationErrorMessage(bindingName, operation) {\n\tconst base = \"[devflare][bridge] Unsupported bridge operation '\" + operation + \"' for binding '\" + bindingName + \"'.\"\n\tif (operation === 'fetch') {\n\t\treturn base + ' Devflare could not dispatch fetch() for this binding through the local bridge. '\n\t\t\t+ 'Expected Cloudflare API: env.' + bindingName + '.fetch(request). '\n\t\t\t+ 'If this came from SvelteKit platform.env, make sure the binding is declared as a service binding; '\n\t\t\t+ 'this is a Devflare local bridge issue when service bindings fall back to a bare fetch operation.'\n\t}\n\tif (operation === 'toString') {\n\t\treturn base + ' A platform.env value was coerced to a string through the bridge. '\n\t\t\t+ 'For SvelteKit local dev, declared vars should be plain string values and missing env names should read as undefined.'\n\t}\n\treturn base + ' Bare verbs and the legacy stmt.*/stub.* sub-prefixes are not supported; '\n\t\t+ 'use the namespaced form (e.g. kv.get, r2.put, d1.stmt.first, do.fetch, service.fetch).'\n}\n\nfunction serializeWorkflowInstance(instance) {\n\treturn {\n\t\t__type: 'WorkflowInstance',\n\t\tid: instance.id\n\t}\n}\n\n// ---------------------------------------------------------------------------\n// WebSocket bridge (shared with src/bridge/server.ts in shape)\n// ---------------------------------------------------------------------------\n// NOTE: wsProxies is intentionally created per handleBridgeWebSocket call so\n// state never leaks across connections or across gateway-script regenerations.\n\nasync function handleBridgeRpcCall(msg, ws, env, ctx) {\n\ttry {\n\t\tconst result = await executeRpcMethod(msg.method, msg.params, env, ctx)\n\t\tws.send(JSON.stringify({ t: 'rpc.ok', id: msg.id, result }))\n\t} catch (error) {\n\t\tws.send(JSON.stringify({\n\t\t\tt: 'rpc.err',\n\t\t\tid: msg.id,\n\t\t\terror: {\n\t\t\t\tcode: error?.code || 'INTERNAL_ERROR',\n\t\t\t\tmessage: error?.message || String(error)\n\t\t\t}\n\t\t}))\n\t}\n}\n\nasync function handleBridgeWsOpen(msg, ws, env, wsProxies) {\n\ttry {\n\t\tconst binding = env[msg.target.binding]\n\t\tconst id = binding.idFromString(msg.target.id)\n\t\tconst stub = binding.get(id)\n\n\t\tconst headers = new Headers(msg.target.headers || [])\n\t\theaders.set('Upgrade', 'websocket')\n\n\t\tconst response = await stub.fetch(new Request(msg.target.url, { method: 'GET', headers }))\n\t\tconst doWs = response.webSocket\n\n\t\tif (!doWs) {\n\t\t\tws.send(JSON.stringify({\n\t\t\t\tt: 'rpc.err',\n\t\t\t\tid: 'ws_' + msg.wid,\n\t\t\t\terror: { code: 'WS_FAILED', message: 'No WebSocket returned' }\n\t\t\t}))\n\t\t\treturn\n\t\t}\n\n\t\tdoWs.accept()\n\t\twsProxies.set(msg.wid, { doWs })\n\n\t\tdoWs.addEventListener('message', (event) => {\n\t\t\tconst isText = typeof event.data === 'string'\n\t\t\tconst payload = isText\n\t\t\t\t? new TextEncoder().encode(event.data)\n\t\t\t\t: new Uint8Array(event.data)\n\t\t\tconst flags = isText ? 2 : 0\n\t\t\tws.send(encodeWsDataFrame(msg.wid, flags, payload))\n\t\t})\n\n\t\tdoWs.addEventListener('close', (event) => {\n\t\t\tws.send(JSON.stringify({ t: 'ws.close', wid: msg.wid, code: event.code, reason: event.reason }))\n\t\t\twsProxies.delete(msg.wid)\n\t\t})\n\n\t\tws.send(JSON.stringify({ t: 'ws.opened', wid: msg.wid }))\n\t} catch (error) {\n\t\tws.send(JSON.stringify({\n\t\t\tt: 'rpc.err',\n\t\t\tid: 'ws_' + msg.wid,\n\t\t\terror: { code: 'WS_FAILED', message: error.message }\n\t\t}))\n\t}\n}\n\nfunction handleBridgeWsClose(msg, wsProxies) {\n\tconst proxy = wsProxies.get(msg.wid)\n\tif (proxy) {\n\t\tproxy.doWs.close(msg.code, msg.reason)\n\t\twsProxies.delete(msg.wid)\n\t}\n}\n\n// Relay an inbound binary WsData frame (client -> DO). Mirrors server.ts\n// handleBinaryMessage: decode the frame, look up the proxy by ws id, and forward\n// the payload to the DO socket honoring the TEXT flag.\nfunction handleBridgeBinaryMessage(buffer, wsProxies) {\n\tconst frame = decodeWsDataFrame(buffer)\n\tif (!frame || frame.kind !== 2) return\n\tconst proxy = wsProxies.get(frame.id)\n\tif (!proxy) return\n\tif ((frame.flags & 2) !== 0) {\n\t\tproxy.doWs.send(new TextDecoder().decode(frame.payload))\n\t} else {\n\t\tproxy.doWs.send(frame.payload)\n\t}\n}\n\nasync function handleBridgeJsonMessage(data, ws, env, ctx, wsProxies) {\n\tconst msg = JSON.parse(data)\n\tswitch (msg.t) {\n\t\tcase 'hello':\n\t\t\t// v2 handshake \u2014 acknowledge with welcome echoing the negotiated\n\t\t\t// capability intersection. The gateway advertises only what it\n\t\t\t// implements end-to-end: 'ws-relay' (binary WsData relay to/from DO\n\t\t\t// sockets) and 'http-transfer' (the R2 transfer HTTP side-channel).\n\t\t\t// 'streams' is intentionally NOT advertised \u2014 large DO/service-fetch\n\t\t\t// responses are inlined, not streamed, in this gateway.\n\t\t\tws.send(JSON.stringify({\n\t\t\t\tt: 'welcome',\n\t\t\t\tprotocolVersion: 2,\n\t\t\t\tcapabilities: ['ws-relay', 'http-transfer']\n\t\t\t\t\t.filter((c) => Array.isArray(msg.capabilities) && msg.capabilities.includes(c))\n\t\t\t\t\t.sort()\n\t\t\t}))\n\t\t\tbreak\n\t\tcase 'rpc.call':\n\t\t\tawait handleBridgeRpcCall(msg, ws, env, ctx)\n\t\t\tbreak\n\t\tcase 'ws.open':\n\t\t\tawait handleBridgeWsOpen(msg, ws, env, wsProxies)\n\t\t\tbreak\n\t\tcase 'ws.close':\n\t\t\thandleBridgeWsClose(msg, wsProxies)\n\t\t\tbreak\n\t}\n}\n\nfunction handleBridgeWebSocket(request, env, ctx) {\n\tconst { 0: client, 1: server } = new WebSocketPair()\n\tserver.accept()\n\n\t// Per-connection state: recreated for every bridge client so reloads and\n\t// concurrent clients never share WS proxy entries.\n\tconst wsProxies = new Map()\n\n\tserver.addEventListener('message', async (event) => {\n\t\ttry {\n\t\t\tif (typeof event.data === 'string') {\n\t\t\t\tawait handleBridgeJsonMessage(event.data, server, env, ctx, wsProxies)\n\t\t\t} else {\n\t\t\t\thandleBridgeBinaryMessage(event.data, wsProxies)\n\t\t\t}\n\t\t} catch (error) {\n\t\t\tconsole.error('[Gateway] Error:', error)\n\t\t}\n\t})\n\n\tserver.addEventListener('close', () => {\n\t\tfor (const proxy of wsProxies.values()) {\n\t\t\t// Best-effort cleanup: the DO-side WS may already be closed or in an\n\t\t\t// invalid state; any throw here would abort sibling closes. Surface\n\t\t\t// the swallowed error when DEVFLARE_DEBUG_BRIDGE is enabled.\n\t\t\ttry { proxy.doWs.close() } catch (error) {\n\t\t\t\tif (globalThis.DEVFLARE_DEBUG_BRIDGE) {\n\t\t\t\t\tconsole.warn('[devflare:bridge] proxy.doWs.close() failed', error)\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t\twsProxies.clear()\n\t})\n\n\treturn new Response(null, { status: 101, webSocket: client })\n}\n\n// ---------------------------------------------------------------------------\n// HTTP transfer for R2 bodies (shared with src/bridge/server.ts in shape)\n// ---------------------------------------------------------------------------\n\nasync function handleHttpTransfer(request, env, url) {\n\tconst transferIdEncoded = url.pathname.split('/').pop()\n\tconst transferId = decodeURIComponent(transferIdEncoded || '')\n\tconst [binding, ...keyParts] = transferId.split(':')\n\tconst key = keyParts.join(':')\n\tconst bucket = env[binding]\n\n\tif (!bucket) return new Response('Bucket not found: ' + binding, { status: 404 })\n\n\tif (request.method === 'PUT' || request.method === 'POST') {\n\t\tconst result = await bucket.put(key, request.body)\n\t\treturn new Response(JSON.stringify(serializeR2Object(result)), {\n\t\t\theaders: { 'Content-Type': 'application/json' }\n\t\t})\n\t}\n\n\tif (request.method === 'GET') {\n\t\tconst object = await bucket.get(key)\n\t\tif (!object) return new Response('Not found', { status: 404 })\n\t\treturn new Response(object.body, {\n\t\t\theaders: {\n\t\t\t\t'Content-Type': object.httpMetadata?.contentType || 'application/octet-stream',\n\t\t\t\t'Content-Length': String(object.size)\n\t\t\t}\n\t\t})\n\t}\n\n\treturn new Response('Method not allowed', { status: 405 })\n}\n";
|
|
8
8
|
//# sourceMappingURL=gateway-runtime.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gateway-runtime.d.ts","sourceRoot":"","sources":["../../src/bridge/gateway-runtime.ts"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,
|
|
1
|
+
{"version":3,"file":"gateway-runtime.d.ts","sourceRoot":"","sources":["../../src/bridge/gateway-runtime.ts"],"names":[],"mappings":"AAcA;;;;;GAKG;AACH,eAAO,MAAM,kBAAkB,+1pBAilB9B,CAAA"}
|
|
@@ -31,11 +31,6 @@ export interface TransportV2WsCloseMsg {
|
|
|
31
31
|
code?: number;
|
|
32
32
|
reason?: string;
|
|
33
33
|
}
|
|
34
|
-
export interface TransportV2EventMsg {
|
|
35
|
-
t: 'event';
|
|
36
|
-
channel: string;
|
|
37
|
-
payload: unknown;
|
|
38
|
-
}
|
|
39
34
|
export interface TransportV2HttpTransferMsg {
|
|
40
35
|
t: 'http.transfer';
|
|
41
36
|
id: string;
|
|
@@ -44,7 +39,7 @@ export interface TransportV2HttpTransferMsg {
|
|
|
44
39
|
headers?: Record<string, string>;
|
|
45
40
|
bytes: number;
|
|
46
41
|
}
|
|
47
|
-
export type TransportV2AuxMsg = TransportV2WsOpenMsg | TransportV2WsOpenedMsg | TransportV2WsOpenErrMsg | TransportV2WsTextMsg | TransportV2WsCloseMsg |
|
|
42
|
+
export type TransportV2AuxMsg = TransportV2WsOpenMsg | TransportV2WsOpenedMsg | TransportV2WsOpenErrMsg | TransportV2WsTextMsg | TransportV2WsCloseMsg | TransportV2HttpTransferMsg;
|
|
48
43
|
export declare function isTransportV2AuxMsg(value: unknown): value is TransportV2AuxMsg;
|
|
49
44
|
export declare function parseTransportV2AuxMsg(text: string): TransportV2AuxMsg | null;
|
|
50
45
|
export declare function stringifyTransportV2AuxMsg(msg: TransportV2AuxMsg): string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"control-messages.d.ts","sourceRoot":"","sources":["../../../src/bridge/v2/control-messages.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"control-messages.d.ts","sourceRoot":"","sources":["../../../src/bridge/v2/control-messages.ts"],"names":[],"mappings":"AAUA,MAAM,WAAW,oBAAoB;IACpC,CAAC,EAAE,SAAS,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,OAAO,EAAE,MAAM,CAAA;IACf,IAAI,EAAE,MAAM,CAAA;IACZ,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAC/B,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,sBAAsB;IACtC,CAAC,EAAE,WAAW,CAAA;IACd,EAAE,EAAE,MAAM,CAAA;IACV,WAAW,CAAC,EAAE,MAAM,CAAA;CACpB;AAED,MAAM,WAAW,uBAAuB;IACvC,CAAC,EAAE,YAAY,CAAA;IACf,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,IAAI,CAAC,EAAE,MAAM,CAAA;KAAE,CAAA;CACzC;AAED,MAAM,WAAW,oBAAoB;IACpC,CAAC,EAAE,SAAS,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,EAAE,MAAM,CAAA;CACZ;AAED,MAAM,WAAW,qBAAqB;IACrC,CAAC,EAAE,UAAU,CAAA;IACb,EAAE,EAAE,MAAM,CAAA;IACV,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CACf;AAED,MAAM,WAAW,0BAA0B;IAC1C,CAAC,EAAE,eAAe,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE,KAAK,GAAG,KAAK,CAAA;IACrB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAChC,KAAK,EAAE,MAAM,CAAA;CACb;AAED,MAAM,MAAM,iBAAiB,GAC1B,oBAAoB,GACpB,sBAAsB,GACtB,uBAAuB,GACvB,oBAAoB,GACpB,qBAAqB,GACrB,0BAA0B,CAAA;AAW7B,wBAAgB,mBAAmB,CAAC,KAAK,EAAE,OAAO,GAAG,KAAK,IAAI,iBAAiB,CAI9E;AAED,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,iBAAiB,GAAG,IAAI,CAO7E;AAED,wBAAgB,0BAA0B,CAAC,GAAG,EAAE,iBAAiB,GAAG,MAAM,CAEzE"}
|
package/dist/bridge/v2/wire.d.ts
CHANGED
|
@@ -163,8 +163,8 @@ export declare function resetIdCounters(): void;
|
|
|
163
163
|
export declare const DEFAULT_CHUNK_SIZE: number;
|
|
164
164
|
/** Threshold for switching to HTTP transfer (512 KB — workerd ~1MB WS message limit). */
|
|
165
165
|
export declare const HTTP_TRANSFER_THRESHOLD: number;
|
|
166
|
-
/** Default WebSocket port for bridge */
|
|
167
|
-
export declare const DEFAULT_BRIDGE_PORT =
|
|
168
|
-
/** Default HTTP port for large transfers */
|
|
169
|
-
export declare const DEFAULT_HTTP_PORT =
|
|
166
|
+
/** Default WebSocket port for the bridge (matches the dev runtime port). */
|
|
167
|
+
export declare const DEFAULT_BRIDGE_PORT = 8787;
|
|
168
|
+
/** Default HTTP port for large transfers (matches the dev runtime HTTP port). */
|
|
169
|
+
export declare const DEFAULT_HTTP_PORT = 8788;
|
|
170
170
|
//# sourceMappingURL=wire.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../../src/bridge/v2/wire.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"wire.d.ts","sourceRoot":"","sources":["../../../src/bridge/v2/wire.ts"],"names":[],"mappings":"AAyBA,qCAAqC;AACrC,MAAM,WAAW,OAAO;IACvB,CAAC,EAAE,UAAU,CAAA;IACb,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,MAAM,CAAA;IACd,MAAM,EAAE,OAAO,EAAE,CAAA;CACjB;AAED,8BAA8B;AAC9B,MAAM,WAAW,KAAK;IACrB,CAAC,EAAE,QAAQ,CAAA;IACX,EAAE,EAAE,MAAM,CAAA;IACV,MAAM,EAAE,OAAO,CAAA;CACf;AAED,yBAAyB;AACzB,MAAM,WAAW,MAAM;IACtB,CAAC,EAAE,SAAS,CAAA;IACZ,EAAE,EAAE,MAAM,CAAA;IACV,KAAK,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,OAAO,CAAA;KACjB,CAAA;CACD;AAED;;;;;;;;;;;;;GAaG;AACH,MAAM,WAAW,SAAS;IACzB,CAAC,EAAE,OAAO,CAAA;IACV,KAAK,EAAE,WAAW,GAAG,KAAK,GAAG,QAAQ,GAAG,IAAI,CAAA;IAC5C,KAAK,EAAE;QACN,IAAI,EAAE,MAAM,CAAA;QACZ,OAAO,EAAE,MAAM,CAAA;QACf,OAAO,CAAC,EAAE,OAAO,CAAA;KACjB,CAAA;IACD,KAAK,CAAC,EAAE,MAAM,GAAG,MAAM,CAAA;CACvB;AAED,2CAA2C;AAC3C,MAAM,WAAW,QAAQ;IACxB,CAAC,EAAE,OAAO,CAAA;IACV,KAAK,EAAE,MAAM,CAAA;IACb,IAAI,EAAE,OAAO,CAAA;CACb;AAED,wBAAwB;AACxB,MAAM,WAAW,UAAU;IAC1B,CAAC,EAAE,aAAa,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE;QACN,WAAW,CAAC,EAAE,MAAM,CAAA;QACpB,MAAM,CAAC,EAAE,MAAM,CAAA;KACf,CAAA;CACD;AAED,0DAA0D;AAC1D,MAAM,WAAW,UAAU;IAC1B,CAAC,EAAE,aAAa,CAAA;IAChB,GAAG,EAAE,MAAM,CAAA;IACX,WAAW,EAAE,MAAM,CAAA;CACnB;AAED,oCAAoC;AACpC,MAAM,WAAW,SAAS;IACzB,CAAC,EAAE,YAAY,CAAA;IACf,GAAG,EAAE,MAAM,CAAA;CACX;AAED,gCAAgC;AAChC,MAAM,WAAW,WAAW;IAC3B,CAAC,EAAE,cAAc,CAAA;IACjB,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,CAAC,EAAE,MAAM,CAAA;CACd;AAED,sCAAsC;AACtC,MAAM,WAAW,MAAM;IACtB,CAAC,EAAE,SAAS,CAAA;IACZ,GAAG,EAAE,MAAM,CAAA;IACX,MAAM,EAAE;QACP,OAAO,EAAE,MAAM,CAAA;QACf,EAAE,EAAE,MAAM,CAAA;QACV,GAAG,EAAE,MAAM,CAAA;QACX,OAAO,CAAC,EAAE,CAAC,MAAM,EAAE,MAAM,CAAC,EAAE,CAAA;KAC5B,CAAA;CACD;AAED,0CAA0C;AAC1C,MAAM,WAAW,QAAQ;IACxB,CAAC,EAAE,WAAW,CAAA;IACd,GAAG,EAAE,MAAM,CAAA;CACX;AAED,4BAA4B;AAC5B,MAAM,WAAW,OAAO;IACvB,CAAC,EAAE,UAAU,CAAA;IACb,GAAG,EAAE,MAAM,CAAA;IACX,IAAI,CAAC,EAAE,MAAM,CAAA;IACb,MAAM,CAAC,EAAE,MAAM,CAAA;CACf;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC5B,CAAC,EAAE,eAAe,CAAA;IAClB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,SAAS,EAAE,QAAQ,GAAG,UAAU,CAAA;CAChC;AAED,sCAAsC;AACtC,MAAM,MAAM,OAAO,GAChB,OAAO,GACP,KAAK,GACL,MAAM,GACN,SAAS,GACT,QAAQ,GACR,UAAU,GACV,UAAU,GACV,SAAS,GACT,WAAW,GACX,MAAM,GACN,QAAQ,GACR,OAAO,GACP,YAAY,CAAA;AAMf,yBAAyB;AACzB,eAAO,MAAM,UAAU;aACtB,WAAW,EAAE,CAAC;aACd,MAAM,EAAE,CAAC;CACA,CAAA;AAEV,MAAM,MAAM,UAAU,GAAG,CAAC,OAAO,UAAU,CAAC,CAAC,MAAM,OAAO,UAAU,CAAC,CAAA;AAErE,yBAAyB;AACzB,eAAO,MAAM,WAAW;aACvB,GAAG,EAAE,CAAM;aACX,IAAI,EAAE,CAAM;CACH,CAAA;AAEV;;;;;;;;;GASG;AACH,eAAO,MAAM,kBAAkB,KAAK,CAAA;AAEpC,4BAA4B;AAC5B,wBAAgB,iBAAiB,CAChC,IAAI,EAAE,UAAU,EAChB,EAAE,EAAE,MAAM,EACV,GAAG,EAAE,MAAM,EACX,KAAK,EAAE,MAAM,EACb,OAAO,EAAE,UAAU,GACjB,UAAU,CAYZ;AAED,2BAA2B;AAC3B,MAAM,WAAW,kBAAkB;IAClC,IAAI,EAAE,UAAU,CAAA;IAChB,EAAE,EAAE,MAAM,CAAA;IACV,GAAG,EAAE,MAAM,CAAA;IACX,KAAK,EAAE,MAAM,CAAA;IACb,OAAO,EAAE,UAAU,CAAA;CACnB;AAED,4BAA4B;AAC5B,wBAAgB,iBAAiB,CAAC,KAAK,EAAE,UAAU,GAAG,kBAAkB,CAcvE;AAED,+BAA+B;AAC/B,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE5C;AAED,4CAA4C;AAC5C,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAE7C;AAMD,uCAAuC;AACvC,wBAAgB,YAAY,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CASlD;AAED,+BAA+B;AAC/B,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,OAAO,GAAG,MAAM,CAErD;AAUD,kCAAkC;AAClC,wBAAgB,SAAS,IAAI,MAAM,CAElC;AAED,gCAAgC;AAChC,wBAAgB,YAAY,IAAI,MAAM,CAErC;AAED,yCAAyC;AACzC,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED,sCAAsC;AACtC,wBAAgB,eAAe,IAAI,IAAI,CAItC;AAMD,gDAAgD;AAChD,eAAO,MAAM,kBAAkB,QAAa,CAAA;AAE5C,yFAAyF;AACzF,eAAO,MAAM,uBAAuB,QAAa,CAAA;AAEjD,4EAA4E;AAC5E,eAAO,MAAM,mBAAmB,OAAO,CAAA;AAEvC,iFAAiF;AACjF,eAAO,MAAM,iBAAiB,OAAO,CAAA"}
|
package/dist/browser.js
CHANGED
|
@@ -704,7 +704,7 @@ function nextWsId() {
|
|
|
704
704
|
}
|
|
705
705
|
var DEFAULT_CHUNK_SIZE = 256 * 1024;
|
|
706
706
|
var HTTP_TRANSFER_THRESHOLD = 512 * 1024;
|
|
707
|
-
var DEFAULT_BRIDGE_PORT =
|
|
707
|
+
var DEFAULT_BRIDGE_PORT = 8787;
|
|
708
708
|
|
|
709
709
|
// src/bridge/v2/value-serialization.ts
|
|
710
710
|
async function serializeRequest(request, options) {
|
package/dist/cli/index.js
CHANGED
|
@@ -1315,7 +1315,7 @@ async function runInit(parsed, logger, options) {
|
|
|
1315
1315
|
return runInitCommand(parsed, logger, options);
|
|
1316
1316
|
}
|
|
1317
1317
|
async function runDev(parsed, logger, options) {
|
|
1318
|
-
const { runDevCommand } = await import("../dev-
|
|
1318
|
+
const { runDevCommand } = await import("../dev-xz2qxw0m.js");
|
|
1319
1319
|
return runDevCommand(parsed, logger, options);
|
|
1320
1320
|
}
|
|
1321
1321
|
async function runBuild(parsed, logger, options) {
|
|
@@ -665,6 +665,12 @@ function buildLocalBindingShimServiceConfig(config) {
|
|
|
665
665
|
var GATEWAY_RUNTIME_JS = `
|
|
666
666
|
const RAW_EMAIL = 'EmailMessage::raw'
|
|
667
667
|
|
|
668
|
+
// Inline body cap for proxied DO/service-fetch responses. Matches
|
|
669
|
+
// HTTP_TRANSFER_THRESHOLD in wire.ts (512 KB). A larger body would exceed
|
|
670
|
+
// workerd's ~1 MB WebSocket message limit once base64-encoded into the rpc.ok
|
|
671
|
+
// frame, so it is rejected with a clear error rather than silently truncated.
|
|
672
|
+
const HTTP_TRANSFER_THRESHOLD = 512 * 1024
|
|
673
|
+
|
|
668
674
|
function arrayBufferToBase64(buffer) {
|
|
669
675
|
const bytes = new Uint8Array(buffer)
|
|
670
676
|
let binary = ''
|
|
@@ -679,6 +685,33 @@ function base64ToArrayBuffer(base64) {
|
|
|
679
685
|
return bytes.buffer
|
|
680
686
|
}
|
|
681
687
|
|
|
688
|
+
// Binary WsData frame codec (10-byte header: kind:u8, id:u32 LE, seq:u32 LE,
|
|
689
|
+
// flags:u8 + payload). MUST match wire.ts encodeBinaryFrame/decodeBinaryFrame so
|
|
690
|
+
// the bridge client and this gateway agree on the WS-data wire format
|
|
691
|
+
// (BinaryKind.WsData = 2, BinaryFlags.TEXT = 2).
|
|
692
|
+
function encodeWsDataFrame(wid, flags, payload) {
|
|
693
|
+
const frame = new Uint8Array(10 + payload.byteLength)
|
|
694
|
+
const view = new DataView(frame.buffer)
|
|
695
|
+
view.setUint8(0, 2)
|
|
696
|
+
view.setUint32(1, wid, true)
|
|
697
|
+
view.setUint32(5, 0, true)
|
|
698
|
+
view.setUint8(9, flags)
|
|
699
|
+
frame.set(payload, 10)
|
|
700
|
+
return frame
|
|
701
|
+
}
|
|
702
|
+
|
|
703
|
+
function decodeWsDataFrame(buffer) {
|
|
704
|
+
const bytes = new Uint8Array(buffer)
|
|
705
|
+
if (bytes.byteLength < 10) return null
|
|
706
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength)
|
|
707
|
+
return {
|
|
708
|
+
kind: view.getUint8(0),
|
|
709
|
+
id: view.getUint32(1, true),
|
|
710
|
+
flags: view.getUint8(9),
|
|
711
|
+
payload: bytes.subarray(10)
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
|
|
682
715
|
function serializeR2Object(obj) {
|
|
683
716
|
if (!obj) return null
|
|
684
717
|
return {
|
|
@@ -730,6 +763,15 @@ async function serializeResponse(response) {
|
|
|
730
763
|
let body = null
|
|
731
764
|
if (response.body) {
|
|
732
765
|
const bytes = await response.arrayBuffer()
|
|
766
|
+
if (bytes.byteLength > HTTP_TRANSFER_THRESHOLD) {
|
|
767
|
+
throw new Error(
|
|
768
|
+
'[devflare][bridge] Response body (' + bytes.byteLength + ' bytes) exceeds the '
|
|
769
|
+
+ HTTP_TRANSFER_THRESHOLD + '-byte inline limit. DO and service-binding fetch responses '
|
|
770
|
+
+ 'are delivered inline over the bridge WebSocket and cannot be streamed locally; keep '
|
|
771
|
+
+ 'proxied response bodies under the limit (large R2 objects are exempt — read them through '
|
|
772
|
+
+ 'the R2 binding, which uses the HTTP transfer side-channel).'
|
|
773
|
+
)
|
|
774
|
+
}
|
|
733
775
|
if (bytes.byteLength > 0) {
|
|
734
776
|
body = { type: 'bytes', data: arrayBufferToBase64(bytes) }
|
|
735
777
|
}
|
|
@@ -1068,8 +1110,11 @@ async function handleBridgeWsOpen(msg, ws, env, wsProxies) {
|
|
|
1068
1110
|
|
|
1069
1111
|
doWs.addEventListener('message', (event) => {
|
|
1070
1112
|
const isText = typeof event.data === 'string'
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1113
|
+
const payload = isText
|
|
1114
|
+
? new TextEncoder().encode(event.data)
|
|
1115
|
+
: new Uint8Array(event.data)
|
|
1116
|
+
const flags = isText ? 2 : 0
|
|
1117
|
+
ws.send(encodeWsDataFrame(msg.wid, flags, payload))
|
|
1073
1118
|
})
|
|
1074
1119
|
|
|
1075
1120
|
doWs.addEventListener('close', (event) => {
|
|
@@ -1095,17 +1140,35 @@ function handleBridgeWsClose(msg, wsProxies) {
|
|
|
1095
1140
|
}
|
|
1096
1141
|
}
|
|
1097
1142
|
|
|
1143
|
+
// Relay an inbound binary WsData frame (client -> DO). Mirrors server.ts
|
|
1144
|
+
// handleBinaryMessage: decode the frame, look up the proxy by ws id, and forward
|
|
1145
|
+
// the payload to the DO socket honoring the TEXT flag.
|
|
1146
|
+
function handleBridgeBinaryMessage(buffer, wsProxies) {
|
|
1147
|
+
const frame = decodeWsDataFrame(buffer)
|
|
1148
|
+
if (!frame || frame.kind !== 2) return
|
|
1149
|
+
const proxy = wsProxies.get(frame.id)
|
|
1150
|
+
if (!proxy) return
|
|
1151
|
+
if ((frame.flags & 2) !== 0) {
|
|
1152
|
+
proxy.doWs.send(new TextDecoder().decode(frame.payload))
|
|
1153
|
+
} else {
|
|
1154
|
+
proxy.doWs.send(frame.payload)
|
|
1155
|
+
}
|
|
1156
|
+
}
|
|
1157
|
+
|
|
1098
1158
|
async function handleBridgeJsonMessage(data, ws, env, ctx, wsProxies) {
|
|
1099
1159
|
const msg = JSON.parse(data)
|
|
1100
1160
|
switch (msg.t) {
|
|
1101
1161
|
case 'hello':
|
|
1102
1162
|
// v2 handshake — acknowledge with welcome echoing the negotiated
|
|
1103
|
-
// capability intersection.
|
|
1104
|
-
//
|
|
1163
|
+
// capability intersection. The gateway advertises only what it
|
|
1164
|
+
// implements end-to-end: 'ws-relay' (binary WsData relay to/from DO
|
|
1165
|
+
// sockets) and 'http-transfer' (the R2 transfer HTTP side-channel).
|
|
1166
|
+
// 'streams' is intentionally NOT advertised — large DO/service-fetch
|
|
1167
|
+
// responses are inlined, not streamed, in this gateway.
|
|
1105
1168
|
ws.send(JSON.stringify({
|
|
1106
1169
|
t: 'welcome',
|
|
1107
1170
|
protocolVersion: 2,
|
|
1108
|
-
capabilities: ['
|
|
1171
|
+
capabilities: ['ws-relay', 'http-transfer']
|
|
1109
1172
|
.filter((c) => Array.isArray(msg.capabilities) && msg.capabilities.includes(c))
|
|
1110
1173
|
.sort()
|
|
1111
1174
|
}))
|
|
@@ -1134,6 +1197,8 @@ function handleBridgeWebSocket(request, env, ctx) {
|
|
|
1134
1197
|
try {
|
|
1135
1198
|
if (typeof event.data === 'string') {
|
|
1136
1199
|
await handleBridgeJsonMessage(event.data, server, env, ctx, wsProxies)
|
|
1200
|
+
} else {
|
|
1201
|
+
handleBridgeBinaryMessage(event.data, wsProxies)
|
|
1137
1202
|
}
|
|
1138
1203
|
} catch (error) {
|
|
1139
1204
|
console.error('[Gateway] Error:', error)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "devflare",
|
|
3
|
-
"version": "1.0.0-next.
|
|
3
|
+
"version": "1.0.0-next.37",
|
|
4
4
|
"description": "Devflare is a developer-first toolkit for Cloudflare Workers that sits on top of Miniflare and Wrangler-compatible config",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -83,7 +83,7 @@
|
|
|
83
83
|
"test:unit": "bun test tests/unit",
|
|
84
84
|
"test:coverage": "bun test tests/unit --coverage",
|
|
85
85
|
"test:integration:control": "bun test tests/integration/cli tests/integration/examples tests/integration/package-entry tests/integration/vite",
|
|
86
|
-
"test:integration:bridge": "bun test tests/integration/bridge/bridge-proxy.test.ts --parallel=1 --max-concurrency=1 && bun test tests/integration/bridge/case18-do.test.ts --parallel=1 --max-concurrency=1 && bun test tests/integration/bridge/durable-object.test.ts --parallel=1 --max-concurrency=1 && bun test tests/integration/bridge/miniflare.test.ts --parallel=1 --max-concurrency=1 && bun test tests/integration/bridge/r2-transfer.test.ts --parallel=1 --max-concurrency=1",
|
|
86
|
+
"test:integration:bridge": "bun test tests/integration/bridge/bridge-proxy.test.ts --parallel=1 --max-concurrency=1 && bun test tests/integration/bridge/case18-do.test.ts --parallel=1 --max-concurrency=1 && bun test tests/integration/bridge/durable-object.test.ts --parallel=1 --max-concurrency=1 && bun test tests/integration/bridge/do-websocket.test.ts --parallel=1 --max-concurrency=1 && bun test tests/integration/bridge/miniflare.test.ts --parallel=1 --max-concurrency=1 && bun test tests/integration/bridge/r2-transfer.test.ts --parallel=1 --max-concurrency=1",
|
|
87
87
|
"test:integration:test-context": "bun test tests/integration/test-context --parallel=1 --max-concurrency=1",
|
|
88
88
|
"test:integration:dev-server": "bun test tests/integration/dev-server --parallel=1 --max-concurrency=1",
|
|
89
89
|
"test:watch": "bun test --watch",
|