@dura-run/cli 0.3.2 → 0.3.3

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 CHANGED
@@ -45,12 +45,12 @@ All project-scoped commands pick up `projectId` from `dura.json` — you only ne
45
45
 
46
46
  ### `dura dev` runs your handlers in-process (GH #118)
47
47
 
48
- Until we wire isolated-vm into the local runner, `dura dev` loads your
49
- automation code via native `import(...)` in the CLI's Node process. That
50
- process has full access to your filesystem including `~/.dura/config`
51
- (where the CLI stores your API key), `~/.ssh`, `~/.aws`, and environment
52
- variables. A malicious npm dependency anywhere in your handler's import
53
- graph could read those files and exfiltrate them on the first request.
48
+ `dura dev` loads your automation code via native `import(...)` in the
49
+ CLI's Node process. That process has full access to your filesystem —
50
+ including `~/.dura/config` (where the CLI stores your API key), `~/.ssh`,
51
+ `~/.aws`, and environment variables. A malicious npm dependency anywhere
52
+ in your handler's import graph could read those files and exfiltrate
53
+ them on the first request.
54
54
 
55
55
  To make this risk explicit, `dura dev` refuses to start whenever you
56
56
  have credentials stored locally unless you opt in per project:
@@ -77,9 +77,15 @@ Treat `dura dev` with the same trust you'd give `node -e ...` in a
77
77
  project that imports every one of your dependencies — audit your
78
78
  `package.json` (and lockfile) before trusting a new project.
79
79
 
80
- We're tracking a full isolated-vm migration for local dev in
81
- [GH #147](https://github.com/dura-run/dura-run/issues/147) once
82
- that ships, the trust gate will be removed.
80
+ The trust gate is the permanent mitigation. We evaluated a full
81
+ isolated-vm migration for local dev in
82
+ [GH #147](https://github.com/dura-run/dura-run/issues/147) and declined
83
+ it: the threat window is narrow (malicious deps mostly fire at install
84
+ or require-time, which sandboxing `dura dev` wouldn't help with), and
85
+ the engineering cost is high relative to the mitigation. Production
86
+ handlers run in a real V8 isolate via the executor service. See
87
+ [docs/internal/architecture.md](../../docs/internal/architecture.md) for
88
+ the threat-model decision.
83
89
 
84
90
  ## Documentation
85
91
 
package/dist/dura.js CHANGED
@@ -14019,6 +14019,62 @@ function generateDeploymentManifest(_projectDir, manifest) {
14019
14019
  }
14020
14020
  var init_manifest_generator = () => {};
14021
14021
 
14022
+ // src/lib/parity-check.ts
14023
+ import { builtinModules } from "node:module";
14024
+ import * as esbuild from "esbuild";
14025
+ function isNodeBuiltinSpecifier(specifier) {
14026
+ if (specifier.startsWith("node:"))
14027
+ return true;
14028
+ return BARE_BUILTIN_SET.has(specifier);
14029
+ }
14030
+ async function analyzeNodeBuiltinImports(entryPath) {
14031
+ const externalList = [
14032
+ "node:*",
14033
+ ...builtinModules,
14034
+ ...builtinModules.map((m) => `node:${m}`)
14035
+ ];
14036
+ let result;
14037
+ try {
14038
+ result = await esbuild.build({
14039
+ entryPoints: [entryPath],
14040
+ bundle: true,
14041
+ metafile: true,
14042
+ write: false,
14043
+ platform: "neutral",
14044
+ format: "esm",
14045
+ logLevel: "silent",
14046
+ external: externalList
14047
+ });
14048
+ } catch {
14049
+ return [];
14050
+ }
14051
+ const seen = new Set;
14052
+ const hits = [];
14053
+ for (const [importerPath, info] of Object.entries(result.metafile.inputs)) {
14054
+ for (const imp of info.imports) {
14055
+ if (!imp.external)
14056
+ continue;
14057
+ if (!isNodeBuiltinSpecifier(imp.path))
14058
+ continue;
14059
+ const key = `${imp.path}\x00${importerPath}`;
14060
+ if (seen.has(key))
14061
+ continue;
14062
+ seen.add(key);
14063
+ hits.push({ specifier: imp.path, importer: importerPath });
14064
+ }
14065
+ }
14066
+ hits.sort((a, b2) => {
14067
+ if (a.specifier !== b2.specifier)
14068
+ return a.specifier < b2.specifier ? -1 : 1;
14069
+ return a.importer < b2.importer ? -1 : a.importer > b2.importer ? 1 : 0;
14070
+ });
14071
+ return hits;
14072
+ }
14073
+ var BARE_BUILTIN_SET;
14074
+ var init_parity_check = __esm(() => {
14075
+ BARE_BUILTIN_SET = new Set(builtinModules);
14076
+ });
14077
+
14022
14078
  // src/lib/bundler.ts
14023
14079
  import { join as join6, resolve as resolve2 } from "node:path";
14024
14080
  import {
@@ -14031,7 +14087,7 @@ import {
14031
14087
  } from "node:fs";
14032
14088
  import { createGzip } from "node:zlib";
14033
14089
  import { Readable } from "node:stream";
14034
- import * as esbuild from "esbuild";
14090
+ import * as esbuild2 from "esbuild";
14035
14091
  async function createBundle(projectDir, options) {
14036
14092
  const maxSize = options?.maxBundleSize ?? MAX_BUNDLE_SIZE_BYTES;
14037
14093
  let duraManifest;
@@ -14048,6 +14104,7 @@ async function createBundle(projectDir, options) {
14048
14104
  rmSync(buildDir, { recursive: true });
14049
14105
  }
14050
14106
  mkdirSync3(buildDir, { recursive: true });
14107
+ const parityWarnings = [];
14051
14108
  for (const auto of duraManifest.automations) {
14052
14109
  const entryPath = resolve2(projectDir, auto.entrypoint);
14053
14110
  if (!existsSync5(entryPath)) {
@@ -14060,7 +14117,7 @@ async function createBundle(projectDir, options) {
14060
14117
  const outFile = join6(buildDir, `${auto.name}.js`);
14061
14118
  try {
14062
14119
  const source = readFileSync5(entryPath, "utf-8");
14063
- const xformed = await esbuild.transform(source, {
14120
+ const xformed = await esbuild2.transform(source, {
14064
14121
  loader: "ts",
14065
14122
  target: "es2022",
14066
14123
  sourcemap: false
@@ -14073,6 +14130,12 @@ async function createBundle(projectDir, options) {
14073
14130
  error: `Failed to bundle ${auto.name}: ${err instanceof Error ? err.message : String(err)}`
14074
14131
  };
14075
14132
  }
14133
+ try {
14134
+ const hits = await analyzeNodeBuiltinImports(entryPath);
14135
+ if (hits.length > 0) {
14136
+ parityWarnings.push({ automation: auto.name, hits });
14137
+ }
14138
+ } catch {}
14076
14139
  }
14077
14140
  const deployManifest = generateDeploymentManifest(projectDir, duraManifest);
14078
14141
  writeFileSync5(join6(buildDir, "manifest.json"), JSON.stringify(deployManifest, null, 2));
@@ -14089,7 +14152,8 @@ async function createBundle(projectDir, options) {
14089
14152
  success: true,
14090
14153
  archiveBuffer,
14091
14154
  manifest: deployManifest,
14092
- sizeBytes: archiveBuffer.length
14155
+ sizeBytes: archiveBuffer.length,
14156
+ parityWarnings
14093
14157
  };
14094
14158
  }
14095
14159
  async function createTarGz(dir) {
@@ -14181,6 +14245,7 @@ var init_bundler = __esm(() => {
14181
14245
  init_src2();
14182
14246
  init_manifest2();
14183
14247
  init_manifest_generator();
14248
+ init_parity_check();
14184
14249
  });
14185
14250
 
14186
14251
  // src/commands/deploy.ts
@@ -14220,6 +14285,18 @@ function registerDeployCommand(program2) {
14220
14285
  return;
14221
14286
  }
14222
14287
  output.info(`Bundle created: ${bundle.sizeBytes} bytes`);
14288
+ if (bundle.parityWarnings && bundle.parityWarnings.length > 0) {
14289
+ for (const w of bundle.parityWarnings) {
14290
+ const lines = [
14291
+ `"${w.automation}" imports Node built-ins that are not available in the dura.run runtime:`,
14292
+ ...w.hits.map((h) => ` ${h.specifier.padEnd(22)} (imported by ${h.importer})`),
14293
+ `Your handler may fail when invoked in production.`,
14294
+ `See https://dura.run/docs/runtime for the available runtime surface.`
14295
+ ];
14296
+ output.warn(lines.join(`
14297
+ `));
14298
+ }
14299
+ }
14223
14300
  output.info("Deploying...");
14224
14301
  try {
14225
14302
  const result = await client.deployBundle(projectId, bundle.archiveBuffer, bundle.manifest);
@@ -14239,7 +14316,8 @@ function registerDeployCommand(program2) {
14239
14316
  activatedAt: result.deployment.activatedAt,
14240
14317
  bundleSize: bundle.sizeBytes,
14241
14318
  automations: bundle.manifest.automations.length,
14242
- urls: result.urls ?? []
14319
+ urls: result.urls ?? [],
14320
+ parityWarnings: bundle.parityWarnings ?? []
14243
14321
  });
14244
14322
  if (result.urls && result.urls.length > 0) {
14245
14323
  output.info(`
@@ -14816,9 +14894,10 @@ function renderTrustRefusal() {
14816
14894
  return [
14817
14895
  `${bold}${yellow}dura dev refuses to start.${reset2}`,
14818
14896
  "",
14819
- "Your local machine has stored dura credentials, and `dura dev` currently",
14820
- "loads your handler code with full filesystem access. Until we ship an",
14821
- "isolated-vm sandbox for local dev, you must explicitly accept this risk.",
14897
+ "Your local machine has stored dura credentials, and `dura dev` loads",
14898
+ "your handler code with full filesystem access. Production isolation",
14899
+ "lives in @dura/executor; dura dev intentionally does not sandbox (see",
14900
+ "#147). You must explicitly accept this risk to start the dev server.",
14822
14901
  "",
14823
14902
  "To proceed, pick ONE of:",
14824
14903
  "",
@@ -19513,7 +19592,7 @@ async function registerAllCommands(program2) {
19513
19592
  registerOpenApiCommand2(program2);
19514
19593
  return program2;
19515
19594
  }
19516
- var CLI_VERSION = "0.3.2";
19595
+ var CLI_VERSION = "0.3.3";
19517
19596
  var init_src3 = __esm(() => {
19518
19597
  init_esm();
19519
19598
  if (import.meta.url === `file://${realpathSync(process.argv[1] ?? "").replace(/\\/g, "/")}`) {
@@ -19530,4 +19609,4 @@ export {
19530
19609
  CLI_VERSION
19531
19610
  };
19532
19611
 
19533
- //# debugId=BE2D824A7368E16F64756E2164756E21
19612
+ //# debugId=865D5FDBF88E91E164756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dura-run/cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "CLI for the dura.run managed automation platform",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -2,6 +2,37 @@
2
2
 
3
3
  > This skill teaches an AI agent how to scaffold projects, write automation handlers, use SDK globals, and run locally with `dura dev`.
4
4
 
5
+ ## Runtime constraints
6
+
7
+ Handlers run in a V8 isolate on dura.run. **No Node built-ins, no filesystem, no child processes, no native addons.** If you reach for `node:fs`, `Buffer`, `child_process`, or `require('some-native-lib')`, the deploy will warn and the handler will fail at runtime.
8
+
9
+ **What IS available:**
10
+
11
+ - Standard JavaScript globals (`Date`, `Math`, `JSON`, `Promise`, `Uint8Array`, etc.)
12
+ - `crypto.randomUUID()` and `crypto.getRandomValues(typedArray)` (WebCrypto-spec random)
13
+ - `TextEncoder` and `TextDecoder` (UTF-8 only)
14
+ - The fetch API (via `dura.fetch`)
15
+ - The `dura.*` SDK surface: `fetch`, `log`, `env`, `kv`, `trigger`, `triggerAndWait`
16
+
17
+ **What is NOT available yet:**
18
+
19
+ - `crypto.subtle.*` (hashing, signing, key derivation). For these, call a trusted external service via `dura.fetch`.
20
+
21
+ **Use these replacements for common Node-isms:**
22
+
23
+ | Don't use | Use instead |
24
+ | --------------------- | ------------------------------------------------------------------------------------------ |
25
+ | `node:fs` | `dura.kv` or the artifact APIs |
26
+ | `node:crypto` for IDs | `crypto.randomUUID()` |
27
+ | `node:crypto` for random bytes | `crypto.getRandomValues(new Uint8Array(n))` |
28
+ | `node:crypto` for hashing/signing | Not yet supported — call an external service via `dura.fetch` |
29
+ | `Buffer` | `Uint8Array` with `TextEncoder` / `TextDecoder` |
30
+ | `node:child_process` | Not supported — there is no process boundary to use |
31
+ | `node:http` / `https` | `dura.fetch` |
32
+ | `process.env.X` | `dura.env.get("X")` |
33
+
34
+ If `dura deploy` warns about Node built-ins in your bundle, swap to the equivalent above before shipping.
35
+
5
36
  ## Scaffolding a New Project
6
37
 
7
38
  ### `dura new <name>` — Create a New Project