@nimbus-sh/worker 0.1.0 → 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.
@@ -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,7 +1,7 @@
1
1
  {
2
2
  "name": "@nimbus-sh/worker",
3
- "version": "0.1.0",
4
- "description": "Nimbus — cloud dev environment runtime for Cloudflare Workers. Re-export NimbusSession + createNimbusHandler from your Worker.",
3
+ "version": "0.1.2",
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": {
7
7
  "type": "git",
@@ -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
  },