@dura-run/cli 0.3.1 → 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 +15 -9
- package/dist/dura.js +118 -11
- package/package.json +1 -1
- package/skills/dura-develop.md +31 -0
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
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
81
|
-
|
|
82
|
-
|
|
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
|
@@ -2834,6 +2834,17 @@ var init_hello = __esm(() => {
|
|
|
2834
2834
|
HELLO_TEMPLATE = GET_TEMPLATE;
|
|
2835
2835
|
});
|
|
2836
2836
|
|
|
2837
|
+
// src/templates/package.json.ts
|
|
2838
|
+
function packageJsonTemplate(projectName) {
|
|
2839
|
+
return JSON.stringify({
|
|
2840
|
+
name: projectName,
|
|
2841
|
+
version: "0.0.0",
|
|
2842
|
+
private: true,
|
|
2843
|
+
type: "module"
|
|
2844
|
+
}, null, 2) + `
|
|
2845
|
+
`;
|
|
2846
|
+
}
|
|
2847
|
+
|
|
2837
2848
|
// src/lib/handle-api-error.ts
|
|
2838
2849
|
function reportApiError(output, err, fallbackCode) {
|
|
2839
2850
|
if (err instanceof ApiClientError) {
|
|
@@ -2913,6 +2924,7 @@ Set required secrets: ${result.requiredSecrets.join(", ")}`);
|
|
|
2913
2924
|
mkdirSync2(join2(projectDir, "routes"), { recursive: true });
|
|
2914
2925
|
mkdirSync2(join2(projectDir, "jobs"), { recursive: true });
|
|
2915
2926
|
writeFileSync2(join2(projectDir, "dura.json"), duraJsonTemplate(name, triggerKind));
|
|
2927
|
+
writeFileSync2(join2(projectDir, "package.json"), packageJsonTemplate(name));
|
|
2916
2928
|
writeFileSync2(join2(projectDir, "routes", "hello.ts"), helloTemplate(triggerKind));
|
|
2917
2929
|
writeFileSync2(join2(projectDir, ".gitignore"), GITIGNORE_CONTENT);
|
|
2918
2930
|
const token = getAuthToken();
|
|
@@ -2946,7 +2958,13 @@ Set required secrets: ${result.requiredSecrets.join(", ")}`);
|
|
|
2946
2958
|
created: true,
|
|
2947
2959
|
name,
|
|
2948
2960
|
path: projectDir,
|
|
2949
|
-
files: [
|
|
2961
|
+
files: [
|
|
2962
|
+
"dura.json",
|
|
2963
|
+
"package.json",
|
|
2964
|
+
"routes/hello.ts",
|
|
2965
|
+
"jobs/",
|
|
2966
|
+
".gitignore"
|
|
2967
|
+
]
|
|
2950
2968
|
});
|
|
2951
2969
|
});
|
|
2952
2970
|
}
|
|
@@ -14001,6 +14019,62 @@ function generateDeploymentManifest(_projectDir, manifest) {
|
|
|
14001
14019
|
}
|
|
14002
14020
|
var init_manifest_generator = () => {};
|
|
14003
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
|
+
|
|
14004
14078
|
// src/lib/bundler.ts
|
|
14005
14079
|
import { join as join6, resolve as resolve2 } from "node:path";
|
|
14006
14080
|
import {
|
|
@@ -14013,7 +14087,7 @@ import {
|
|
|
14013
14087
|
} from "node:fs";
|
|
14014
14088
|
import { createGzip } from "node:zlib";
|
|
14015
14089
|
import { Readable } from "node:stream";
|
|
14016
|
-
import * as
|
|
14090
|
+
import * as esbuild2 from "esbuild";
|
|
14017
14091
|
async function createBundle(projectDir, options) {
|
|
14018
14092
|
const maxSize = options?.maxBundleSize ?? MAX_BUNDLE_SIZE_BYTES;
|
|
14019
14093
|
let duraManifest;
|
|
@@ -14030,6 +14104,7 @@ async function createBundle(projectDir, options) {
|
|
|
14030
14104
|
rmSync(buildDir, { recursive: true });
|
|
14031
14105
|
}
|
|
14032
14106
|
mkdirSync3(buildDir, { recursive: true });
|
|
14107
|
+
const parityWarnings = [];
|
|
14033
14108
|
for (const auto of duraManifest.automations) {
|
|
14034
14109
|
const entryPath = resolve2(projectDir, auto.entrypoint);
|
|
14035
14110
|
if (!existsSync5(entryPath)) {
|
|
@@ -14042,7 +14117,7 @@ async function createBundle(projectDir, options) {
|
|
|
14042
14117
|
const outFile = join6(buildDir, `${auto.name}.js`);
|
|
14043
14118
|
try {
|
|
14044
14119
|
const source = readFileSync5(entryPath, "utf-8");
|
|
14045
|
-
const xformed = await
|
|
14120
|
+
const xformed = await esbuild2.transform(source, {
|
|
14046
14121
|
loader: "ts",
|
|
14047
14122
|
target: "es2022",
|
|
14048
14123
|
sourcemap: false
|
|
@@ -14055,6 +14130,12 @@ async function createBundle(projectDir, options) {
|
|
|
14055
14130
|
error: `Failed to bundle ${auto.name}: ${err instanceof Error ? err.message : String(err)}`
|
|
14056
14131
|
};
|
|
14057
14132
|
}
|
|
14133
|
+
try {
|
|
14134
|
+
const hits = await analyzeNodeBuiltinImports(entryPath);
|
|
14135
|
+
if (hits.length > 0) {
|
|
14136
|
+
parityWarnings.push({ automation: auto.name, hits });
|
|
14137
|
+
}
|
|
14138
|
+
} catch {}
|
|
14058
14139
|
}
|
|
14059
14140
|
const deployManifest = generateDeploymentManifest(projectDir, duraManifest);
|
|
14060
14141
|
writeFileSync5(join6(buildDir, "manifest.json"), JSON.stringify(deployManifest, null, 2));
|
|
@@ -14071,7 +14152,8 @@ async function createBundle(projectDir, options) {
|
|
|
14071
14152
|
success: true,
|
|
14072
14153
|
archiveBuffer,
|
|
14073
14154
|
manifest: deployManifest,
|
|
14074
|
-
sizeBytes: archiveBuffer.length
|
|
14155
|
+
sizeBytes: archiveBuffer.length,
|
|
14156
|
+
parityWarnings
|
|
14075
14157
|
};
|
|
14076
14158
|
}
|
|
14077
14159
|
async function createTarGz(dir) {
|
|
@@ -14163,6 +14245,7 @@ var init_bundler = __esm(() => {
|
|
|
14163
14245
|
init_src2();
|
|
14164
14246
|
init_manifest2();
|
|
14165
14247
|
init_manifest_generator();
|
|
14248
|
+
init_parity_check();
|
|
14166
14249
|
});
|
|
14167
14250
|
|
|
14168
14251
|
// src/commands/deploy.ts
|
|
@@ -14202,6 +14285,18 @@ function registerDeployCommand(program2) {
|
|
|
14202
14285
|
return;
|
|
14203
14286
|
}
|
|
14204
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
|
+
}
|
|
14205
14300
|
output.info("Deploying...");
|
|
14206
14301
|
try {
|
|
14207
14302
|
const result = await client.deployBundle(projectId, bundle.archiveBuffer, bundle.manifest);
|
|
@@ -14221,7 +14316,8 @@ function registerDeployCommand(program2) {
|
|
|
14221
14316
|
activatedAt: result.deployment.activatedAt,
|
|
14222
14317
|
bundleSize: bundle.sizeBytes,
|
|
14223
14318
|
automations: bundle.manifest.automations.length,
|
|
14224
|
-
urls: result.urls ?? []
|
|
14319
|
+
urls: result.urls ?? [],
|
|
14320
|
+
parityWarnings: bundle.parityWarnings ?? []
|
|
14225
14321
|
});
|
|
14226
14322
|
if (result.urls && result.urls.length > 0) {
|
|
14227
14323
|
output.info(`
|
|
@@ -14798,9 +14894,10 @@ function renderTrustRefusal() {
|
|
|
14798
14894
|
return [
|
|
14799
14895
|
`${bold}${yellow}dura dev refuses to start.${reset2}`,
|
|
14800
14896
|
"",
|
|
14801
|
-
"Your local machine has stored dura credentials, and `dura dev`
|
|
14802
|
-
"
|
|
14803
|
-
"
|
|
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.",
|
|
14804
14901
|
"",
|
|
14805
14902
|
"To proceed, pick ONE of:",
|
|
14806
14903
|
"",
|
|
@@ -19230,6 +19327,10 @@ function registerInitCommand(program2) {
|
|
|
19230
19327
|
if (!existsSync13(gitignorePath)) {
|
|
19231
19328
|
writeFileSync11(gitignorePath, GITIGNORE_CONTENT2);
|
|
19232
19329
|
}
|
|
19330
|
+
const pkgPath = join14(projectDir, "package.json");
|
|
19331
|
+
if (!existsSync13(pkgPath)) {
|
|
19332
|
+
writeFileSync11(pkgPath, packageJsonTemplate(projectName));
|
|
19333
|
+
}
|
|
19233
19334
|
if (alreadyInitialized) {
|
|
19234
19335
|
output.info(`Re-checked dura project "${projectName}" in ${projectDir}`);
|
|
19235
19336
|
} else {
|
|
@@ -19262,7 +19363,13 @@ function registerInitCommand(program2) {
|
|
|
19262
19363
|
alreadyInitialized,
|
|
19263
19364
|
name: projectName,
|
|
19264
19365
|
path: projectDir,
|
|
19265
|
-
files: [
|
|
19366
|
+
files: [
|
|
19367
|
+
"dura.json",
|
|
19368
|
+
"package.json",
|
|
19369
|
+
"routes/hello.ts",
|
|
19370
|
+
"jobs/",
|
|
19371
|
+
".gitignore"
|
|
19372
|
+
]
|
|
19266
19373
|
});
|
|
19267
19374
|
if (!output.isJson()) {
|
|
19268
19375
|
output.info("");
|
|
@@ -19485,7 +19592,7 @@ async function registerAllCommands(program2) {
|
|
|
19485
19592
|
registerOpenApiCommand2(program2);
|
|
19486
19593
|
return program2;
|
|
19487
19594
|
}
|
|
19488
|
-
var CLI_VERSION = "0.3.
|
|
19595
|
+
var CLI_VERSION = "0.3.3";
|
|
19489
19596
|
var init_src3 = __esm(() => {
|
|
19490
19597
|
init_esm();
|
|
19491
19598
|
if (import.meta.url === `file://${realpathSync(process.argv[1] ?? "").replace(/\\/g, "/")}`) {
|
|
@@ -19502,4 +19609,4 @@ export {
|
|
|
19502
19609
|
CLI_VERSION
|
|
19503
19610
|
};
|
|
19504
19611
|
|
|
19505
|
-
//# debugId=
|
|
19612
|
+
//# debugId=865D5FDBF88E91E164756E2164756E21
|
package/package.json
CHANGED
package/skills/dura-develop.md
CHANGED
|
@@ -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
|