@nimbus-sh/worker 0.1.1 → 0.1.2
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/README.md +38 -3
- package/dist/_shared/session-id.d.ts +10 -8
- package/dist/_shared/session-id.d.ts.map +1 -1
- package/dist/_shared/session-id.js +13 -12
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -1
- package/dist/auth/types.d.ts +2 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/types.js +4 -1
- package/dist/git-bundle.generated.d.ts +3 -3
- package/dist/git-bundle.generated.d.ts.map +1 -1
- package/dist/git-bundle.generated.js +4 -4
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/loaders/generated-workers.d.ts +3 -3
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +7 -7
- package/dist/router/index.d.ts +6 -2
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +4 -0
- package/dist/router/remote-api.d.ts +44 -0
- package/dist/router/remote-api.d.ts.map +1 -0
- package/dist/router/remote-api.js +333 -0
- package/package.json +2 -1
package/dist/index.d.ts
CHANGED
|
@@ -25,7 +25,7 @@ import { NimbusSession, NimbusAssetsRPC, NimbusLoaderRPC, NimbusLoadedWorker, Ni
|
|
|
25
25
|
import { SupervisorRPC } from './session/supervisor-rpc.js';
|
|
26
26
|
import { CirrusHmrRPC } from './facets/real-vite-hmr.js';
|
|
27
27
|
export { createNimbusHandler } from './router/index.js';
|
|
28
|
-
export type { NimbusHandler, NimbusHooks, NimbusHookContext, CustomRoutes, CreateNimbusHandlerOptions, AuthMode, NimbusAuthConfig, } from './router/index.js';
|
|
28
|
+
export type { NimbusHandler, NimbusHooks, NimbusHookContext, CustomRoutes, CreateNimbusHandlerOptions, AuthMode, NimbusAuthConfig, NimbusRemoteApiConfig, NimbusRuntimePolicy, NimbusSandboxProfile, NimbusSdkConfig, NimbusSdkRouterConfig, } from './router/index.js';
|
|
29
29
|
export { issueNimbusToken, verifyNimbusToken, NimbusAuthError, } from './auth/index.js';
|
|
30
30
|
export type { NimbusAuthEnv, NimbusTokenClaims, VerifiedNimbusToken, } from './auth/index.js';
|
|
31
31
|
export { NimbusSession, SupervisorRPC, NimbusAssetsRPC, NimbusLoaderRPC, NimbusLoadedWorker, NimbusLoadedEntrypoint, NimbusDurableObjectNamespace, NimbusDOStub, CirrusHmrRPC, };
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,aAAa,EACb,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,4BAA4B,EAC5B,YAAY,EACb,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAUzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,YAAY,EACV,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,0BAA0B,EAC1B,QAAQ,EACR,gBAAgB,
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EACL,aAAa,EACb,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,4BAA4B,EAC5B,YAAY,EACb,MAAM,6BAA6B,CAAC;AACrC,OAAO,EAAE,aAAa,EAAE,MAAM,6BAA6B,CAAC;AAC5D,OAAO,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAUzD,OAAO,EAAE,mBAAmB,EAAE,MAAM,mBAAmB,CAAC;AACxD,YAAY,EACV,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,YAAY,EACZ,0BAA0B,EAC1B,QAAQ,EACR,gBAAgB,EAChB,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,eAAe,EACf,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAI3B,OAAO,EACL,gBAAgB,EAChB,iBAAiB,EACjB,eAAe,GAChB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,iBAAiB,CAAC;AAmBzB,OAAO,EACL,aAAa,EACb,aAAa,EACb,eAAe,EACf,eAAe,EACf,kBAAkB,EAClB,sBAAsB,EACtB,4BAA4B,EAC5B,YAAY,EACZ,YAAY,GACb,CAAC;AAEF;;;;GAIG;AACH,wBAAgB,aAAa,IAAI,GAAG,CAEnC;;AAOD,wBAAqC"}
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
* W7-frame symbols: encodeWriteBatchStream, decodeWriteBatchStream,
|
|
16
16
|
* W7_MAGIC, W7_TRAILER.
|
|
17
17
|
*
|
|
18
|
-
* Generated at: 2026-06-
|
|
19
|
-
* Tar size: 3.
|
|
20
|
-
* W7 size: 7.
|
|
18
|
+
* Generated at: 2026-06-04T22:29:13.391Z
|
|
19
|
+
* Tar size: 3.86 KiB
|
|
20
|
+
* W7 size: 7.78 KiB
|
|
21
21
|
*/
|
|
22
22
|
export declare const TAR_STREAM_PREAMBLE: string;
|
|
23
23
|
export declare const TAR_STREAM_PREAMBLE_SIZE: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"generated-workers.d.ts","sourceRoot":"","sources":["../../src/loaders/generated-workers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,eAAO,MAAM,mBAAmB,EAAE,
|
|
1
|
+
{"version":3,"file":"generated-workers.d.ts","sourceRoot":"","sources":["../../src/loaders/generated-workers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,eAAO,MAAM,mBAAmB,EAAE,MAAyhI,CAAC;AAE5jI,eAAO,MAAM,wBAAwB,EAAE,MAAa,CAAC;AAErD,eAAO,MAAM,iBAAiB,EAAE,MAA+jQ,CAAC;AAEhmQ,eAAO,MAAM,sBAAsB,EAAE,MAAa,CAAC"}
|
|
@@ -15,11 +15,11 @@
|
|
|
15
15
|
* W7-frame symbols: encodeWriteBatchStream, decodeWriteBatchStream,
|
|
16
16
|
* W7_MAGIC, W7_TRAILER.
|
|
17
17
|
*
|
|
18
|
-
* Generated at: 2026-06-
|
|
19
|
-
* Tar size: 3.
|
|
20
|
-
* W7 size: 7.
|
|
18
|
+
* Generated at: 2026-06-04T22:29:13.391Z
|
|
19
|
+
* Tar size: 3.86 KiB
|
|
20
|
+
* W7 size: 7.78 KiB
|
|
21
21
|
*/
|
|
22
|
-
export const TAR_STREAM_PREAMBLE = "// src/npm/tarball-stream.ts\nvar MAX_FILE_BYTES = 2e7;\nfunction parseTarHeader(block) {\n if (block[0] === 0) return null;\n let name = \"\";\n for (let i = 0; i < 100 && block[i] !== 0; i++) {\n name += String.fromCharCode(block[i]);\n }\n let prefix = \"\";\n for (let i = 345; i < 500 && block[i] !== 0; i++) {\n prefix += String.fromCharCode(block[i]);\n }\n if (prefix) name = prefix + \"/\" + name;\n name = name.replace(/^package\\//, \"\");\n let sizeStr = \"\";\n for (let i = 124; i < 136 && block[i] !== 0; i++) {\n sizeStr += String.fromCharCode(block[i]);\n }\n const size = parseInt(sizeStr.trim(), 8) || 0;\n const typeFlag = block[156];\n return { name, size, typeFlag };\n}\nasync function* readableStreamToAsyncIterable(rs) {\n const reader = rs.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) return;\n if (value && value.length > 0) yield value;\n }\n } finally {\n try {\n reader.releaseLock();\n } catch {\n }\n }\n}\nasync function* streamTarEntries(source, onSkip) {\n let carry = new Uint8Array(0);\n let state = { kind: \"header\" };\n function concat(a, b) {\n if (a.length === 0) return b;\n if (b.length === 0) return a;\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n }\n for await (const chunkRaw of source) {\n let buf = concat(carry, chunkRaw);\n let cursor = 0;\n while (true) {\n if (state.kind === \"header\") {\n if (buf.length - cursor < 512) break;\n const header = buf.subarray(cursor, cursor + 512);\n const parsed = parseTarHeader(header);\n cursor += 512;\n if (!parsed) return;\n const { name, size, typeFlag } = parsed;\n const pad = size === 0 ? 0 : (512 - size % 512) % 512;\n const isRegularFile = typeFlag === 48 || typeFlag === 0;\n if (size === 0) {\n if (isRegularFile && name) {\n yield { name, data: new Uint8Array(0) };\n }\n state = { kind: \"header\" };\n continue;\n }\n if (!isRegularFile || !name || size > MAX_FILE_BYTES) {\n if (onSkip) {\n const reason = !isRegularFile ? \"non-regular\" : !name ? \"no-name\" : \"too-large\";\n try {\n onSkip(name, size, reason);\n } catch {\n }\n }\n state = { kind: \"skip\", remaining: size + pad };\n continue;\n }\n state = {\n kind: \"file\",\n name,\n remaining: size,\n fileBuf: new Uint8Array(size),\n fileOffset: 0,\n pad,\n skip: false\n };\n continue;\n }\n if (state.kind === \"file\") {\n const avail2 = buf.length - cursor;\n if (avail2 === 0) break;\n if (state.remaining > 0) {\n const take2 = Math.min(state.remaining, avail2);\n state.fileBuf.set(buf.subarray(cursor, cursor + take2), state.fileOffset);\n state.fileOffset += take2;\n state.remaining -= take2;\n cursor += take2;\n if (state.remaining > 0) break;\n }\n if (state.pad > 0) {\n const avail22 = buf.length - cursor;\n if (avail22 === 0) break;\n const take2 = Math.min(state.pad, avail22);\n state.pad -= take2;\n cursor += take2;\n if (state.pad > 0) break;\n }\n yield { name: state.name, data: state.fileBuf };\n state = { kind: \"header\" };\n continue;\n }\n const avail = buf.length - cursor;\n if (avail === 0) break;\n const take = Math.min(state.remaining, avail);\n cursor += take;\n state.remaining -= take;\n if (state.remaining > 0) break;\n state = { kind: \"header\" };\n }\n if (cursor >= buf.length) {\n carry = new Uint8Array(0);\n } else if (cursor === 0) {\n carry = buf;\n } else {\n carry = buf.slice(cursor);\n }\n }\n}";
|
|
23
|
-
export const TAR_STREAM_PREAMBLE_SIZE =
|
|
24
|
-
export const W7_FRAME_PREAMBLE = "// src/_shared/w7-frame.ts\nvar W7_MAGIC = new Uint8Array([78, 87, 55, 1]);\nvar W7_TRAILER = new Uint8Array([78, 69, 78, 68]);\nvar ENCODER_QUEUE_HWM = 256 * 1024;\nvar ENCODER_EMIT_CAP = 64 * 1024;\nvar _peakInFlight = 0;\nvar _currentInFlight = 0;\nfunction _peakInFlightBytes() {\n return _peakInFlight;\n}\nfunction _resetPeakInFlightBytes() {\n _peakInFlight = 0;\n _currentInFlight = 0;\n}\nfunction _trackInFlightAdd(n) {\n _currentInFlight += n;\n if (_currentInFlight > _peakInFlight) _peakInFlight = _currentInFlight;\n}\nfunction _trackInFlightSub(n) {\n _currentInFlight -= n;\n if (_currentInFlight < 0) _currentInFlight = 0;\n}\nfunction encodeWriteBatchStream(payload) {\n const inodes = payload.inodes ?? [];\n const chunks = payload.chunks ?? [];\n const deletePaths = payload.deletePaths;\n let phase = 0;\n let chunkIdx = 0;\n const enc = new TextEncoder();\n const headerBytes = enc.encode(JSON.stringify({\n inodes,\n deletePaths,\n chunkCount: chunks.length\n }));\n const source = {\n type: \"bytes\",\n pull(controller) {\n try {\n if (phase === 0) {\n enqueue(controller, W7_MAGIC.slice());\n phase = 1;\n return;\n }\n if (phase === 1) {\n const hdr = new Uint8Array(4 + headerBytes.length);\n writeU32LE(hdr, 0, headerBytes.length);\n hdr.set(headerBytes, 4);\n enqueue(controller, hdr);\n phase = 2;\n return;\n }\n if (phase === 2) {\n if (chunkIdx >= chunks.length) {\n phase = 3;\n } else {\n const c = chunks[chunkIdx++];\n const pathBytes = enc.encode(c.path);\n const data = c.data instanceof Uint8Array ? c.data : new Uint8Array(c.data);\n const headerSize = 4 + pathBytes.length + 4 + 4;\n const headerOut = new Uint8Array(headerSize);\n let o = 0;\n writeU32LE(headerOut, o, pathBytes.length);\n o += 4;\n headerOut.set(pathBytes, o);\n o += pathBytes.length;\n writeU32LE(headerOut, o, c.chunkId);\n o += 4;\n writeU32LE(headerOut, o, data.length);\n o += 4;\n enqueue(controller, headerOut);\n if (data.length === 0) return;\n if (data.length <= ENCODER_EMIT_CAP) {\n enqueue(controller, data);\n } else {\n for (let i = 0; i < data.length; i += ENCODER_EMIT_CAP) {\n enqueue(controller, data.subarray(i, Math.min(i + ENCODER_EMIT_CAP, data.length)));\n }\n }\n return;\n }\n }\n if (phase === 3) {\n enqueue(controller, W7_TRAILER.slice());\n phase = 4;\n controller.close();\n return;\n }\n controller.close();\n } catch (e) {\n controller.error(e);\n }\n },\n cancel() {\n phase = 4;\n chunkIdx = chunks.length;\n }\n };\n return new ReadableStream(source, {\n // Byte streams (`type: 'bytes'`) MUST use the default byte-counted\n // strategy: highWaterMark is in bytes, size is implicit. Custom\n // `size` callbacks are forbidden — they throw at construction.\n highWaterMark: ENCODER_QUEUE_HWM\n });\n function enqueue(controller, bytes) {\n _trackInFlightAdd(bytes.byteLength);\n controller.enqueue(bytes);\n queueMicrotask(() => _trackInFlightSub(bytes.byteLength));\n }\n}\nasync function decodeWriteBatchStream(stream) {\n const reader = stream.getReader();\n const buf = new ByteBuffer(reader);\n const magic = await buf.readExact(4, \"magic\");\n if (!bytesEqual(magic, W7_MAGIC)) {\n try {\n reader.releaseLock();\n } catch {\n }\n throw new Error(`w7-frame: bad magic, expected NW7\\\\x01, got ${Array.from(magic).map((b) => b.toString(16)).join(\" \")}`);\n }\n const hdrLenBytes = await buf.readExact(4, \"header-length\");\n const hdrLen = readU32LE(hdrLenBytes, 0);\n if (hdrLen < 0 || hdrLen > 64 * 1024 * 1024) {\n throw new Error(`w7-frame: implausible header length ${hdrLen}`);\n }\n const hdrJson = await buf.readExact(hdrLen, \"header-json\");\n let header;\n try {\n header = JSON.parse(new TextDecoder().decode(hdrJson));\n } catch (e) {\n throw new Error(`w7-frame: header JSON parse failed: ${e?.message || e}`);\n }\n if (!header || !Array.isArray(header.inodes) || typeof header.chunkCount !== \"number\") {\n throw new Error(\"w7-frame: header missing required fields (inodes / chunkCount)\");\n }\n const chunkIter = {\n [Symbol.asyncIterator]() {\n let idx = 0;\n let exhausted = false;\n return {\n async next() {\n if (exhausted) return { value: void 0, done: true };\n if (idx >= header.chunkCount) {\n const trailer = await buf.readExact(4, \"trailer\");\n if (!bytesEqual(trailer, W7_TRAILER)) {\n throw new Error(\n `w7-frame: bad trailer, expected NEND, got ${Array.from(trailer).map((b) => String.fromCharCode(b)).join(\"\")}`\n );\n }\n try {\n reader.releaseLock();\n } catch {\n }\n exhausted = true;\n return { value: void 0, done: true };\n }\n const pathLenBytes = await buf.readExact(4, \"chunk-path-length\");\n const pathLen = readU32LE(pathLenBytes, 0);\n if (pathLen < 0 || pathLen > 64 * 1024) {\n throw new Error(`w7-frame: implausible path length ${pathLen} at chunk ${idx}`);\n }\n const pathBytes = await buf.readExact(pathLen, \"chunk-path\");\n const path = new TextDecoder().decode(pathBytes);\n const chunkIdBytes = await buf.readExact(4, \"chunk-id\");\n const chunkId = readU32LE(chunkIdBytes, 0);\n const dataLenBytes = await buf.readExact(4, \"chunk-data-length\");\n const dataLen = readU32LE(dataLenBytes, 0);\n if (dataLen < 0 || dataLen > 64 * 1024 * 1024) {\n throw new Error(`w7-frame: implausible data length ${dataLen} at chunk ${idx}`);\n }\n const data = await buf.readExact(dataLen, \"chunk-data\");\n idx++;\n return { value: { path, chunkId, data }, done: false };\n },\n async return() {\n exhausted = true;\n try {\n reader.releaseLock();\n } catch {\n }\n return { value: void 0, done: true };\n }\n };\n }\n };\n return {\n inodes: header.inodes,\n deletePaths: header.deletePaths,\n chunkIter\n };\n}\nfunction writeU32LE(out, off, n) {\n out[off] = n & 255;\n out[off + 1] = n >>> 8 & 255;\n out[off + 2] = n >>> 16 & 255;\n out[off + 3] = n >>> 24 & 255;\n}\nfunction readU32LE(buf, off) {\n return (buf[off] | buf[off + 1] << 8 | buf[off + 2] << 16 | buf[off + 3] << 24) >>> 0;\n}\nfunction bytesEqual(a, b) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\nvar ByteBuffer = class {\n constructor(reader) {\n this.reader = reader;\n }\n chunks = [];\n avail = 0;\n done = false;\n async readExact(n, label) {\n if (n === 0) return new Uint8Array(0);\n while (this.avail < n) {\n if (this.done) {\n throw new Error(\n `w7-frame: stream ended ${this.avail} bytes into expected ${n}-byte ${label}`\n );\n }\n const { value, done } = await this.reader.read();\n if (done) {\n this.done = true;\n } else if (value && value.length > 0) {\n this.chunks.push(value);\n this.avail += value.length;\n }\n }\n const out = new Uint8Array(n);\n let copied = 0;\n while (copied < n) {\n const head = this.chunks[0];\n const take = Math.min(head.length, n - copied);\n out.set(head.subarray(0, take), copied);\n copied += take;\n if (take === head.length) {\n this.chunks.shift();\n } else {\n this.chunks[0] = head.subarray(take);\n }\n }\n this.avail -= n;\n return out;\n }\n};";
|
|
25
|
-
export const W7_FRAME_PREAMBLE_SIZE =
|
|
22
|
+
export const TAR_STREAM_PREAMBLE = "// packages/worker/src/npm/tarball-stream.ts\nvar MAX_FILE_BYTES = 2e7;\nfunction parseTarHeader(block) {\n if (block[0] === 0) return null;\n let name = \"\";\n for (let i = 0; i < 100 && block[i] !== 0; i++) {\n name += String.fromCharCode(block[i]);\n }\n let prefix = \"\";\n for (let i = 345; i < 500 && block[i] !== 0; i++) {\n prefix += String.fromCharCode(block[i]);\n }\n if (prefix) name = prefix + \"/\" + name;\n name = name.replace(/^package\\//, \"\");\n let sizeStr = \"\";\n for (let i = 124; i < 136 && block[i] !== 0; i++) {\n sizeStr += String.fromCharCode(block[i]);\n }\n const size = parseInt(sizeStr.trim(), 8) || 0;\n const typeFlag = block[156];\n return { name, size, typeFlag };\n}\nasync function* readableStreamToAsyncIterable(rs) {\n const reader = rs.getReader();\n try {\n while (true) {\n const { value, done } = await reader.read();\n if (done) return;\n if (value && value.length > 0) yield value;\n }\n } finally {\n try {\n reader.releaseLock();\n } catch {\n }\n }\n}\nasync function* streamTarEntries(source, onSkip) {\n let carry = new Uint8Array(0);\n let state = { kind: \"header\" };\n function concat(a, b) {\n if (a.length === 0) return b;\n if (b.length === 0) return a;\n const out = new Uint8Array(a.length + b.length);\n out.set(a, 0);\n out.set(b, a.length);\n return out;\n }\n for await (const chunkRaw of source) {\n let buf = concat(carry, chunkRaw);\n let cursor = 0;\n while (true) {\n if (state.kind === \"header\") {\n if (buf.length - cursor < 512) break;\n const header = buf.subarray(cursor, cursor + 512);\n const parsed = parseTarHeader(header);\n cursor += 512;\n if (!parsed) return;\n const { name, size, typeFlag } = parsed;\n const pad = size === 0 ? 0 : (512 - size % 512) % 512;\n const isRegularFile = typeFlag === 48 || typeFlag === 0;\n if (size === 0) {\n if (isRegularFile && name) {\n yield { name, data: new Uint8Array(0) };\n }\n state = { kind: \"header\" };\n continue;\n }\n if (!isRegularFile || !name || size > MAX_FILE_BYTES) {\n if (onSkip) {\n const reason = !isRegularFile ? \"non-regular\" : !name ? \"no-name\" : \"too-large\";\n try {\n onSkip(name, size, reason);\n } catch {\n }\n }\n state = { kind: \"skip\", remaining: size + pad };\n continue;\n }\n state = {\n kind: \"file\",\n name,\n remaining: size,\n fileBuf: new Uint8Array(size),\n fileOffset: 0,\n pad,\n skip: false\n };\n continue;\n }\n if (state.kind === \"file\") {\n const avail2 = buf.length - cursor;\n if (avail2 === 0) break;\n if (state.remaining > 0) {\n const take2 = Math.min(state.remaining, avail2);\n state.fileBuf.set(buf.subarray(cursor, cursor + take2), state.fileOffset);\n state.fileOffset += take2;\n state.remaining -= take2;\n cursor += take2;\n if (state.remaining > 0) break;\n }\n if (state.pad > 0) {\n const avail22 = buf.length - cursor;\n if (avail22 === 0) break;\n const take2 = Math.min(state.pad, avail22);\n state.pad -= take2;\n cursor += take2;\n if (state.pad > 0) break;\n }\n yield { name: state.name, data: state.fileBuf };\n state = { kind: \"header\" };\n continue;\n }\n const avail = buf.length - cursor;\n if (avail === 0) break;\n const take = Math.min(state.remaining, avail);\n cursor += take;\n state.remaining -= take;\n if (state.remaining > 0) break;\n state = { kind: \"header\" };\n }\n if (cursor >= buf.length) {\n carry = new Uint8Array(0);\n } else if (cursor === 0) {\n carry = buf;\n } else {\n carry = buf.slice(cursor);\n }\n }\n}";
|
|
23
|
+
export const TAR_STREAM_PREAMBLE_SIZE = 3948;
|
|
24
|
+
export const W7_FRAME_PREAMBLE = "// packages/worker/src/_shared/w7-frame.ts\nvar W7_MAGIC = new Uint8Array([78, 87, 55, 1]);\nvar W7_TRAILER = new Uint8Array([78, 69, 78, 68]);\nvar ENCODER_QUEUE_HWM = 256 * 1024;\nvar ENCODER_EMIT_CAP = 64 * 1024;\nvar _peakInFlight = 0;\nvar _currentInFlight = 0;\nfunction _peakInFlightBytes() {\n return _peakInFlight;\n}\nfunction _resetPeakInFlightBytes() {\n _peakInFlight = 0;\n _currentInFlight = 0;\n}\nfunction _trackInFlightAdd(n) {\n _currentInFlight += n;\n if (_currentInFlight > _peakInFlight) _peakInFlight = _currentInFlight;\n}\nfunction _trackInFlightSub(n) {\n _currentInFlight -= n;\n if (_currentInFlight < 0) _currentInFlight = 0;\n}\nfunction encodeWriteBatchStream(payload) {\n const inodes = payload.inodes ?? [];\n const chunks = payload.chunks ?? [];\n const deletePaths = payload.deletePaths;\n let phase = 0;\n let chunkIdx = 0;\n const enc = new TextEncoder();\n const headerBytes = enc.encode(JSON.stringify({\n inodes,\n deletePaths,\n chunkCount: chunks.length\n }));\n const source = {\n type: \"bytes\",\n pull(controller) {\n try {\n if (phase === 0) {\n enqueue(controller, W7_MAGIC.slice());\n phase = 1;\n return;\n }\n if (phase === 1) {\n const hdr = new Uint8Array(4 + headerBytes.length);\n writeU32LE(hdr, 0, headerBytes.length);\n hdr.set(headerBytes, 4);\n enqueue(controller, hdr);\n phase = 2;\n return;\n }\n if (phase === 2) {\n if (chunkIdx >= chunks.length) {\n phase = 3;\n } else {\n const c = chunks[chunkIdx++];\n const pathBytes = enc.encode(c.path);\n const data = c.data instanceof Uint8Array ? c.data : new Uint8Array(c.data);\n const headerSize = 4 + pathBytes.length + 4 + 4;\n const headerOut = new Uint8Array(headerSize);\n let o = 0;\n writeU32LE(headerOut, o, pathBytes.length);\n o += 4;\n headerOut.set(pathBytes, o);\n o += pathBytes.length;\n writeU32LE(headerOut, o, c.chunkId);\n o += 4;\n writeU32LE(headerOut, o, data.length);\n o += 4;\n enqueue(controller, headerOut);\n if (data.length === 0) return;\n if (data.length <= ENCODER_EMIT_CAP) {\n enqueue(controller, data);\n } else {\n for (let i = 0; i < data.length; i += ENCODER_EMIT_CAP) {\n enqueue(controller, data.subarray(i, Math.min(i + ENCODER_EMIT_CAP, data.length)));\n }\n }\n return;\n }\n }\n if (phase === 3) {\n enqueue(controller, W7_TRAILER.slice());\n phase = 4;\n controller.close();\n return;\n }\n controller.close();\n } catch (e) {\n controller.error(e);\n }\n },\n cancel() {\n phase = 4;\n chunkIdx = chunks.length;\n }\n };\n return new ReadableStream(source, {\n // Byte streams (`type: 'bytes'`) MUST use the default byte-counted\n // strategy: highWaterMark is in bytes, size is implicit. Custom\n // `size` callbacks are forbidden — they throw at construction.\n highWaterMark: ENCODER_QUEUE_HWM\n });\n function enqueue(controller, bytes) {\n _trackInFlightAdd(bytes.byteLength);\n controller.enqueue(bytes);\n queueMicrotask(() => _trackInFlightSub(bytes.byteLength));\n }\n}\nasync function decodeWriteBatchStream(stream) {\n const reader = stream.getReader();\n const buf = new ByteBuffer(reader);\n const magic = await buf.readExact(4, \"magic\");\n if (!bytesEqual(magic, W7_MAGIC)) {\n try {\n reader.releaseLock();\n } catch {\n }\n throw new Error(`w7-frame: bad magic, expected NW7\\\\x01, got ${Array.from(magic).map((b) => b.toString(16)).join(\" \")}`);\n }\n const hdrLenBytes = await buf.readExact(4, \"header-length\");\n const hdrLen = readU32LE(hdrLenBytes, 0);\n if (hdrLen < 0 || hdrLen > 64 * 1024 * 1024) {\n throw new Error(`w7-frame: implausible header length ${hdrLen}`);\n }\n const hdrJson = await buf.readExact(hdrLen, \"header-json\");\n let header;\n try {\n header = JSON.parse(new TextDecoder().decode(hdrJson));\n } catch (e) {\n throw new Error(`w7-frame: header JSON parse failed: ${e?.message || e}`);\n }\n if (!header || !Array.isArray(header.inodes) || typeof header.chunkCount !== \"number\") {\n throw new Error(\"w7-frame: header missing required fields (inodes / chunkCount)\");\n }\n const chunkIter = {\n [Symbol.asyncIterator]() {\n let idx = 0;\n let exhausted = false;\n return {\n async next() {\n if (exhausted) return { value: void 0, done: true };\n if (idx >= header.chunkCount) {\n const trailer = await buf.readExact(4, \"trailer\");\n if (!bytesEqual(trailer, W7_TRAILER)) {\n throw new Error(\n `w7-frame: bad trailer, expected NEND, got ${Array.from(trailer).map((b) => String.fromCharCode(b)).join(\"\")}`\n );\n }\n try {\n reader.releaseLock();\n } catch {\n }\n exhausted = true;\n return { value: void 0, done: true };\n }\n const pathLenBytes = await buf.readExact(4, \"chunk-path-length\");\n const pathLen = readU32LE(pathLenBytes, 0);\n if (pathLen < 0 || pathLen > 64 * 1024) {\n throw new Error(`w7-frame: implausible path length ${pathLen} at chunk ${idx}`);\n }\n const pathBytes = await buf.readExact(pathLen, \"chunk-path\");\n const path = new TextDecoder().decode(pathBytes);\n const chunkIdBytes = await buf.readExact(4, \"chunk-id\");\n const chunkId = readU32LE(chunkIdBytes, 0);\n const dataLenBytes = await buf.readExact(4, \"chunk-data-length\");\n const dataLen = readU32LE(dataLenBytes, 0);\n if (dataLen < 0 || dataLen > 64 * 1024 * 1024) {\n throw new Error(`w7-frame: implausible data length ${dataLen} at chunk ${idx}`);\n }\n const data = await buf.readExact(dataLen, \"chunk-data\");\n idx++;\n return { value: { path, chunkId, data }, done: false };\n },\n async return() {\n exhausted = true;\n try {\n reader.releaseLock();\n } catch {\n }\n return { value: void 0, done: true };\n }\n };\n }\n };\n return {\n inodes: header.inodes,\n deletePaths: header.deletePaths,\n chunkIter\n };\n}\nfunction writeU32LE(out, off, n) {\n out[off] = n & 255;\n out[off + 1] = n >>> 8 & 255;\n out[off + 2] = n >>> 16 & 255;\n out[off + 3] = n >>> 24 & 255;\n}\nfunction readU32LE(buf, off) {\n return (buf[off] | buf[off + 1] << 8 | buf[off + 2] << 16 | buf[off + 3] << 24) >>> 0;\n}\nfunction bytesEqual(a, b) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n}\nvar ByteBuffer = class {\n constructor(reader) {\n this.reader = reader;\n }\n chunks = [];\n avail = 0;\n done = false;\n async readExact(n, label) {\n if (n === 0) return new Uint8Array(0);\n while (this.avail < n) {\n if (this.done) {\n throw new Error(\n `w7-frame: stream ended ${this.avail} bytes into expected ${n}-byte ${label}`\n );\n }\n const { value, done } = await this.reader.read();\n if (done) {\n this.done = true;\n } else if (value && value.length > 0) {\n this.chunks.push(value);\n this.avail += value.length;\n }\n }\n const out = new Uint8Array(n);\n let copied = 0;\n while (copied < n) {\n const head = this.chunks[0];\n const take = Math.min(head.length, n - copied);\n out.set(head.subarray(0, take), copied);\n copied += take;\n if (take === head.length) {\n this.chunks.shift();\n } else {\n this.chunks[0] = head.subarray(take);\n }\n }\n this.avail -= n;\n return out;\n }\n};";
|
|
25
|
+
export const W7_FRAME_PREAMBLE_SIZE = 7970;
|
package/dist/router/index.d.ts
CHANGED
|
@@ -31,6 +31,8 @@
|
|
|
31
31
|
* - The default `createNimbusHandler()` (zero args) is the
|
|
32
32
|
* "ship-Nimbus-as-is" case — exactly what `apps/dogfood/` does.
|
|
33
33
|
*/
|
|
34
|
+
import { type NimbusSdkRouterConfig } from './remote-api.js';
|
|
35
|
+
export type { NimbusConfig as NimbusSdkConfig, NimbusRemoteApiConfig, NimbusRuntimePolicy, NimbusSandboxProfile, NimbusSdkRouterConfig, } from './remote-api.js';
|
|
34
36
|
/**
|
|
35
37
|
* Lifecycle event a hook receives. Hooks should treat this as read-only.
|
|
36
38
|
*/
|
|
@@ -50,8 +52,8 @@ export interface NimbusHookContext {
|
|
|
50
52
|
* NOT short-circuit the request.
|
|
51
53
|
*
|
|
52
54
|
* `onSessionStart` fires on the first WebSocket upgrade for a session
|
|
53
|
-
* (not on every HTTP API call). `onSessionEnd`
|
|
54
|
-
* session
|
|
55
|
+
* (not on every HTTP API call). `onSessionEnd` is reserved for future
|
|
56
|
+
* session lifecycle events.
|
|
55
57
|
*/
|
|
56
58
|
export interface NimbusHooks {
|
|
57
59
|
/**
|
|
@@ -110,6 +112,8 @@ export interface CreateNimbusHandlerOptions {
|
|
|
110
112
|
routes?: CustomRoutes;
|
|
111
113
|
/** Auth-mode selector. Default `'auto'`. */
|
|
112
114
|
auth?: NimbusAuthConfig;
|
|
115
|
+
/** Programmatic SDK HTTP surface. Disabled unless explicitly enabled. */
|
|
116
|
+
sdk?: NimbusSdkRouterConfig;
|
|
113
117
|
}
|
|
114
118
|
/**
|
|
115
119
|
* The shape every `default export` Workers handler must satisfy.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/router/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AAqBH,OAAO,EAEL,KAAK,qBAAqB,EAC3B,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,YAAY,IAAI,eAAe,EAC/B,qBAAqB,EACrB,mBAAmB,EACnB,oBAAoB,EACpB,qBAAqB,GACtB,MAAM,iBAAiB,CAAC;AAEzB;;GAEG;AACH,MAAM,WAAW,iBAAiB;IAChC,oDAAoD;IACpD,aAAa,EAAE,MAAM,CAAC;IACtB,qCAAqC;IACrC,SAAS,EAAE,MAAM,CAAC;IAClB,kFAAkF;IAClF,OAAO,EAAE,OAAO,CAAC;IACjB,oBAAoB;IACpB,GAAG,EAAE,GAAG,CAAC;CACV;AAED;;;;;;;;GAQG;AACH,MAAM,WAAW,WAAW;IAC1B;;;;;;OAMG;IACH,cAAc,CAAC,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAE9D;;;OAGG;IACH,YAAY,CAAC,CAAC,GAAG,EAAE,iBAAiB,GAAG,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC7D;AAED;;;;;;;;;;;;;;GAcG;AACH,MAAM,MAAM,QAAQ,GAAG,MAAM,GAAG,SAAS,GAAG,QAAQ,CAAC;AAErD,0CAA0C;AAC1C,MAAM,WAAW,gBAAgB;IAC/B,IAAI,CAAC,EAAE,QAAQ,CAAC;IAChB;;;;OAIG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB;AAED;;;;;;GAMG;AACH,MAAM,MAAM,YAAY,GAAG,CACzB,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,gBAAgB,KAClB,QAAQ,GAAG,IAAI,GAAG,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CAAC;AAEhD,+CAA+C;AAC/C,MAAM,WAAW,0BAA0B;IACzC,oCAAoC;IACpC,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,qDAAqD;IACrD,MAAM,CAAC,EAAE,YAAY,CAAC;IACtB,4CAA4C;IAC5C,IAAI,CAAC,EAAE,gBAAgB,CAAC;IACxB,yEAAyE;IACzE,GAAG,CAAC,EAAE,qBAAqB,CAAC;CAC7B;AAED;;;GAGG;AACH,MAAM,WAAW,aAAa;IAC5B,KAAK,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,EAAE,GAAG,EAAE,GAAG,EAAE,gBAAgB,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAC7E;AAED;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAiCG;AACH,wBAAgB,mBAAmB,CACjC,OAAO,GAAE,0BAA+B,GACvC,aAAa,CAkHf"}
|
package/dist/router/index.js
CHANGED
|
@@ -35,6 +35,7 @@ import { generateSessionId, isValidSessionId, } from '../_shared/session-id.js';
|
|
|
35
35
|
import { parseSessionRoute, forwardToSession, renderInvalidSessionHtml, SESSION_ROUTE_PREFIX, LEGACY_PUBLIC_DO_SEGMENT, } from '../_shared/session-router.js';
|
|
36
36
|
import { verifyRequestToken, authErrorResponse, NimbusAuthError, NimbusTokenMalformedError, } from '../auth/index.js';
|
|
37
37
|
import { setCtxExports } from '../session/ctx-exports.js';
|
|
38
|
+
import { handleNimbusRemoteApi, } from './remote-api.js';
|
|
38
39
|
/**
|
|
39
40
|
* Build a Nimbus default-export handler. The returned object is exactly
|
|
40
41
|
* what `export default` expects in a Workers entry module.
|
|
@@ -94,6 +95,9 @@ export function createNimbusHandler(options = {}) {
|
|
|
94
95
|
}
|
|
95
96
|
}
|
|
96
97
|
const url = new URL(request.url);
|
|
98
|
+
const sdkResponse = await handleNimbusRemoteApi(request, env, options.sdk);
|
|
99
|
+
if (sdkResponse)
|
|
100
|
+
return sdkResponse;
|
|
97
101
|
// ── /new — spawn a fresh session and redirect ───────────────────
|
|
98
102
|
if (url.pathname === '/new') {
|
|
99
103
|
if (request.method !== 'POST' && request.method !== 'GET') {
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
export type NimbusRuntimeName = 'node' | 'bun' | 'npm' | 'git' | 'python' | 'ruby' | 'clang' | 'shell' | (string & {});
|
|
2
|
+
export interface NimbusRuntimePolicy {
|
|
3
|
+
preinstall?: string[];
|
|
4
|
+
onDemand?: boolean;
|
|
5
|
+
allow?: NimbusRuntimeName[];
|
|
6
|
+
}
|
|
7
|
+
export interface NimbusSandboxProfile {
|
|
8
|
+
root?: string;
|
|
9
|
+
runtimes?: NimbusRuntimePolicy;
|
|
10
|
+
tools?: {
|
|
11
|
+
namespace?: string;
|
|
12
|
+
kind?: string;
|
|
13
|
+
};
|
|
14
|
+
preview?: {
|
|
15
|
+
baseUrl?: string;
|
|
16
|
+
pathStyle?: boolean;
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
export interface NimbusConfig {
|
|
20
|
+
endpoint?: string;
|
|
21
|
+
sandboxes?: Record<string, NimbusSandboxProfile>;
|
|
22
|
+
}
|
|
23
|
+
export interface NimbusRemoteApiConfig {
|
|
24
|
+
/** Enable the remote programmatic sandbox API. */
|
|
25
|
+
enabled?: boolean;
|
|
26
|
+
/** Route prefix. Defaults to `/api/nimbus/v1`. */
|
|
27
|
+
basePath?: string;
|
|
28
|
+
/**
|
|
29
|
+
* Permit unauthenticated remote calls when JWT_SECRET is absent. This is
|
|
30
|
+
* intended only for private development deployments.
|
|
31
|
+
*/
|
|
32
|
+
allowLegacy?: boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Required token scopes. Tokens without an explicit `scopes` claim keep the
|
|
35
|
+
* existing full-trust semantics.
|
|
36
|
+
*/
|
|
37
|
+
requiredScopes?: string[];
|
|
38
|
+
}
|
|
39
|
+
export interface NimbusSdkRouterConfig {
|
|
40
|
+
remote?: boolean | NimbusRemoteApiConfig;
|
|
41
|
+
config?: NimbusConfig;
|
|
42
|
+
}
|
|
43
|
+
export declare function handleNimbusRemoteApi(request: Request, env: any, sdk: NimbusSdkRouterConfig | undefined): Promise<Response | null>;
|
|
44
|
+
//# sourceMappingURL=remote-api.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"remote-api.d.ts","sourceRoot":"","sources":["../../src/router/remote-api.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,iBAAiB,GACzB,MAAM,GACN,KAAK,GACL,KAAK,GACL,KAAK,GACL,QAAQ,GACR,MAAM,GACN,OAAO,GACP,OAAO,GACP,CAAC,MAAM,GAAG,EAAE,CAAC,CAAC;AAElB,MAAM,WAAW,mBAAmB;IAClC,UAAU,CAAC,EAAE,MAAM,EAAE,CAAC;IACtB,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,iBAAiB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,oBAAoB;IACnC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,KAAK,CAAC,EAAE;QACN,SAAS,CAAC,EAAE,MAAM,CAAC;QACnB,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,CAAC;IACF,OAAO,CAAC,EAAE;QACR,OAAO,CAAC,EAAE,MAAM,CAAC;QACjB,SAAS,CAAC,EAAE,OAAO,CAAC;KACrB,CAAC;CACH;AAED,MAAM,WAAW,YAAY;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,SAAS,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,oBAAoB,CAAC,CAAC;CAClD;AAED,MAAM,WAAW,qBAAqB;IACpC,kDAAkD;IAClD,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kDAAkD;IAClD,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,qBAAqB;IACpC,MAAM,CAAC,EAAE,OAAO,GAAG,qBAAqB,CAAC;IACzC,MAAM,CAAC,EAAE,YAAY,CAAC;CACvB;AAqBD,wBAAsB,qBAAqB,CACzC,OAAO,EAAE,OAAO,EAChB,GAAG,EAAE,GAAG,EACR,GAAG,EAAE,qBAAqB,GAAG,SAAS,GACrC,OAAO,CAAC,QAAQ,GAAG,IAAI,CAAC,CA8D1B"}
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
import { LEGACY_PUBLIC_DO_SEGMENT, } from '../_shared/session-router.js';
|
|
2
|
+
import { requireScopes, requireSessionPin, verifyRequestToken, NimbusAuthError, NimbusTokenMalformedError, isNimbusIdComponent, } from '../auth/index.js';
|
|
3
|
+
const DEFAULT_REMOTE_BASE_PATH = '/api/nimbus/v1';
|
|
4
|
+
export async function handleNimbusRemoteApi(request, env, sdk) {
|
|
5
|
+
const remote = normalizeRemoteConfig(sdk?.remote);
|
|
6
|
+
if (!remote.enabled)
|
|
7
|
+
return null;
|
|
8
|
+
const url = new URL(request.url);
|
|
9
|
+
const match = matchRemoteRpc(url.pathname, remote.basePath);
|
|
10
|
+
if (!match)
|
|
11
|
+
return null;
|
|
12
|
+
if (request.method === 'OPTIONS')
|
|
13
|
+
return corsResponse(null, 204);
|
|
14
|
+
if (request.method !== 'POST') {
|
|
15
|
+
return remoteJson({ ok: false, error: 'Method not allowed', code: 'E_METHOD_NOT_ALLOWED' }, 405);
|
|
16
|
+
}
|
|
17
|
+
if (!env?.NIMBUS_SESSION) {
|
|
18
|
+
return remoteJson({ ok: false, error: 'NIMBUS_SESSION binding is missing', code: 'E_NIMBUS_BINDING_MISSING' }, 500);
|
|
19
|
+
}
|
|
20
|
+
if (!isNimbusIdComponent(match.sandboxId)) {
|
|
21
|
+
return remoteJson({ ok: false, error: 'Invalid sandbox id', code: 'E_SANDBOX_ID' }, 400);
|
|
22
|
+
}
|
|
23
|
+
let body;
|
|
24
|
+
try {
|
|
25
|
+
body = decodeWire(await request.json());
|
|
26
|
+
}
|
|
27
|
+
catch (e) {
|
|
28
|
+
return remoteJson({ ok: false, error: `Invalid JSON body: ${e?.message || e}`, code: 'E_BAD_JSON' }, 400);
|
|
29
|
+
}
|
|
30
|
+
const tenantSegment = await resolveRemoteTenantSegment(request, env, remote, match.sandboxId);
|
|
31
|
+
if (tenantSegment instanceof Response)
|
|
32
|
+
return tenantSegment;
|
|
33
|
+
const profileName = body.profile ?? 'default';
|
|
34
|
+
const profile = sdk?.config?.sandboxes?.[profileName]
|
|
35
|
+
?? sdk?.config?.sandboxes?.default
|
|
36
|
+
?? {};
|
|
37
|
+
const root = body.root ?? profile.root ?? '/home/user';
|
|
38
|
+
const doName = `${tenantSegment}:${match.sandboxId}`;
|
|
39
|
+
const id = env.NIMBUS_SESSION.idFromName(doName);
|
|
40
|
+
const stub = env.NIMBUS_SESSION.get(id);
|
|
41
|
+
const ctx = {
|
|
42
|
+
env,
|
|
43
|
+
stub,
|
|
44
|
+
body,
|
|
45
|
+
profileName,
|
|
46
|
+
profile,
|
|
47
|
+
root,
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
const result = await dispatchRemoteRpc(ctx);
|
|
51
|
+
return remoteJson({ ok: true, result });
|
|
52
|
+
}
|
|
53
|
+
catch (e) {
|
|
54
|
+
return remoteJson({
|
|
55
|
+
ok: false,
|
|
56
|
+
error: e?.message || String(e),
|
|
57
|
+
code: e?.code || 'E_NIMBUS_REMOTE_RPC',
|
|
58
|
+
}, e?.httpStatus || 500);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function normalizeRemoteConfig(remote) {
|
|
62
|
+
const value = typeof remote === 'object' ? remote : {};
|
|
63
|
+
return {
|
|
64
|
+
enabled: remote === true || value.enabled === true,
|
|
65
|
+
basePath: normalizeBasePath(value.basePath ?? DEFAULT_REMOTE_BASE_PATH),
|
|
66
|
+
allowLegacy: value.allowLegacy === true,
|
|
67
|
+
requiredScopes: value.requiredScopes ?? ['sandbox:use'],
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
function matchRemoteRpc(pathname, basePath) {
|
|
71
|
+
const prefix = `${basePath}/sandboxes/`;
|
|
72
|
+
if (!pathname.startsWith(prefix) || !pathname.endsWith('/rpc'))
|
|
73
|
+
return null;
|
|
74
|
+
const raw = pathname.slice(prefix.length, -'/rpc'.length);
|
|
75
|
+
if (!raw || raw.includes('/'))
|
|
76
|
+
return null;
|
|
77
|
+
try {
|
|
78
|
+
return { sandboxId: decodeURIComponent(raw) };
|
|
79
|
+
}
|
|
80
|
+
catch {
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
async function resolveRemoteTenantSegment(request, env, remote, sandboxId) {
|
|
85
|
+
const hasSecret = typeof env?.JWT_SECRET === 'string' && env.JWT_SECRET.length > 0;
|
|
86
|
+
if (!hasSecret) {
|
|
87
|
+
if (remote.allowLegacy)
|
|
88
|
+
return LEGACY_PUBLIC_DO_SEGMENT;
|
|
89
|
+
return remoteJson({
|
|
90
|
+
ok: false,
|
|
91
|
+
error: 'Remote Nimbus sandbox API requires JWT_SECRET',
|
|
92
|
+
code: 'E_AUTH_CONFIG_MISSING',
|
|
93
|
+
}, 500);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const verified = await verifyRequestToken(request, env);
|
|
97
|
+
requireScopes(verified, remote.requiredScopes);
|
|
98
|
+
requireSessionPin(verified, sandboxId);
|
|
99
|
+
return verified.doInstanceName;
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
if (e instanceof NimbusAuthError || e instanceof NimbusTokenMalformedError) {
|
|
103
|
+
return remoteJson({ ok: false, error: e.message, code: e.code }, e.httpStatus);
|
|
104
|
+
}
|
|
105
|
+
console.error('[nimbus] remote API auth error:', e);
|
|
106
|
+
return remoteJson({ ok: false, error: 'Internal auth error', code: 'E_AUTH_UNKNOWN' }, 500);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function dispatchRemoteRpc(ctx) {
|
|
110
|
+
const op = ctx.body.op;
|
|
111
|
+
const args = Array.isArray(ctx.body.args) ? ctx.body.args : [];
|
|
112
|
+
switch (op) {
|
|
113
|
+
case 'ready': {
|
|
114
|
+
const options = objectArg(args[0]);
|
|
115
|
+
const preinstall = configuredPreinstall(ctx.profile, options.preinstall);
|
|
116
|
+
for (const spec of preinstall)
|
|
117
|
+
assertRuntimeAllowed(ctx, spec, 'preinstall');
|
|
118
|
+
return ctx.stub._rpcReady({ preinstall });
|
|
119
|
+
}
|
|
120
|
+
case 'exec':
|
|
121
|
+
return ctx.stub._rpcExec(stringArg(args[0], 'command'), execOptions(ctx, args[1]));
|
|
122
|
+
case 'startProcess':
|
|
123
|
+
return ctx.stub._rpcStartProcess(stringArg(args[0], 'command'), execOptions(ctx, args[1]));
|
|
124
|
+
case 'runCode': {
|
|
125
|
+
const options = execOptions(ctx, args[1]);
|
|
126
|
+
const language = typeof options.language === 'string' ? options.language : 'javascript';
|
|
127
|
+
assertRuntimeForLanguage(ctx, language, options.install);
|
|
128
|
+
return ctx.stub._rpcRunCode(stringArg(args[0], 'code'), options);
|
|
129
|
+
}
|
|
130
|
+
case 'readFile':
|
|
131
|
+
return ctx.stub._rpcReadFile(stringArg(args[0], 'path'));
|
|
132
|
+
case 'readFileBytes':
|
|
133
|
+
return ctx.stub._rpcReadFileBytes(stringArg(args[0], 'path'));
|
|
134
|
+
case 'writeFile':
|
|
135
|
+
return ctx.stub._rpcWriteFile(stringArg(args[0], 'path'), args[1]);
|
|
136
|
+
case 'readdir':
|
|
137
|
+
return ctx.stub._rpcReaddir(stringArg(args[0], 'path'));
|
|
138
|
+
case 'exists':
|
|
139
|
+
return ctx.stub._rpcExists(stringArg(args[0], 'path'));
|
|
140
|
+
case 'mkdir':
|
|
141
|
+
return ctx.stub._rpcMkdir(stringArg(args[0], 'path'));
|
|
142
|
+
case 'deleteFile':
|
|
143
|
+
return ctx.stub._rpcDeleteFile(stringArg(args[0], 'path'), objectArg(args[1]));
|
|
144
|
+
case 'installRuntime': {
|
|
145
|
+
const spec = stringArg(args[0], 'spec');
|
|
146
|
+
assertRuntimeAllowed(ctx, spec, 'onDemand');
|
|
147
|
+
return ctx.stub._rpcInstallRuntime(spec, objectArg(args[1]));
|
|
148
|
+
}
|
|
149
|
+
case 'ensureRuntimes': {
|
|
150
|
+
const specs = arrayArg(args[0], 'specs').map((s) => String(s));
|
|
151
|
+
for (const spec of specs)
|
|
152
|
+
assertRuntimeAllowed(ctx, spec, 'onDemand');
|
|
153
|
+
return ctx.stub._rpcEnsureRuntimes(specs, objectArg(args[1]));
|
|
154
|
+
}
|
|
155
|
+
case 'listRuntimes':
|
|
156
|
+
return ctx.stub._rpcListRuntimes();
|
|
157
|
+
case 'listProcesses':
|
|
158
|
+
return ctx.stub._rpcListProcesses();
|
|
159
|
+
case 'killProcess':
|
|
160
|
+
return ctx.stub._rpcKillProcess(numberArg(args[0], 'pid'));
|
|
161
|
+
case 'processLogs':
|
|
162
|
+
return ctx.stub._rpcProcessLogs(numberArg(args[0], 'pid'), objectArg(args[1]));
|
|
163
|
+
case 'listPorts':
|
|
164
|
+
return ctx.stub._rpcListPorts();
|
|
165
|
+
case 'exposePort':
|
|
166
|
+
return ctx.stub._rpcExposePort(numberArg(args[0], 'port'));
|
|
167
|
+
case 'unexposePort':
|
|
168
|
+
return ctx.stub._rpcUnexposePort(numberArg(args[0], 'port'));
|
|
169
|
+
default:
|
|
170
|
+
throw apiError(`Unknown Nimbus sandbox operation: ${String(op)}`, 'E_REMOTE_OP', 400);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function execOptions(ctx, value) {
|
|
174
|
+
const options = objectArg(value);
|
|
175
|
+
return {
|
|
176
|
+
...options,
|
|
177
|
+
cwd: typeof options.cwd === 'string' ? options.cwd : ctx.root,
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
function configuredPreinstall(profile, requested) {
|
|
181
|
+
if (Array.isArray(profile.runtimes?.preinstall))
|
|
182
|
+
return profile.runtimes.preinstall;
|
|
183
|
+
return Array.isArray(requested) ? requested.map((s) => String(s)) : [];
|
|
184
|
+
}
|
|
185
|
+
function assertRuntimeForLanguage(ctx, language, install) {
|
|
186
|
+
if (language === 'python' || language === 'ruby') {
|
|
187
|
+
assertRuntimeAllowed(ctx, language, install === 'ifMissing' ? 'onDemand' : 'use');
|
|
188
|
+
}
|
|
189
|
+
else if (language === 'shell') {
|
|
190
|
+
assertRuntimeAllowed(ctx, 'shell', 'use');
|
|
191
|
+
}
|
|
192
|
+
else if (language === 'javascript' || language === 'typescript') {
|
|
193
|
+
assertRuntimeAllowed(ctx, 'node', 'use');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
function assertRuntimeAllowed(ctx, spec, action) {
|
|
197
|
+
const policy = ctx.profile.runtimes;
|
|
198
|
+
const name = runtimeName(spec);
|
|
199
|
+
if (policy?.allow && !policy.allow.includes(name)) {
|
|
200
|
+
throw apiError(`Nimbus runtime '${name}' is not allowed by sandbox profile '${ctx.profileName}'`, 'E_RUNTIME_NOT_ALLOWED', 403);
|
|
201
|
+
}
|
|
202
|
+
if (action !== 'onDemand' || policy?.onDemand !== false)
|
|
203
|
+
return;
|
|
204
|
+
const preinstalled = new Set((policy.preinstall ?? []).map(runtimeName));
|
|
205
|
+
if (!preinstalled.has(name)) {
|
|
206
|
+
throw apiError(`Nimbus runtime '${name}' is not preinstalled and on-demand runtime installs are disabled by sandbox profile '${ctx.profileName}'`, 'E_RUNTIME_ON_DEMAND_DISABLED', 403);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function runtimeName(spec) {
|
|
210
|
+
return String(spec).split('@')[0];
|
|
211
|
+
}
|
|
212
|
+
function objectArg(value) {
|
|
213
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
214
|
+
? value
|
|
215
|
+
: {};
|
|
216
|
+
}
|
|
217
|
+
function arrayArg(value, name) {
|
|
218
|
+
if (!Array.isArray(value))
|
|
219
|
+
throw apiError(`${name} must be an array`, 'E_ARG_SHAPE', 400);
|
|
220
|
+
return value;
|
|
221
|
+
}
|
|
222
|
+
function stringArg(value, name) {
|
|
223
|
+
if (typeof value !== 'string')
|
|
224
|
+
throw apiError(`${name} must be a string`, 'E_ARG_SHAPE', 400);
|
|
225
|
+
return value;
|
|
226
|
+
}
|
|
227
|
+
function numberArg(value, name) {
|
|
228
|
+
const number = Number(value);
|
|
229
|
+
if (!Number.isFinite(number))
|
|
230
|
+
throw apiError(`${name} must be a number`, 'E_ARG_SHAPE', 400);
|
|
231
|
+
return number;
|
|
232
|
+
}
|
|
233
|
+
function apiError(message, code, status) {
|
|
234
|
+
const err = new Error(message);
|
|
235
|
+
err.code = code;
|
|
236
|
+
err.httpStatus = status;
|
|
237
|
+
return err;
|
|
238
|
+
}
|
|
239
|
+
function normalizeBasePath(path) {
|
|
240
|
+
const trimmed = trimSlashes(String(path || DEFAULT_REMOTE_BASE_PATH));
|
|
241
|
+
return `/${trimmed}`;
|
|
242
|
+
}
|
|
243
|
+
function trimSlashes(value) {
|
|
244
|
+
let start = 0;
|
|
245
|
+
let end = value.length;
|
|
246
|
+
while (start < end && value[start] === '/')
|
|
247
|
+
start++;
|
|
248
|
+
while (end > start && value[end - 1] === '/')
|
|
249
|
+
end--;
|
|
250
|
+
return value.slice(start, end);
|
|
251
|
+
}
|
|
252
|
+
function remoteJson(value, status = 200) {
|
|
253
|
+
return corsResponse(JSON.stringify(encodeWire(value)), status, {
|
|
254
|
+
'Content-Type': 'application/json',
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
function corsResponse(body, status, headers = {}) {
|
|
258
|
+
return new Response(body, {
|
|
259
|
+
status,
|
|
260
|
+
headers: {
|
|
261
|
+
...headers,
|
|
262
|
+
'Cache-Control': 'no-store',
|
|
263
|
+
'Access-Control-Allow-Origin': '*',
|
|
264
|
+
'Access-Control-Allow-Methods': 'POST, OPTIONS',
|
|
265
|
+
'Access-Control-Allow-Headers': 'Authorization, Content-Type',
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
}
|
|
269
|
+
function encodeWire(value) {
|
|
270
|
+
if (value instanceof Uint8Array) {
|
|
271
|
+
return {
|
|
272
|
+
__nimbusWireType: 'bytes',
|
|
273
|
+
base64: bytesToBase64(value),
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
if (value instanceof ArrayBuffer) {
|
|
277
|
+
return {
|
|
278
|
+
__nimbusWireType: 'bytes',
|
|
279
|
+
base64: bytesToBase64(new Uint8Array(value)),
|
|
280
|
+
};
|
|
281
|
+
}
|
|
282
|
+
if (ArrayBuffer.isView(value)) {
|
|
283
|
+
const view = value;
|
|
284
|
+
return {
|
|
285
|
+
__nimbusWireType: 'bytes',
|
|
286
|
+
base64: bytesToBase64(new Uint8Array(view.buffer, view.byteOffset, view.byteLength)),
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
if (Array.isArray(value))
|
|
290
|
+
return value.map(encodeWire);
|
|
291
|
+
if (value && typeof value === 'object') {
|
|
292
|
+
const out = {};
|
|
293
|
+
for (const [key, item] of Object.entries(value)) {
|
|
294
|
+
if (item !== undefined)
|
|
295
|
+
out[key] = encodeWire(item);
|
|
296
|
+
}
|
|
297
|
+
return out;
|
|
298
|
+
}
|
|
299
|
+
return value;
|
|
300
|
+
}
|
|
301
|
+
function decodeWire(value) {
|
|
302
|
+
if (value
|
|
303
|
+
&& typeof value === 'object'
|
|
304
|
+
&& value.__nimbusWireType === 'bytes'
|
|
305
|
+
&& typeof value.base64 === 'string') {
|
|
306
|
+
return base64ToBytes(value.base64);
|
|
307
|
+
}
|
|
308
|
+
if (Array.isArray(value))
|
|
309
|
+
return value.map(decodeWire);
|
|
310
|
+
if (value && typeof value === 'object') {
|
|
311
|
+
const out = {};
|
|
312
|
+
for (const [key, item] of Object.entries(value)) {
|
|
313
|
+
out[key] = decodeWire(item);
|
|
314
|
+
}
|
|
315
|
+
return out;
|
|
316
|
+
}
|
|
317
|
+
return value;
|
|
318
|
+
}
|
|
319
|
+
function bytesToBase64(bytes) {
|
|
320
|
+
let binary = '';
|
|
321
|
+
const chunk = 0x8000;
|
|
322
|
+
for (let i = 0; i < bytes.length; i += chunk) {
|
|
323
|
+
binary += String.fromCharCode(...bytes.subarray(i, i + chunk));
|
|
324
|
+
}
|
|
325
|
+
return btoa(binary);
|
|
326
|
+
}
|
|
327
|
+
function base64ToBytes(base64) {
|
|
328
|
+
const binary = atob(base64);
|
|
329
|
+
const bytes = new Uint8Array(binary.length);
|
|
330
|
+
for (let i = 0; i < binary.length; i++)
|
|
331
|
+
bytes[i] = binary.charCodeAt(i);
|
|
332
|
+
return bytes;
|
|
333
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nimbus-sh/worker",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Nimbus runtime package for Cloudflare Workers, re-exported publicly through @nimbus-sh/sdk/worker.",
|
|
5
5
|
"homepage": "https://github.com/AshishKumar4/Nimbus",
|
|
6
6
|
"repository": {
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"types": "./dist/auth/index.d.ts",
|
|
31
31
|
"import": "./dist/auth/index.js"
|
|
32
32
|
},
|
|
33
|
+
"./runtime-sync-helper": "./scripts/bundle-runtime.mjs",
|
|
33
34
|
"./public/*": "./public/*",
|
|
34
35
|
"./package.json": "./package.json"
|
|
35
36
|
},
|