nebula-ai 0.0.1-dev.20260415044357.874f788
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/bin/nebula-ai.js +293 -0
- package/package.json +35 -0
package/bin/nebula-ai.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Launcher for the `nebula-ai` npm package.
|
|
3
|
+
//
|
|
4
|
+
// The npm package itself is a tiny stub: no JS dependencies, no binary.
|
|
5
|
+
// The first time the user runs `nebula-ai`, this script downloads the
|
|
6
|
+
// matching native Bun single-file executable from the binary host,
|
|
7
|
+
// verifies its integrity against a signed manifest, caches it under
|
|
8
|
+
// `~/.cache/nebula-ai/v<version>/`, and execs it. Subsequent invocations
|
|
9
|
+
// short-circuit straight to exec.
|
|
10
|
+
//
|
|
11
|
+
// ─── Supply-chain integrity gate ────────────────────────────────────────────
|
|
12
|
+
//
|
|
13
|
+
// Before any downloaded binary is marked executable we verify:
|
|
14
|
+
//
|
|
15
|
+
// 1. a per-version `checksums.json` signed with ed25519,
|
|
16
|
+
// 2. the signature in `checksums.json.sig`, against an ed25519 public
|
|
17
|
+
// key baked into this file below (`EMBEDDED_PUBLIC_KEY_PEM`),
|
|
18
|
+
// 3. the downloaded binary's SHA-256 against the entry in the manifest.
|
|
19
|
+
//
|
|
20
|
+
// A mismatch aborts before `chmodSync`/`renameSync`, and the staged
|
|
21
|
+
// tempfile is unlinked. This means a CDN or on-path compromise of the
|
|
22
|
+
// binary host cannot result in arbitrary code execution on the user's
|
|
23
|
+
// machine without also compromising the offline signing key.
|
|
24
|
+
//
|
|
25
|
+
// ─── Key rotation ───────────────────────────────────────────────────────────
|
|
26
|
+
//
|
|
27
|
+
// The ed25519 signing key is stored as the `NEBULA_AI_SIGNING_PRIVATE_KEY`
|
|
28
|
+
// GitHub Actions secret. To rotate:
|
|
29
|
+
//
|
|
30
|
+
// 1. Generate a new keypair locally:
|
|
31
|
+
// openssl genpkey -algorithm ED25519 -out new.key
|
|
32
|
+
// openssl pkey -in new.key -pubout -out new.pub
|
|
33
|
+
// 2. Replace `NEBULA_AI_SIGNING_PRIVATE_KEY` in repo secrets with the
|
|
34
|
+
// contents of new.key.
|
|
35
|
+
// 3. Paste the contents of new.pub into `EMBEDDED_PUBLIC_KEY_PEM` below
|
|
36
|
+
// and ship a new wrapper release. Users on older wrapper versions
|
|
37
|
+
// will continue to verify against the old key until they upgrade,
|
|
38
|
+
// so keep the old key valid (publish one more release with it)
|
|
39
|
+
// long enough for the dist-tag to catch up, then delete the old
|
|
40
|
+
// key material.
|
|
41
|
+
|
|
42
|
+
import { spawnSync } from "node:child_process";
|
|
43
|
+
import { createHash, createPublicKey, verify as cryptoVerify } from "node:crypto";
|
|
44
|
+
import {
|
|
45
|
+
chmodSync,
|
|
46
|
+
createWriteStream,
|
|
47
|
+
existsSync,
|
|
48
|
+
mkdirSync,
|
|
49
|
+
renameSync,
|
|
50
|
+
rmSync,
|
|
51
|
+
} from "node:fs";
|
|
52
|
+
import { readFile } from "node:fs/promises";
|
|
53
|
+
import { homedir } from "node:os";
|
|
54
|
+
import { dirname, join } from "node:path";
|
|
55
|
+
import { pipeline } from "node:stream/promises";
|
|
56
|
+
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
57
|
+
|
|
58
|
+
// The public half of the ed25519 key that signs `checksums.json`. The
|
|
59
|
+
// private half lives only in the `NEBULA_AI_SIGNING_PRIVATE_KEY` GitHub
|
|
60
|
+
// Actions secret.
|
|
61
|
+
const EMBEDDED_PUBLIC_KEY_PEM = `-----BEGIN PUBLIC KEY-----
|
|
62
|
+
MCowBQYDK2VwAyEAd8QFvCTSMY0HmSsJgL6jtFQas00WedvSZhkc9bxdAl0=
|
|
63
|
+
-----END PUBLIC KEY-----
|
|
64
|
+
`;
|
|
65
|
+
|
|
66
|
+
// Stamped by `scripts/publish/prepare.mjs` at wrapper staging time with
|
|
67
|
+
// the channel-appropriate base URL (dev vs prod). The `__NEBULA_AI_…__`
|
|
68
|
+
// sentinel lets us keep the checked-in source runnable for local tests
|
|
69
|
+
// without pointing at any real host.
|
|
70
|
+
const STAMPED_DOWNLOAD_BASE_URL = "https://cli-assets.nebula-dev.ai/dev";
|
|
71
|
+
const DOWNLOAD_BASE_URL =
|
|
72
|
+
process.env.NEBULA_AI_DOWNLOAD_BASE ?? STAMPED_DOWNLOAD_BASE_URL;
|
|
73
|
+
|
|
74
|
+
const SUPPORTED_PLATFORMS = new Set([
|
|
75
|
+
"darwin-arm64",
|
|
76
|
+
"darwin-x64",
|
|
77
|
+
"linux-x64",
|
|
78
|
+
"linux-arm64",
|
|
79
|
+
"win32-x64",
|
|
80
|
+
]);
|
|
81
|
+
|
|
82
|
+
// ─── pure helpers (exported for tests) ─────────────────────────────────────
|
|
83
|
+
|
|
84
|
+
export function sha256HexSync(buffer) {
|
|
85
|
+
return createHash("sha256").update(buffer).digest("hex");
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Verify a signed checksums.json blob and return the parsed manifest.
|
|
90
|
+
* Throws if the signature is invalid or the manifest is malformed.
|
|
91
|
+
*/
|
|
92
|
+
export function verifyManifest(manifestBytes, signatureBase64, publicKeyPem) {
|
|
93
|
+
const pubKey = createPublicKey(publicKeyPem);
|
|
94
|
+
if (pubKey.asymmetricKeyType !== "ed25519") {
|
|
95
|
+
throw new Error(
|
|
96
|
+
`embedded public key is not ed25519 (got ${pubKey.asymmetricKeyType})`,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
const sig = Buffer.from(signatureBase64.trim(), "base64");
|
|
100
|
+
const ok = cryptoVerify(null, manifestBytes, pubKey, sig);
|
|
101
|
+
if (!ok) {
|
|
102
|
+
throw new Error("manifest signature verification failed");
|
|
103
|
+
}
|
|
104
|
+
const manifest = JSON.parse(manifestBytes.toString("utf8"));
|
|
105
|
+
if (!manifest || typeof manifest !== "object") {
|
|
106
|
+
throw new Error("manifest is not a JSON object");
|
|
107
|
+
}
|
|
108
|
+
if (manifest.algorithm !== "sha256") {
|
|
109
|
+
throw new Error(
|
|
110
|
+
`unsupported manifest algorithm: ${manifest.algorithm ?? "(missing)"}`,
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
if (!manifest.binaries || typeof manifest.binaries !== "object") {
|
|
114
|
+
throw new Error("manifest is missing the `binaries` map");
|
|
115
|
+
}
|
|
116
|
+
return manifest;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Given a verified manifest + a platform key + the bytes of a downloaded
|
|
121
|
+
* binary, confirm the binary's SHA-256 matches the manifest entry. Throws
|
|
122
|
+
* on mismatch.
|
|
123
|
+
*/
|
|
124
|
+
export function assertBinaryMatchesManifest(manifest, platformKey, binName, binaryBuffer) {
|
|
125
|
+
const entryKey = `${platformKey}/${binName}`;
|
|
126
|
+
const expected = manifest.binaries[entryKey];
|
|
127
|
+
if (typeof expected !== "string") {
|
|
128
|
+
throw new Error(`manifest has no entry for ${entryKey}`);
|
|
129
|
+
}
|
|
130
|
+
const actual = sha256HexSync(binaryBuffer);
|
|
131
|
+
if (actual !== expected) {
|
|
132
|
+
throw new Error(
|
|
133
|
+
`binary digest mismatch for ${entryKey}\n expected: ${expected}\n actual: ${actual}`,
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ─── runtime (only runs when this file is the entrypoint) ─────────────────
|
|
139
|
+
|
|
140
|
+
const platformKey = `${process.platform}-${process.arch}`;
|
|
141
|
+
const binName = process.platform === "win32" ? "nebula-ai.exe" : "nebula-ai";
|
|
142
|
+
|
|
143
|
+
async function readVersion() {
|
|
144
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
145
|
+
const pkgPath = join(here, "..", "package.json");
|
|
146
|
+
const raw = await readFile(pkgPath, "utf8");
|
|
147
|
+
const pkg = JSON.parse(raw);
|
|
148
|
+
if (typeof pkg.version !== "string") {
|
|
149
|
+
throw new Error("nebula-ai: package.json is missing a version field");
|
|
150
|
+
}
|
|
151
|
+
return pkg.version;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function getCacheDir(version) {
|
|
155
|
+
// Respect XDG_CACHE_HOME on linux, honor LOCALAPPDATA on windows, default
|
|
156
|
+
// to ~/.cache elsewhere. Keeps behavior predictable across platforms.
|
|
157
|
+
const base =
|
|
158
|
+
process.env.NEBULA_AI_CACHE_DIR ||
|
|
159
|
+
(process.platform === "win32"
|
|
160
|
+
? process.env.LOCALAPPDATA || join(homedir(), "AppData", "Local")
|
|
161
|
+
: process.env.XDG_CACHE_HOME || join(homedir(), ".cache"));
|
|
162
|
+
return join(base, "nebula-ai", `v${version}`, platformKey);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
async function fetchManifest(version) {
|
|
166
|
+
const manifestUrl = `${DOWNLOAD_BASE_URL}/v${version}/checksums.json`;
|
|
167
|
+
const sigUrl = `${DOWNLOAD_BASE_URL}/v${version}/checksums.json.sig`;
|
|
168
|
+
const [manifestRes, sigRes] = await Promise.all([
|
|
169
|
+
fetch(manifestUrl, { redirect: "follow" }),
|
|
170
|
+
fetch(sigUrl, { redirect: "follow" }),
|
|
171
|
+
]);
|
|
172
|
+
if (!manifestRes.ok) {
|
|
173
|
+
throw new Error(
|
|
174
|
+
`manifest download failed: ${manifestRes.status} ${manifestRes.statusText}\n url: ${manifestUrl}`,
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
if (!sigRes.ok) {
|
|
178
|
+
throw new Error(
|
|
179
|
+
`signature download failed: ${sigRes.status} ${sigRes.statusText}\n url: ${sigUrl}`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const manifestBytes = Buffer.from(await manifestRes.arrayBuffer());
|
|
183
|
+
const signatureBase64 = await sigRes.text();
|
|
184
|
+
return verifyManifest(manifestBytes, signatureBase64, EMBEDDED_PUBLIC_KEY_PEM);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
async function downloadBinary(version, destPath) {
|
|
188
|
+
const url = `${DOWNLOAD_BASE_URL}/v${version}/${platformKey}/${binName}`;
|
|
189
|
+
|
|
190
|
+
process.stderr.write(
|
|
191
|
+
`nebula-ai: fetching ${platformKey} v${version} prebuilt binary…\n`,
|
|
192
|
+
);
|
|
193
|
+
|
|
194
|
+
const manifest = await fetchManifest(version);
|
|
195
|
+
|
|
196
|
+
const res = await fetch(url, { redirect: "follow" });
|
|
197
|
+
if (!res.ok || !res.body) {
|
|
198
|
+
throw new Error(
|
|
199
|
+
`download failed: ${res.status} ${res.statusText}\n url: ${url}`,
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
mkdirSync(dirname(destPath), { recursive: true });
|
|
204
|
+
|
|
205
|
+
// Stage under a unique temp name in the SAME directory as destPath so the
|
|
206
|
+
// final renameSync is always an intra-filesystem move. tmpdir() can live on
|
|
207
|
+
// a different mount (tmpfs on linux, user-local tmp on some sandboxes) and
|
|
208
|
+
// rename across mounts fails with EXDEV.
|
|
209
|
+
const tmpPath = join(
|
|
210
|
+
dirname(destPath),
|
|
211
|
+
`.nebula-ai-${version}-${process.pid}-${Date.now()}.partial`,
|
|
212
|
+
);
|
|
213
|
+
try {
|
|
214
|
+
await pipeline(res.body, createWriteStream(tmpPath));
|
|
215
|
+
const tmpBytes = await readFile(tmpPath);
|
|
216
|
+
assertBinaryMatchesManifest(manifest, platformKey, binName, tmpBytes);
|
|
217
|
+
if (process.platform !== "win32") chmodSync(tmpPath, 0o755);
|
|
218
|
+
renameSync(tmpPath, destPath);
|
|
219
|
+
} catch (err) {
|
|
220
|
+
try {
|
|
221
|
+
rmSync(tmpPath, { force: true });
|
|
222
|
+
} catch {
|
|
223
|
+
// best-effort cleanup
|
|
224
|
+
}
|
|
225
|
+
throw err;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
async function main() {
|
|
230
|
+
if (DOWNLOAD_BASE_URL.includes("__NEBULA_AI_DOWNLOAD_BASE__")) {
|
|
231
|
+
console.error(
|
|
232
|
+
"nebula-ai: this build of the wrapper was not stamped with a download base URL.\n" +
|
|
233
|
+
" Set NEBULA_AI_DOWNLOAD_BASE to a reachable mirror, or install\n" +
|
|
234
|
+
" nebula-ai from a dev checkout of https://github.com/thirdweb-dev/nebula-cli.",
|
|
235
|
+
);
|
|
236
|
+
process.exit(1);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (!SUPPORTED_PLATFORMS.has(platformKey)) {
|
|
240
|
+
console.error(
|
|
241
|
+
`nebula-ai: no prebuilt binary for ${platformKey}.\n` +
|
|
242
|
+
`Supported platforms: ${[...SUPPORTED_PLATFORMS].join(", ")}.\n` +
|
|
243
|
+
`File an issue at https://github.com/thirdweb-dev/nebula-cli/issues`,
|
|
244
|
+
);
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
let version;
|
|
249
|
+
try {
|
|
250
|
+
version = await readVersion();
|
|
251
|
+
} catch (err) {
|
|
252
|
+
console.error(`nebula-ai: ${err instanceof Error ? err.message : err}`);
|
|
253
|
+
process.exit(1);
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const cacheDir = getCacheDir(version);
|
|
257
|
+
const binaryPath = join(cacheDir, binName);
|
|
258
|
+
|
|
259
|
+
if (!existsSync(binaryPath)) {
|
|
260
|
+
try {
|
|
261
|
+
await downloadBinary(version, binaryPath);
|
|
262
|
+
} catch (err) {
|
|
263
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
264
|
+
console.error(
|
|
265
|
+
`nebula-ai: failed to install prebuilt binary.\n ${msg}\n` +
|
|
266
|
+
`\nIf your environment blocks outbound HTTPS, set NEBULA_AI_DOWNLOAD_BASE\n` +
|
|
267
|
+
`to a reachable mirror, or install from source.`,
|
|
268
|
+
);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const result = spawnSync(binaryPath, process.argv.slice(2), {
|
|
274
|
+
stdio: "inherit",
|
|
275
|
+
});
|
|
276
|
+
if (result.error) {
|
|
277
|
+
console.error(`nebula-ai: failed to exec binary: ${result.error.message}`);
|
|
278
|
+
process.exit(1);
|
|
279
|
+
}
|
|
280
|
+
process.exit(result.status ?? 0);
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
// Only run main() when this file is the CLI entrypoint, so tests can
|
|
284
|
+
// import the pure helpers above without triggering a download. Use
|
|
285
|
+
// pathToFileURL so the comparison round-trips correctly on Windows
|
|
286
|
+
// (where process.argv[1] uses backslashes but import.meta.url uses
|
|
287
|
+
// forward slashes inside a file:// URL).
|
|
288
|
+
const isEntrypoint =
|
|
289
|
+
process.argv[1] &&
|
|
290
|
+
import.meta.url === pathToFileURL(process.argv[1]).href;
|
|
291
|
+
if (isEntrypoint) {
|
|
292
|
+
main();
|
|
293
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "nebula-ai",
|
|
3
|
+
"version": "0.0.1-dev.20260415044357.874f788",
|
|
4
|
+
"description": "CLI/TUI for the nebula.gg AI task orchestration platform",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"bin": {
|
|
7
|
+
"nebula-ai": "./bin/nebula-ai.js"
|
|
8
|
+
},
|
|
9
|
+
"files": [
|
|
10
|
+
"bin"
|
|
11
|
+
],
|
|
12
|
+
"engines": {
|
|
13
|
+
"node": ">=18.0.0"
|
|
14
|
+
},
|
|
15
|
+
"keywords": [
|
|
16
|
+
"nebula",
|
|
17
|
+
"cli",
|
|
18
|
+
"tui",
|
|
19
|
+
"ai",
|
|
20
|
+
"agents",
|
|
21
|
+
"tasks"
|
|
22
|
+
],
|
|
23
|
+
"license": "MIT",
|
|
24
|
+
"homepage": "https://github.com/thirdweb-dev/nebula-cli#readme",
|
|
25
|
+
"repository": {
|
|
26
|
+
"type": "git",
|
|
27
|
+
"url": "git+https://github.com/thirdweb-dev/nebula-cli.git"
|
|
28
|
+
},
|
|
29
|
+
"bugs": {
|
|
30
|
+
"url": "https://github.com/thirdweb-dev/nebula-cli/issues"
|
|
31
|
+
},
|
|
32
|
+
"publishConfig": {
|
|
33
|
+
"access": "public"
|
|
34
|
+
}
|
|
35
|
+
}
|