@dura-run/cli 0.3.2 → 0.4.0

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
@@ -2262,9 +2262,20 @@ function formatObject(data) {
2262
2262
  }
2263
2263
 
2264
2264
  // src/lib/config-store.ts
2265
- import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
2265
+ import {
2266
+ chmodSync,
2267
+ existsSync,
2268
+ mkdirSync,
2269
+ readFileSync,
2270
+ writeFileSync
2271
+ } from "node:fs";
2266
2272
  import { join } from "node:path";
2267
2273
  import { homedir } from "node:os";
2274
+ function chmodPosix(path, mode) {
2275
+ if (process.platform === "win32")
2276
+ return;
2277
+ chmodSync(path, mode);
2278
+ }
2268
2279
  function getConfigDir() {
2269
2280
  const explicitDir = process.env["DURA_CONFIG_DIR"];
2270
2281
  if (explicitDir)
@@ -2301,12 +2312,15 @@ function readConfig() {
2301
2312
  function writeConfig(config) {
2302
2313
  const dir = getConfigDir();
2303
2314
  if (!existsSync(dir)) {
2304
- mkdirSync(dir, { recursive: true });
2315
+ mkdirSync(dir, { recursive: true, mode: 448 });
2305
2316
  }
2306
- writeFileSync(getConfigPath(), JSON.stringify(config, null, 2) + `
2317
+ chmodPosix(dir, 448);
2318
+ const path = getConfigPath();
2319
+ writeFileSync(path, JSON.stringify(config, null, 2) + `
2307
2320
  `, {
2308
2321
  mode: 384
2309
2322
  });
2323
+ chmodPosix(path, 384);
2310
2324
  }
2311
2325
  function updateConfig(partial) {
2312
2326
  const current = readConfig();
@@ -3003,10 +3017,19 @@ var init_project_id = () => {};
3003
3017
  // src/commands/secrets.ts
3004
3018
  var exports_secrets = {};
3005
3019
  __export(exports_secrets, {
3006
- registerSecretsCommand: () => registerSecretsCommand
3020
+ registerSecretsCommand: () => registerSecretsCommand,
3021
+ escapeEnvValue: () => escapeEnvValue
3007
3022
  });
3008
- import { writeFileSync as writeFileSync3 } from "node:fs";
3023
+ import {
3024
+ writeFileSync as writeFileSync3,
3025
+ existsSync as existsSync4,
3026
+ readFileSync as readFileSync4,
3027
+ appendFileSync
3028
+ } from "node:fs";
3009
3029
  import { join as join4 } from "node:path";
3030
+ function escapeEnvValue(v) {
3031
+ return v.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n");
3032
+ }
3010
3033
  function resolveAuth(opts, output) {
3011
3034
  const projectId = resolveProjectId({ project: opts.project });
3012
3035
  if (!projectId) {
@@ -3066,28 +3089,65 @@ function registerSecretsCommand(program2) {
3066
3089
  reportApiError(output, err, "SECRET_REMOVE_FAILED");
3067
3090
  }
3068
3091
  });
3069
- secrets.command("pull").description("Write secrets to .env.local for local development").option("--project <id>", "Project ID (defaults to dura.json)").option("--api-url <url>", "API base URL").option("--token <token>", "Auth token").option("--dir <path>", "Directory to write .env.local to", ".").action(async (opts, cmd) => {
3092
+ secrets.command("pull").description("Write secrets to .env.local for local development (file holds DECRYPTED plaintext secrets)").option("--project <id>", "Project ID (defaults to dura.json)").option("--confirm", "Overwrite an existing .env.local").option("--api-url <url>", "API base URL").option("--token <token>", "Auth token").option("--dir <path>", "Directory to write .env.local to", ".").action(async (opts, cmd) => {
3070
3093
  const output = getOutput(cmd);
3094
+ const envPath = join4(opts.dir, ".env.local");
3095
+ if (existsSync4(envPath) && !opts.confirm) {
3096
+ output.error("FILE_EXISTS", `${envPath} already exists. Re-run with --confirm to overwrite.`);
3097
+ return;
3098
+ }
3071
3099
  const auth = resolveAuth(opts, output);
3072
3100
  if (!auth)
3073
3101
  return;
3074
3102
  const { client, projectId } = auth;
3075
3103
  try {
3076
3104
  const values = await client.get(`/api/v1/projects/${projectId}/secrets/pull`);
3077
- const lines = Object.entries(values).map(([k, v]) => `${k}="${v.replace(/\\/g, "\\\\").replace(/"/g, "\\\"").replace(/\n/g, "\\n")}"`);
3078
- const envPath = join4(opts.dir, ".env.local");
3105
+ const lines = Object.entries(values).map(([k, v]) => `${k}="${escapeEnvValue(v)}"`);
3079
3106
  writeFileSync3(envPath, lines.join(`
3080
3107
  `) + `
3081
3108
  `, { mode: 384 });
3109
+ let gitignore;
3110
+ try {
3111
+ gitignore = ensureGitignored(opts.dir) ? "added" : "present";
3112
+ } catch {
3113
+ gitignore = "failed";
3114
+ output.warn(`Could not update ${join4(opts.dir, ".gitignore")} — add ".env.local" to it yourself; the file holds plaintext secrets.`);
3115
+ }
3082
3116
  output.success({
3083
3117
  written: envPath,
3084
- count: Object.keys(values).length
3118
+ count: Object.keys(values).length,
3119
+ gitignore
3085
3120
  });
3086
3121
  } catch (err) {
3087
3122
  reportApiError(output, err, "SECRET_PULL_FAILED");
3088
3123
  }
3089
3124
  });
3090
3125
  }
3126
+ function ensureGitignored(dir) {
3127
+ const gitignorePath = join4(dir, ".gitignore");
3128
+ const covers = new Set([
3129
+ ".env.local",
3130
+ "/.env.local",
3131
+ ".env.local/",
3132
+ ".env*",
3133
+ ".env.*",
3134
+ "*.local"
3135
+ ]);
3136
+ let existing = "";
3137
+ if (existsSync4(gitignorePath)) {
3138
+ existing = readFileSync4(gitignorePath, "utf-8");
3139
+ const alreadyCovered = existing.split(`
3140
+ `).map((line) => line.trim()).filter((line) => line.length > 0 && !line.startsWith("#")).some((line) => covers.has(line));
3141
+ if (alreadyCovered)
3142
+ return false;
3143
+ }
3144
+ const prefix = existing.length > 0 && !existing.endsWith(`
3145
+ `) ? `
3146
+ ` : "";
3147
+ appendFileSync(gitignorePath, `${prefix}.env.local
3148
+ `);
3149
+ return true;
3150
+ }
3091
3151
  var init_secrets = __esm(() => {
3092
3152
  init_src3();
3093
3153
  init_api_client();
@@ -3096,6 +3156,9 @@ var init_secrets = __esm(() => {
3096
3156
  init_project_id();
3097
3157
  });
3098
3158
 
3159
+ // ../shared/src/types/queue.ts
3160
+ var init_queue = () => {};
3161
+
3099
3162
  // ../shared/src/constants/defaults.ts
3100
3163
  var DEFAULT_TIMEOUT_MS = 30000, DEFAULT_MEMORY_LIMIT_MB = 128;
3101
3164
  // ../shared/src/constants/billing.ts
@@ -7272,15 +7335,15 @@ var init_marketplace = __esm(() => {
7272
7335
  readme: exports_external.string().max(50000).optional()
7273
7336
  }).strict();
7274
7337
  installAdapterSchema = exports_external.object({
7275
- adapterId: exports_external.string().min(1, "Adapter ID is required"),
7338
+ adapterId: exports_external.string().uuid("Adapter ID must be a valid UUID"),
7276
7339
  config: exports_external.record(exports_external.unknown()).optional()
7277
7340
  });
7278
7341
  searchAdaptersSchema = exports_external.object({
7279
7342
  query: exports_external.string().optional(),
7280
7343
  category: exports_external.string().optional(),
7281
7344
  status: exports_external.enum(["draft", "published", "deprecated"]).optional(),
7282
- limit: exports_external.number().int().positive().max(100).default(20),
7283
- offset: exports_external.number().int().nonnegative().default(0)
7345
+ limit: exports_external.coerce.number().int().positive().max(100).default(20),
7346
+ offset: exports_external.coerce.number().int().nonnegative().default(0)
7284
7347
  });
7285
7348
  });
7286
7349
 
@@ -7787,7 +7850,7 @@ function Queue(initial = []) {
7787
7850
  };
7788
7851
  }
7789
7852
  var queue_default;
7790
- var init_queue = __esm(() => {
7853
+ var init_queue2 = __esm(() => {
7791
7854
  queue_default = Queue;
7792
7855
  });
7793
7856
 
@@ -8565,7 +8628,7 @@ var init_connection = __esm(() => {
8565
8628
  init_types2();
8566
8629
  init_errors2();
8567
8630
  init_result();
8568
- init_queue();
8631
+ init_queue2();
8569
8632
  init_query();
8570
8633
  init_bytes();
8571
8634
  connection_default = Connection;
@@ -9245,7 +9308,7 @@ var init_src = __esm(() => {
9245
9308
  init_types2();
9246
9309
  init_connection();
9247
9310
  init_query();
9248
- init_queue();
9311
+ init_queue2();
9249
9312
  init_errors2();
9250
9313
  init_large();
9251
9314
  Object.assign(Postgres, {
@@ -12789,7 +12852,8 @@ var init_deployments = __esm(() => {
12789
12852
  index("deployments_project_idx").on(table3.projectId),
12790
12853
  index("deployments_project_status_idx").on(table3.projectId, table3.status),
12791
12854
  index("deployments_environment_idx").on(table3.environmentId),
12792
- index("deployments_project_created_idx").on(table3.projectId, desc(table3.createdAt))
12855
+ index("deployments_project_created_idx").on(table3.projectId, desc(table3.createdAt)),
12856
+ uniqueIndex("deployments_one_active_per_project").on(table3.projectId).where(sql`${table3.status} = 'active'`)
12793
12857
  ]);
12794
12858
  });
12795
12859
 
@@ -12894,6 +12958,32 @@ var init_metrics = __esm(() => {
12894
12958
  ]);
12895
12959
  });
12896
12960
 
12961
+ // ../shared/src/db/schema/spans.ts
12962
+ var spans;
12963
+ var init_spans = __esm(() => {
12964
+ init_pg_core();
12965
+ init_execution_records();
12966
+ init_projects();
12967
+ spans = pgTable("spans", {
12968
+ id: uuid("id").defaultRandom().primaryKey(),
12969
+ executionId: uuid("execution_id").notNull().references(() => executionRecords.id, { onDelete: "cascade" }),
12970
+ projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
12971
+ traceId: text("trace_id").notNull(),
12972
+ spanId: text("span_id").notNull(),
12973
+ parentSpanId: text("parent_span_id"),
12974
+ name: text("name").notNull(),
12975
+ kind: text("kind").notNull(),
12976
+ startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
12977
+ endedAt: timestamp("ended_at", { withTimezone: true }).notNull(),
12978
+ attributes: jsonb("attributes").$type(),
12979
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow()
12980
+ }, (table3) => [
12981
+ index("spans_execution_idx").on(table3.executionId),
12982
+ index("spans_project_idx").on(table3.projectId),
12983
+ index("spans_trace_idx").on(table3.traceId)
12984
+ ]);
12985
+ });
12986
+
12897
12987
  // ../shared/src/db/schema/artifacts.ts
12898
12988
  var artifacts;
12899
12989
  var init_artifacts = __esm(() => {
@@ -13091,6 +13181,7 @@ var init_credit_ledger = __esm(() => {
13091
13181
  }, (table3) => [
13092
13182
  index("credit_ledger_org_created_idx").on(table3.organizationId, table3.createdAt),
13093
13183
  uniqueIndex("credit_ledger_execution_debit_dedup").on(sql`((metadata->>'executionId'))`).where(sql`entry_type IN ('execution_debit', 'overage_debit') AND metadata ? 'executionId'`),
13184
+ uniqueIndex("credit_ledger_execution_refund_dedup").on(sql`((metadata->>'executionId'))`).where(sql`entry_type = 'execution_refund' AND metadata ? 'executionId'`),
13094
13185
  uniqueIndex("credit_ledger_included_grant_dedup").on(table3.organizationId, sql`((metadata->>'billingCycle'))`, sql`((metadata->>'plan'))`).where(sql`entry_type = 'included_grant' AND metadata ? 'billingCycle' AND metadata ? 'plan'`)
13095
13186
  ]);
13096
13187
  });
@@ -13412,12 +13503,14 @@ var init_workflows = __esm(() => {
13412
13503
  ]);
13413
13504
  stepRuns = pgTable("step_runs", {
13414
13505
  id: uuid("id").defaultRandom().primaryKey(),
13415
- workflowRunId: uuid("workflow_run_id").notNull().references(() => workflowRuns.id),
13506
+ workflowRunId: uuid("workflow_run_id").notNull().references(() => workflowRuns.id, { onDelete: "cascade" }),
13416
13507
  stepName: text("step_name").notNull(),
13417
13508
  status: text("status").notNull().default("pending"),
13418
13509
  input: jsonb("input"),
13419
13510
  output: jsonb("output"),
13420
- executionId: uuid("execution_id").references(() => executionRecords.id),
13511
+ executionId: uuid("execution_id").references(() => executionRecords.id, {
13512
+ onDelete: "set null"
13513
+ }),
13421
13514
  attempt: integer("attempt").notNull().default(1),
13422
13515
  startedAt: timestamp("started_at", { withTimezone: true }),
13423
13516
  completedAt: timestamp("completed_at", { withTimezone: true }),
@@ -13430,7 +13523,7 @@ var init_workflows = __esm(() => {
13430
13523
  ]);
13431
13524
  approvalRequests = pgTable("approval_requests", {
13432
13525
  id: uuid("id").defaultRandom().primaryKey(),
13433
- stepRunId: uuid("step_run_id").notNull().references(() => stepRuns.id),
13526
+ stepRunId: uuid("step_run_id").notNull().references(() => stepRuns.id, { onDelete: "cascade" }),
13434
13527
  approvers: jsonb("approvers").$type().notNull(),
13435
13528
  message: text("message"),
13436
13529
  status: text("status").notNull().default("pending"),
@@ -13732,6 +13825,27 @@ var init_admin_audit_log = __esm(() => {
13732
13825
  ]);
13733
13826
  });
13734
13827
 
13828
+ // ../shared/src/db/schema/custom-domains.ts
13829
+ var customDomains;
13830
+ var init_custom_domains = __esm(() => {
13831
+ init_pg_core();
13832
+ init_projects();
13833
+ customDomains = pgTable("custom_domains", {
13834
+ id: uuid("id").defaultRandom().primaryKey(),
13835
+ projectId: uuid("project_id").notNull().references(() => projects.id, { onDelete: "cascade" }),
13836
+ domain: text("domain").notNull(),
13837
+ status: text("status").notNull(),
13838
+ txtRecord: text("txt_record").notNull(),
13839
+ verifiedAt: timestamp("verified_at", { withTimezone: true }),
13840
+ tlsProvisionedAt: timestamp("tls_provisioned_at", { withTimezone: true }),
13841
+ createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
13842
+ updatedAt: timestamp("updated_at", { withTimezone: true }).notNull().defaultNow()
13843
+ }, (table3) => [
13844
+ uniqueIndex("custom_domains_domain_idx").on(table3.domain),
13845
+ index("custom_domains_project_idx").on(table3.projectId)
13846
+ ]);
13847
+ });
13848
+
13735
13849
  // ../shared/src/db/schema/index.ts
13736
13850
  var init_schema2 = __esm(() => {
13737
13851
  init_users();
@@ -13744,6 +13858,7 @@ var init_schema2 = __esm(() => {
13744
13858
  init_execution_records();
13745
13859
  init_log_entries();
13746
13860
  init_metrics();
13861
+ init_spans();
13747
13862
  init_artifacts();
13748
13863
  init_secrets2();
13749
13864
  init_api_keys();
@@ -13768,6 +13883,7 @@ var init_schema2 = __esm(() => {
13768
13883
  init_auth();
13769
13884
  init_admin_sessions();
13770
13885
  init_admin_audit_log();
13886
+ init_custom_domains();
13771
13887
  });
13772
13888
 
13773
13889
  // ../shared/src/db/index.ts
@@ -13800,6 +13916,7 @@ function parseHttpBody(body, _headers) {
13800
13916
 
13801
13917
  // ../shared/src/index.ts
13802
13918
  var init_src2 = __esm(() => {
13919
+ init_queue();
13803
13920
  init_billing();
13804
13921
  init_limits();
13805
13922
  init_native_deps();
@@ -13813,16 +13930,16 @@ var init_src2 = __esm(() => {
13813
13930
  });
13814
13931
 
13815
13932
  // src/lib/manifest.ts
13816
- import { existsSync as existsSync4, readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
13933
+ import { existsSync as existsSync5, readFileSync as readFileSync5, writeFileSync as writeFileSync4 } from "node:fs";
13817
13934
  import { join as join5 } from "node:path";
13818
13935
  function readManifest(dir) {
13819
13936
  const path = join5(dir, "dura.json");
13820
- if (!existsSync4(path)) {
13937
+ if (!existsSync5(path)) {
13821
13938
  throw new ManifestError(`No dura.json found in ${dir}`);
13822
13939
  }
13823
13940
  let raw;
13824
13941
  try {
13825
- raw = JSON.parse(readFileSync4(path, "utf-8"));
13942
+ raw = JSON.parse(readFileSync5(path, "utf-8"));
13826
13943
  } catch {
13827
13944
  throw new ManifestError("dura.json is not valid JSON");
13828
13945
  }
@@ -13834,10 +13951,10 @@ function readManifest(dir) {
13834
13951
  }
13835
13952
  function readRawManifest(dir) {
13836
13953
  const path = join5(dir, "dura.json");
13837
- if (!existsSync4(path)) {
13954
+ if (!existsSync5(path)) {
13838
13955
  throw new ManifestError(`No dura.json found in ${dir}`);
13839
13956
  }
13840
- return JSON.parse(readFileSync4(path, "utf-8"));
13957
+ return JSON.parse(readFileSync5(path, "utf-8"));
13841
13958
  }
13842
13959
  function writeRawManifest(dir, data) {
13843
13960
  const path = join5(dir, "dura.json");
@@ -14019,11 +14136,67 @@ function generateDeploymentManifest(_projectDir, manifest) {
14019
14136
  }
14020
14137
  var init_manifest_generator = () => {};
14021
14138
 
14139
+ // src/lib/parity-check.ts
14140
+ import { builtinModules } from "node:module";
14141
+ import * as esbuild from "esbuild";
14142
+ function isNodeBuiltinSpecifier(specifier) {
14143
+ if (specifier.startsWith("node:"))
14144
+ return true;
14145
+ return BARE_BUILTIN_SET.has(specifier);
14146
+ }
14147
+ async function analyzeNodeBuiltinImports(entryPath) {
14148
+ const externalList = [
14149
+ "node:*",
14150
+ ...builtinModules,
14151
+ ...builtinModules.map((m) => `node:${m}`)
14152
+ ];
14153
+ let result;
14154
+ try {
14155
+ result = await esbuild.build({
14156
+ entryPoints: [entryPath],
14157
+ bundle: true,
14158
+ metafile: true,
14159
+ write: false,
14160
+ platform: "neutral",
14161
+ format: "esm",
14162
+ logLevel: "silent",
14163
+ external: externalList
14164
+ });
14165
+ } catch {
14166
+ return [];
14167
+ }
14168
+ const seen = new Set;
14169
+ const hits = [];
14170
+ for (const [importerPath, info] of Object.entries(result.metafile.inputs)) {
14171
+ for (const imp of info.imports) {
14172
+ if (!imp.external)
14173
+ continue;
14174
+ if (!isNodeBuiltinSpecifier(imp.path))
14175
+ continue;
14176
+ const key = `${imp.path}\x00${importerPath}`;
14177
+ if (seen.has(key))
14178
+ continue;
14179
+ seen.add(key);
14180
+ hits.push({ specifier: imp.path, importer: importerPath });
14181
+ }
14182
+ }
14183
+ hits.sort((a, b2) => {
14184
+ if (a.specifier !== b2.specifier)
14185
+ return a.specifier < b2.specifier ? -1 : 1;
14186
+ return a.importer < b2.importer ? -1 : a.importer > b2.importer ? 1 : 0;
14187
+ });
14188
+ return hits;
14189
+ }
14190
+ var BARE_BUILTIN_SET;
14191
+ var init_parity_check = __esm(() => {
14192
+ BARE_BUILTIN_SET = new Set(builtinModules);
14193
+ });
14194
+
14022
14195
  // src/lib/bundler.ts
14023
14196
  import { join as join6, resolve as resolve2 } from "node:path";
14024
14197
  import {
14025
- existsSync as existsSync5,
14026
- readFileSync as readFileSync5,
14198
+ existsSync as existsSync6,
14199
+ readFileSync as readFileSync6,
14027
14200
  writeFileSync as writeFileSync5,
14028
14201
  mkdirSync as mkdirSync3,
14029
14202
  readdirSync,
@@ -14031,7 +14204,7 @@ import {
14031
14204
  } from "node:fs";
14032
14205
  import { createGzip } from "node:zlib";
14033
14206
  import { Readable } from "node:stream";
14034
- import * as esbuild from "esbuild";
14207
+ import * as esbuild2 from "esbuild";
14035
14208
  async function createBundle(projectDir, options) {
14036
14209
  const maxSize = options?.maxBundleSize ?? MAX_BUNDLE_SIZE_BYTES;
14037
14210
  let duraManifest;
@@ -14044,13 +14217,14 @@ async function createBundle(projectDir, options) {
14044
14217
  };
14045
14218
  }
14046
14219
  const buildDir = join6(projectDir, ".dura", "build");
14047
- if (existsSync5(buildDir)) {
14220
+ if (existsSync6(buildDir)) {
14048
14221
  rmSync(buildDir, { recursive: true });
14049
14222
  }
14050
14223
  mkdirSync3(buildDir, { recursive: true });
14224
+ const parityWarnings = [];
14051
14225
  for (const auto of duraManifest.automations) {
14052
14226
  const entryPath = resolve2(projectDir, auto.entrypoint);
14053
- if (!existsSync5(entryPath)) {
14227
+ if (!existsSync6(entryPath)) {
14054
14228
  rmSync(buildDir, { recursive: true });
14055
14229
  return {
14056
14230
  success: false,
@@ -14059,8 +14233,8 @@ async function createBundle(projectDir, options) {
14059
14233
  }
14060
14234
  const outFile = join6(buildDir, `${auto.name}.js`);
14061
14235
  try {
14062
- const source = readFileSync5(entryPath, "utf-8");
14063
- const xformed = await esbuild.transform(source, {
14236
+ const source = readFileSync6(entryPath, "utf-8");
14237
+ const xformed = await esbuild2.transform(source, {
14064
14238
  loader: "ts",
14065
14239
  target: "es2022",
14066
14240
  sourcemap: false
@@ -14073,6 +14247,12 @@ async function createBundle(projectDir, options) {
14073
14247
  error: `Failed to bundle ${auto.name}: ${err instanceof Error ? err.message : String(err)}`
14074
14248
  };
14075
14249
  }
14250
+ try {
14251
+ const hits = await analyzeNodeBuiltinImports(entryPath);
14252
+ if (hits.length > 0) {
14253
+ parityWarnings.push({ automation: auto.name, hits });
14254
+ }
14255
+ } catch {}
14076
14256
  }
14077
14257
  const deployManifest = generateDeploymentManifest(projectDir, duraManifest);
14078
14258
  writeFileSync5(join6(buildDir, "manifest.json"), JSON.stringify(deployManifest, null, 2));
@@ -14089,7 +14269,8 @@ async function createBundle(projectDir, options) {
14089
14269
  success: true,
14090
14270
  archiveBuffer,
14091
14271
  manifest: deployManifest,
14092
- sizeBytes: archiveBuffer.length
14272
+ sizeBytes: archiveBuffer.length,
14273
+ parityWarnings
14093
14274
  };
14094
14275
  }
14095
14276
  async function createTarGz(dir) {
@@ -14123,7 +14304,7 @@ function collectFiles(dir, base) {
14123
14304
  const relativePath = fullPath.slice(base.length + 1);
14124
14305
  result.push({
14125
14306
  name: relativePath,
14126
- content: new Uint8Array(readFileSync5(fullPath))
14307
+ content: new Uint8Array(readFileSync6(fullPath))
14127
14308
  });
14128
14309
  } else if (entry.isDirectory()) {
14129
14310
  result.push(...collectFiles(fullPath, base));
@@ -14181,6 +14362,7 @@ var init_bundler = __esm(() => {
14181
14362
  init_src2();
14182
14363
  init_manifest2();
14183
14364
  init_manifest_generator();
14365
+ init_parity_check();
14184
14366
  });
14185
14367
 
14186
14368
  // src/commands/deploy.ts
@@ -14220,6 +14402,18 @@ function registerDeployCommand(program2) {
14220
14402
  return;
14221
14403
  }
14222
14404
  output.info(`Bundle created: ${bundle.sizeBytes} bytes`);
14405
+ if (bundle.parityWarnings && bundle.parityWarnings.length > 0) {
14406
+ for (const w of bundle.parityWarnings) {
14407
+ const lines = [
14408
+ `"${w.automation}" imports Node built-ins that are not available in the dura.run runtime:`,
14409
+ ...w.hits.map((h) => ` ${h.specifier.padEnd(22)} (imported by ${h.importer})`),
14410
+ `Your handler may fail when invoked in production.`,
14411
+ `See https://dura.run/docs/runtime for the available runtime surface.`
14412
+ ];
14413
+ output.warn(lines.join(`
14414
+ `));
14415
+ }
14416
+ }
14223
14417
  output.info("Deploying...");
14224
14418
  try {
14225
14419
  const result = await client.deployBundle(projectId, bundle.archiveBuffer, bundle.manifest);
@@ -14239,7 +14433,8 @@ function registerDeployCommand(program2) {
14239
14433
  activatedAt: result.deployment.activatedAt,
14240
14434
  bundleSize: bundle.sizeBytes,
14241
14435
  automations: bundle.manifest.automations.length,
14242
- urls: result.urls ?? []
14436
+ urls: result.urls ?? [],
14437
+ parityWarnings: bundle.parityWarnings ?? []
14243
14438
  });
14244
14439
  if (result.urls && result.urls.length > 0) {
14245
14440
  output.info(`
@@ -14337,12 +14532,13 @@ __export(exports_logs, {
14337
14532
  parseFilters: () => parseFilters,
14338
14533
  parseDuration: () => parseDuration,
14339
14534
  formatLogEntry: () => formatLogEntry,
14535
+ fetchWsTicket: () => fetchWsTicket,
14340
14536
  createFollowClient: () => createFollowClient,
14341
14537
  buildWsUrl: () => buildWsUrl
14342
14538
  });
14343
- function buildWsUrl(apiUrl, projectId, filters, token = "") {
14539
+ function buildWsUrl(apiUrl, projectId, filters, ticket = "") {
14344
14540
  const wsBase = apiUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
14345
- const params = new URLSearchParams({ token, projectId });
14541
+ const params = new URLSearchParams({ ticket, projectId });
14346
14542
  if (filters.level)
14347
14543
  params.set("level", filters.level);
14348
14544
  if (filters.executionId)
@@ -14351,6 +14547,10 @@ function buildWsUrl(apiUrl, projectId, filters, token = "") {
14351
14547
  params.set("automationName", filters.automationName);
14352
14548
  return `${wsBase}/api/v1/ws?${params.toString()}`;
14353
14549
  }
14550
+ async function fetchWsTicket(client) {
14551
+ const res = await client.post("/api/v1/logs/ws-ticket");
14552
+ return res.ticket;
14553
+ }
14354
14554
  function formatLogEntry(entry) {
14355
14555
  const fieldsStr = entry.fields && Object.keys(entry.fields).length > 0 ? " " + JSON.stringify(entry.fields) : "";
14356
14556
  return `${entry.timestamp} [${entry.level.toUpperCase()}] ${entry.message}${fieldsStr}`;
@@ -14452,7 +14652,14 @@ function registerLogsCommand(program2) {
14452
14652
  }
14453
14653
  if (executionId)
14454
14654
  filters.executionId = executionId;
14455
- const wsUrl = buildWsUrl(apiUrl, projectId, filters, token);
14655
+ let ticket;
14656
+ try {
14657
+ ticket = await fetchWsTicket(client);
14658
+ } catch (err) {
14659
+ reportApiError(output, err, "WS_TICKET_FAILED");
14660
+ return;
14661
+ }
14662
+ const wsUrl = buildWsUrl(apiUrl, projectId, filters, ticket);
14456
14663
  if (!output.isJson()) {
14457
14664
  output.info(`Connecting to live log stream for project ${projectId}…`);
14458
14665
  }
@@ -14733,9 +14940,20 @@ var init_schedule = __esm(() => {
14733
14940
  });
14734
14941
 
14735
14942
  // src/lib/dev-trust.ts
14736
- import { existsSync as existsSync6, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
14943
+ import { chmodSync as chmodSync2, existsSync as existsSync7, mkdirSync as mkdirSync4, writeFileSync as writeFileSync6 } from "node:fs";
14737
14944
  import { dirname, join as join7 } from "node:path";
14945
+ function chmodPosix2(path, mode) {
14946
+ if (process.platform === "win32")
14947
+ return;
14948
+ chmodSync2(path, mode);
14949
+ }
14738
14950
  function hasStoredCredentials() {
14951
+ const envToken = process.env["DURA_TOKEN"];
14952
+ if (typeof envToken === "string" && envToken.length > 0)
14953
+ return true;
14954
+ const envApiKey = process.env["DURA_API_KEY"];
14955
+ if (typeof envApiKey === "string" && envApiKey.length > 0)
14956
+ return true;
14739
14957
  try {
14740
14958
  const cfg = readConfig();
14741
14959
  const apiKey = typeof cfg.apiKey === "string" ? cfg.apiKey : "";
@@ -14746,14 +14964,15 @@ function hasStoredCredentials() {
14746
14964
  }
14747
14965
  }
14748
14966
  function hasProjectTrustMarker(projectDir) {
14749
- return existsSync6(join7(projectDir, TRUST_MARKER_RELATIVE_PATH));
14967
+ return existsSync7(join7(projectDir, TRUST_MARKER_RELATIVE_PATH));
14750
14968
  }
14751
14969
  function grantProjectTrust(projectDir) {
14752
14970
  const markerPath = join7(projectDir, TRUST_MARKER_RELATIVE_PATH);
14753
14971
  const dir = dirname(markerPath);
14754
- if (!existsSync6(dir)) {
14755
- mkdirSync4(dir, { recursive: true });
14972
+ if (!existsSync7(dir)) {
14973
+ mkdirSync4(dir, { recursive: true, mode: 448 });
14756
14974
  }
14975
+ chmodPosix2(dir, 448);
14757
14976
  const note = [
14758
14977
  "# dura dev trust marker",
14759
14978
  "#",
@@ -14765,6 +14984,7 @@ function grantProjectTrust(projectDir) {
14765
14984
  ].join(`
14766
14985
  `);
14767
14986
  writeFileSync6(markerPath, note, { mode: 384 });
14987
+ chmodPosix2(markerPath, 384);
14768
14988
  }
14769
14989
  function isEnvTruthy(val) {
14770
14990
  if (!val)
@@ -14816,9 +15036,10 @@ function renderTrustRefusal() {
14816
15036
  return [
14817
15037
  `${bold}${yellow}dura dev refuses to start.${reset2}`,
14818
15038
  "",
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.",
15039
+ "Your local machine has stored dura credentials, and `dura dev` loads",
15040
+ "your handler code with full filesystem access. Production isolation",
15041
+ "lives in @dura/executor; dura dev intentionally does not sandbox (see",
15042
+ "#147). You must explicitly accept this risk to start the dev server.",
14822
15043
  "",
14823
15044
  "To proceed, pick ONE of:",
14824
15045
  "",
@@ -15366,7 +15587,7 @@ __export(exports_file_watcher, {
15366
15587
  resolveAffectedAutomation: () => resolveAffectedAutomation,
15367
15588
  createFileWatcher: () => createFileWatcher
15368
15589
  });
15369
- import { watch, existsSync as existsSync7 } from "node:fs";
15590
+ import { watch, existsSync as existsSync8 } from "node:fs";
15370
15591
  import { join as join8 } from "node:path";
15371
15592
  function resolveAffectedAutomation(manifest, changedPath) {
15372
15593
  const normalized = changedPath.replace(/\\/g, "/");
@@ -15403,7 +15624,7 @@ function createFileWatcher(options) {
15403
15624
  watching = true;
15404
15625
  for (const dir of watchDirs) {
15405
15626
  const fullDir = join8(projectDir, dir);
15406
- if (!existsSync7(fullDir))
15627
+ if (!existsSync8(fullDir))
15407
15628
  continue;
15408
15629
  try {
15409
15630
  const fsWatcher = watch(fullDir, { recursive: true }, (_eventType, filename) => {
@@ -15454,7 +15675,10 @@ function parseEnvFile(content) {
15454
15675
  }
15455
15676
  const key = line3.slice(0, eqIndex).trim();
15456
15677
  let value = line3.slice(eqIndex + 1).trim();
15457
- if (value.startsWith('"') && value.endsWith('"') || value.startsWith("'") && value.endsWith("'")) {
15678
+ if (value.length >= 2 && value.startsWith('"') && value.endsWith('"')) {
15679
+ value = value.slice(1, -1).replace(/\\(.)/g, (_, c) => c === "n" ? `
15680
+ ` : c);
15681
+ } else if (value.length >= 2 && value.startsWith("'") && value.endsWith("'")) {
15458
15682
  value = value.slice(1, -1);
15459
15683
  }
15460
15684
  if (key.length > 0) {
@@ -15515,12 +15739,12 @@ function registerDevCommand(program2) {
15515
15739
  }
15516
15740
  throw err;
15517
15741
  }
15518
- const { existsSync: existsSync8, readFileSync: readFileSync6 } = await import("node:fs");
15742
+ const { existsSync: existsSync9, readFileSync: readFileSync7 } = await import("node:fs");
15519
15743
  const { join: join9 } = await import("node:path");
15520
15744
  const envPath = join9(projectDir, ".env.local");
15521
15745
  let secrets2 = {};
15522
- if (existsSync8(envPath)) {
15523
- const content = readFileSync6(envPath, "utf-8");
15746
+ if (existsSync9(envPath)) {
15747
+ const content = readFileSync7(envPath, "utf-8");
15524
15748
  secrets2 = parseEnvFile(content);
15525
15749
  output.info(`Loaded ${Object.keys(secrets2).length} secret(s) from .env.local`);
15526
15750
  }
@@ -15871,9 +16095,9 @@ var init_comparator = __esm(() => {
15871
16095
 
15872
16096
  // src/snapshot/file-manager.ts
15873
16097
  import {
15874
- existsSync as existsSync8,
16098
+ existsSync as existsSync9,
15875
16099
  mkdirSync as mkdirSync5,
15876
- readFileSync as readFileSync6,
16100
+ readFileSync as readFileSync7,
15877
16101
  writeFileSync as writeFileSync7,
15878
16102
  readdirSync as readdirSync2,
15879
16103
  unlinkSync
@@ -15895,16 +16119,16 @@ class SnapshotFileManager {
15895
16119
  return join9(this.snapshotsDir, automationNameToFileName(automationName));
15896
16120
  }
15897
16121
  ensureDir() {
15898
- if (!existsSync8(this.snapshotsDir)) {
16122
+ if (!existsSync9(this.snapshotsDir)) {
15899
16123
  mkdirSync5(this.snapshotsDir, { recursive: true });
15900
16124
  }
15901
16125
  }
15902
16126
  read(automationName) {
15903
16127
  const path = this.filePath(automationName);
15904
- if (!existsSync8(path))
16128
+ if (!existsSync9(path))
15905
16129
  return null;
15906
16130
  try {
15907
- const raw = readFileSync6(path, "utf-8");
16131
+ const raw = readFileSync7(path, "utf-8");
15908
16132
  return JSON.parse(raw);
15909
16133
  } catch {
15910
16134
  return null;
@@ -15934,14 +16158,14 @@ class SnapshotFileManager {
15934
16158
  });
15935
16159
  }
15936
16160
  listAutomationNames() {
15937
- if (!existsSync8(this.snapshotsDir))
16161
+ if (!existsSync9(this.snapshotsDir))
15938
16162
  return [];
15939
16163
  const files = readdirSync2(this.snapshotsDir).filter((f) => f.endsWith(".snap.json"));
15940
16164
  const names = [];
15941
16165
  for (const file of files) {
15942
16166
  const path = join9(this.snapshotsDir, file);
15943
16167
  try {
15944
- const raw = readFileSync6(path, "utf-8");
16168
+ const raw = readFileSync7(path, "utf-8");
15945
16169
  const parsed = JSON.parse(raw);
15946
16170
  if (parsed.automationName) {
15947
16171
  names.push(parsed.automationName);
@@ -15952,7 +16176,7 @@ class SnapshotFileManager {
15952
16176
  }
15953
16177
  delete(automationName) {
15954
16178
  const path = this.filePath(automationName);
15955
- if (existsSync8(path)) {
16179
+ if (existsSync9(path)) {
15956
16180
  unlinkSync(path);
15957
16181
  }
15958
16182
  }
@@ -15961,7 +16185,7 @@ var SNAPSHOTS_DIR = "__snapshots__";
15961
16185
  var init_file_manager = () => {};
15962
16186
 
15963
16187
  // src/snapshot/config-reader.ts
15964
- import { existsSync as existsSync9, readFileSync as readFileSync7 } from "node:fs";
16188
+ import { existsSync as existsSync10, readFileSync as readFileSync8 } from "node:fs";
15965
16189
  import { join as join10 } from "node:path";
15966
16190
  function readSnapshotConfig(projectDir) {
15967
16191
  const durajsonPath = join10(projectDir, "dura.json");
@@ -15969,17 +16193,17 @@ function readSnapshotConfig(projectDir) {
15969
16193
  let fromDuraJson = {};
15970
16194
  let fromTestJson = {};
15971
16195
  let mocks;
15972
- if (existsSync9(durajsonPath)) {
16196
+ if (existsSync10(durajsonPath)) {
15973
16197
  try {
15974
- const raw = JSON.parse(readFileSync7(durajsonPath, "utf-8"));
16198
+ const raw = JSON.parse(readFileSync8(durajsonPath, "utf-8"));
15975
16199
  if (raw["snapshots"] && typeof raw["snapshots"] === "object" && !Array.isArray(raw["snapshots"])) {
15976
16200
  fromDuraJson = raw["snapshots"];
15977
16201
  }
15978
16202
  } catch {}
15979
16203
  }
15980
- if (existsSync9(testjsonPath)) {
16204
+ if (existsSync10(testjsonPath)) {
15981
16205
  try {
15982
- const raw = JSON.parse(readFileSync7(testjsonPath, "utf-8"));
16206
+ const raw = JSON.parse(readFileSync8(testjsonPath, "utf-8"));
15983
16207
  if (raw["snapshots"] && typeof raw["snapshots"] === "object" && !Array.isArray(raw["snapshots"])) {
15984
16208
  fromTestJson = raw["snapshots"];
15985
16209
  }
@@ -16729,7 +16953,7 @@ __export(exports_events, {
16729
16953
  resolvePayload: () => resolvePayload,
16730
16954
  registerEventsCommand: () => registerEventsCommand
16731
16955
  });
16732
- import { readFileSync as readFileSync8 } from "node:fs";
16956
+ import { readFileSync as readFileSync9 } from "node:fs";
16733
16957
  function resolvePayload(opts) {
16734
16958
  if (opts.payload !== undefined && opts.payloadFile !== undefined) {
16735
16959
  throw new Error("Use either --payload or --payload-file, not both");
@@ -16737,7 +16961,7 @@ function resolvePayload(opts) {
16737
16961
  if (opts.payload === undefined && opts.payloadFile === undefined) {
16738
16962
  throw new Error("--payload or --payload-file is required");
16739
16963
  }
16740
- const raw = opts.payload !== undefined ? opts.payload : readFileSync8(opts.payloadFile, "utf-8");
16964
+ const raw = opts.payload !== undefined ? opts.payload : readFileSync9(opts.payloadFile, "utf-8");
16741
16965
  try {
16742
16966
  return JSON.parse(raw);
16743
16967
  } catch (err) {
@@ -17023,8 +17247,8 @@ __export(exports_export, {
17023
17247
  collectExportFiles: () => collectExportFiles
17024
17248
  });
17025
17249
  import {
17026
- existsSync as existsSync10,
17027
- readFileSync as readFileSync9,
17250
+ existsSync as existsSync11,
17251
+ readFileSync as readFileSync10,
17028
17252
  readdirSync as readdirSync3,
17029
17253
  statSync,
17030
17254
  writeFileSync as writeFileSync8
@@ -17044,7 +17268,7 @@ function collectExportFiles(baseDir, currentDir) {
17044
17268
  }
17045
17269
  } else if (stat.isFile()) {
17046
17270
  if (!EXCLUDED_FILES.has(item)) {
17047
- const content = readFileSync9(fullPath, "utf-8");
17271
+ const content = readFileSync10(fullPath, "utf-8");
17048
17272
  entries.push({ relativePath: relPath, content });
17049
17273
  }
17050
17274
  }
@@ -17069,13 +17293,13 @@ function registerExportCommand(program2) {
17069
17293
  const output = getOutput(cmd);
17070
17294
  const projectDir = resolve4(opts.dir ?? ".");
17071
17295
  const manifestPath = join11(projectDir, "dura.json");
17072
- if (!existsSync10(manifestPath)) {
17296
+ if (!existsSync11(manifestPath)) {
17073
17297
  output.error("EXPORT_NO_MANIFEST", "No dura.json found in project directory", "Run this command from a dura project directory or use --dir");
17074
17298
  return;
17075
17299
  }
17076
17300
  let projectName;
17077
17301
  try {
17078
- const manifestContent = readFileSync9(manifestPath, "utf-8");
17302
+ const manifestContent = readFileSync10(manifestPath, "utf-8");
17079
17303
  const manifest = JSON.parse(manifestContent);
17080
17304
  projectName = manifest.name ?? "unnamed-project";
17081
17305
  } catch {
@@ -17942,17 +18166,18 @@ var init_diagnose = __esm(() => {
17942
18166
  // src/commands/create.ts
17943
18167
  var exports_create = {};
17944
18168
  __export(exports_create, {
18169
+ writeGeneratedFiles: () => writeGeneratedFiles,
17945
18170
  registerCreateCommand: () => registerCreateCommand,
17946
18171
  registerAddCommand: () => registerAddCommand
17947
18172
  });
17948
18173
  import {
17949
- existsSync as existsSync11,
18174
+ existsSync as existsSync12,
17950
18175
  mkdirSync as mkdirSync6,
17951
18176
  writeFileSync as writeFileSync9,
17952
- readFileSync as readFileSync10,
18177
+ readFileSync as readFileSync11,
17953
18178
  readdirSync as readdirSync4
17954
18179
  } from "node:fs";
17955
- import { join as join12, resolve as resolve5, dirname as dirname2 } from "node:path";
18180
+ import { join as join12, resolve as resolve5, dirname as dirname2, isAbsolute as isAbsolute2, sep } from "node:path";
17956
18181
  function slugifyDescription(description) {
17957
18182
  return description.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 30).replace(/-+$/, "");
17958
18183
  }
@@ -17960,13 +18185,13 @@ function getExistingRoutes(projectDir) {
17960
18185
  const routesDir = join12(projectDir, "routes");
17961
18186
  const jobsDir = join12(projectDir, "jobs");
17962
18187
  const routes = [];
17963
- if (existsSync11(routesDir)) {
18188
+ if (existsSync12(routesDir)) {
17964
18189
  try {
17965
18190
  const files = readdirSync4(routesDir);
17966
18191
  routes.push(...files.map((f) => `routes/${f}`));
17967
18192
  } catch {}
17968
18193
  }
17969
- if (existsSync11(jobsDir)) {
18194
+ if (existsSync12(jobsDir)) {
17970
18195
  try {
17971
18196
  const files = readdirSync4(jobsDir);
17972
18197
  routes.push(...files.map((f) => `jobs/${f}`));
@@ -17975,13 +18200,26 @@ function getExistingRoutes(projectDir) {
17975
18200
  return routes;
17976
18201
  }
17977
18202
  function writeGeneratedFiles(projectDir, files) {
17978
- for (const file of files) {
17979
- const filePath = join12(projectDir, file.path);
17980
- const dir = dirname2(filePath);
17981
- if (!existsSync11(dir)) {
18203
+ const root = resolve5(projectDir);
18204
+ const resolved = files.map((file) => {
18205
+ if (isAbsolute2(file.path)) {
18206
+ throw new Error(`Refusing to write generated file with absolute path "${file.path}" — paths must be relative to the project directory.`);
18207
+ }
18208
+ if (file.path.split(/[\\/]/).includes("..")) {
18209
+ throw new Error(`Refusing to write generated file "${file.path}" — path traversal ("..") is not allowed.`);
18210
+ }
18211
+ const abs = resolve5(root, file.path);
18212
+ if (abs !== root && !abs.startsWith(root + sep)) {
18213
+ throw new Error(`Refusing to write generated file "${file.path}" — resolved path "${abs}" is outside the project directory.`);
18214
+ }
18215
+ return { abs, content: file.content };
18216
+ });
18217
+ for (const { abs, content } of resolved) {
18218
+ const dir = dirname2(abs);
18219
+ if (!existsSync12(dir)) {
17982
18220
  mkdirSync6(dir, { recursive: true });
17983
18221
  }
17984
- writeFileSync9(filePath, file.content, "utf-8");
18222
+ writeFileSync9(abs, content, "utf-8");
17985
18223
  }
17986
18224
  }
17987
18225
  async function confirm(question) {
@@ -18011,7 +18249,7 @@ function registerCreateCommand(program2) {
18011
18249
  const projectName = opts.name || slugifyDescription(description);
18012
18250
  const parentDir = opts.dir ? resolve5(opts.dir) : process.cwd();
18013
18251
  const projectDir = join12(parentDir, projectName);
18014
- if (existsSync11(projectDir)) {
18252
+ if (existsSync12(projectDir)) {
18015
18253
  output.error("DIRECTORY_EXISTS", `Directory "${projectName}" already exists`, "Choose a different name with --name or remove the existing directory");
18016
18254
  return;
18017
18255
  }
@@ -18038,7 +18276,12 @@ function registerCreateCommand(program2) {
18038
18276
  }
18039
18277
  mkdirSync6(join12(projectDir, "routes"), { recursive: true });
18040
18278
  mkdirSync6(join12(projectDir, "jobs"), { recursive: true });
18041
- writeGeneratedFiles(projectDir, generateResult.files);
18279
+ try {
18280
+ writeGeneratedFiles(projectDir, generateResult.files);
18281
+ } catch (err) {
18282
+ output.error("UNSAFE_GENERATED_PATH", err instanceof Error ? err.message : "Unsafe generated file path", "The AI response contained a file path outside the project directory and was rejected.");
18283
+ return;
18284
+ }
18042
18285
  output.info(`Files written to ${projectDir}`);
18043
18286
  if (opts.deploy !== false) {
18044
18287
  output.info("Deploying...");
@@ -18069,14 +18312,14 @@ function registerAddCommand(program2) {
18069
18312
  }
18070
18313
  const projectDir = opts.dir ? resolve5(opts.dir) : process.cwd();
18071
18314
  const duraJsonPath = join12(projectDir, "dura.json");
18072
- if (!existsSync11(duraJsonPath)) {
18315
+ if (!existsSync12(duraJsonPath)) {
18073
18316
  output.error("NOT_A_DURA_PROJECT", "No dura.json found in the current directory", "Run this command from a dura project directory or use --dir");
18074
18317
  return;
18075
18318
  }
18076
18319
  const existingRoutes = getExistingRoutes(projectDir);
18077
18320
  let projectConfig = {};
18078
18321
  try {
18079
- projectConfig = JSON.parse(readFileSync10(duraJsonPath, "utf-8"));
18322
+ projectConfig = JSON.parse(readFileSync11(duraJsonPath, "utf-8"));
18080
18323
  } catch {}
18081
18324
  const apiUrl = getApiUrl();
18082
18325
  const client = new ApiClient(apiUrl, token);
@@ -18105,7 +18348,12 @@ function registerAddCommand(program2) {
18105
18348
  return;
18106
18349
  }
18107
18350
  }
18108
- writeGeneratedFiles(projectDir, generateResult.files);
18351
+ try {
18352
+ writeGeneratedFiles(projectDir, generateResult.files);
18353
+ } catch (err) {
18354
+ output.error("UNSAFE_GENERATED_PATH", err instanceof Error ? err.message : "Unsafe generated file path", "The AI response contained a file path outside the project directory and was rejected.");
18355
+ return;
18356
+ }
18109
18357
  output.info(`Files written to ${projectDir}`);
18110
18358
  if (opts.deploy !== false) {
18111
18359
  output.info("Run `dura deploy --project " + projectId + " --dir " + projectDir + "` to deploy");
@@ -19170,7 +19418,7 @@ var init_heal = __esm(() => {
19170
19418
  });
19171
19419
 
19172
19420
  // src/lib/skill-installer.ts
19173
- import { existsSync as existsSync12, mkdirSync as mkdirSync7, readFileSync as readFileSync11, writeFileSync as writeFileSync10 } from "node:fs";
19421
+ import { existsSync as existsSync13, mkdirSync as mkdirSync7, readFileSync as readFileSync12, writeFileSync as writeFileSync10 } from "node:fs";
19174
19422
  import { join as join13, dirname as dirname3 } from "node:path";
19175
19423
  import { homedir as homedir2 } from "node:os";
19176
19424
  import { fileURLToPath } from "node:url";
@@ -19186,14 +19434,14 @@ function getLocalSkillsDir(projectDir) {
19186
19434
  }
19187
19435
  function installSkills(targetDir) {
19188
19436
  const sourceDir = getSkillSourceDir();
19189
- if (!existsSync12(targetDir)) {
19437
+ if (!existsSync13(targetDir)) {
19190
19438
  mkdirSync7(targetDir, { recursive: true });
19191
19439
  }
19192
19440
  const installedFiles = [];
19193
19441
  for (const file of SKILL_FILES) {
19194
19442
  const sourcePath = join13(sourceDir, file);
19195
19443
  const targetPath = join13(targetDir, file);
19196
- const content = readFileSync11(sourcePath, "utf-8");
19444
+ const content = readFileSync12(sourcePath, "utf-8");
19197
19445
  writeFileSync10(targetPath, content, "utf-8");
19198
19446
  installedFiles.push(targetPath);
19199
19447
  }
@@ -19218,7 +19466,7 @@ var exports_init = {};
19218
19466
  __export(exports_init, {
19219
19467
  registerInitCommand: () => registerInitCommand
19220
19468
  });
19221
- import { existsSync as existsSync13, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
19469
+ import { existsSync as existsSync14, mkdirSync as mkdirSync8, writeFileSync as writeFileSync11 } from "node:fs";
19222
19470
  import { join as join14, resolve as resolve6, basename } from "node:path";
19223
19471
  function registerInitCommand(program2) {
19224
19472
  program2.command("init").description("Initialize a dura project in the current directory and install AI agent skills").option("--dir <path>", "Project directory (defaults to current dir)").option("--name <name>", "Project name (defaults to directory name)").option("--global", "Install AI agent skills globally (~/.claude/skills/dura/)").option("--local", "Install AI agent skills locally (.claude/skills/dura/)").option("--skip-skills", "Skip AI agent skill installation").action(async (opts, cmd) => {
@@ -19232,24 +19480,24 @@ function registerInitCommand(program2) {
19232
19480
  mkdirSync8(join14(projectDir, "routes"), { recursive: true });
19233
19481
  mkdirSync8(join14(projectDir, "jobs"), { recursive: true });
19234
19482
  const manifestPath = join14(projectDir, "dura.json");
19235
- const alreadyInitialized = existsSync13(manifestPath);
19483
+ const alreadyInitialized = existsSync14(manifestPath);
19236
19484
  if (!alreadyInitialized) {
19237
19485
  writeFileSync11(manifestPath, duraJsonTemplate(projectName));
19238
19486
  } else {
19239
19487
  output.info(`dura.json already exists in ${projectDir} — leaving it in place.`);
19240
19488
  }
19241
19489
  const helloPath = join14(projectDir, "routes", "hello.ts");
19242
- if (existsSync13(helloPath)) {
19490
+ if (existsSync14(helloPath)) {
19243
19491
  output.warn("routes/hello.ts already exists — skipping");
19244
19492
  } else {
19245
19493
  writeFileSync11(helloPath, HELLO_TEMPLATE);
19246
19494
  }
19247
19495
  const gitignorePath = join14(projectDir, ".gitignore");
19248
- if (!existsSync13(gitignorePath)) {
19496
+ if (!existsSync14(gitignorePath)) {
19249
19497
  writeFileSync11(gitignorePath, GITIGNORE_CONTENT2);
19250
19498
  }
19251
19499
  const pkgPath = join14(projectDir, "package.json");
19252
- if (!existsSync13(pkgPath)) {
19500
+ if (!existsSync14(pkgPath)) {
19253
19501
  writeFileSync11(pkgPath, packageJsonTemplate(projectName));
19254
19502
  }
19255
19503
  if (alreadyInitialized) {
@@ -19469,29 +19717,36 @@ async function registerAllCommands(program2) {
19469
19717
  const { registerHealCommand: registerHealCommand2 } = await Promise.resolve().then(() => (init_heal(), exports_heal));
19470
19718
  const { registerInitCommand: registerInitCommand2 } = await Promise.resolve().then(() => (init_init(), exports_init));
19471
19719
  const { registerProjectsCommand: registerProjectsCommand2 } = await Promise.resolve().then(() => (init_projects2(), exports_projects));
19720
+ program2.commandsGroup("Auth & setup:");
19472
19721
  registerLoginCommand2(program2);
19473
19722
  registerLogoutCommand2(program2);
19474
19723
  registerConfigCommand2(program2);
19724
+ program2.commandsGroup("Projects:");
19475
19725
  registerNewCommand2(program2);
19476
19726
  registerInitCommand2(program2);
19477
19727
  registerCreateCommand2(program2);
19478
19728
  registerAddCommand2(program2);
19479
19729
  registerProjectsCommand2(program2);
19730
+ program2.commandsGroup("Templates:");
19480
19731
  registerTemplateCommand2(program2);
19732
+ program2.commandsGroup("Config:");
19481
19733
  registerSecretsCommand2(program2);
19482
19734
  registerDomainsCommand2(program2);
19483
19735
  registerEndpointKeysCommand2(program2);
19484
19736
  registerEnvCommand2(program2);
19737
+ program2.commandsGroup("Deploy lifecycle:");
19485
19738
  registerDeployCommand2(program2);
19486
19739
  registerRollbackCommand2(program2);
19487
19740
  registerDeploymentsCommand2(program2);
19488
19741
  registerPromoteCommand2(program2);
19489
19742
  registerCanaryCommand2(program2);
19743
+ program2.commandsGroup("Run & develop:");
19490
19744
  registerDevCommand2(program2);
19491
19745
  registerTestCommand2(program2);
19492
19746
  registerRunCommand2(program2);
19493
19747
  registerScheduleCommand2(program2);
19494
19748
  registerEventsCommand2(program2);
19749
+ program2.commandsGroup("Observe & debug:");
19495
19750
  registerStatusCommand2(program2);
19496
19751
  registerLogsCommand2(program2);
19497
19752
  registerUsageCommand2(program2);
@@ -19499,6 +19754,7 @@ async function registerAllCommands(program2) {
19499
19754
  registerReplayCommand2(program2);
19500
19755
  registerReplaysCommand2(program2);
19501
19756
  registerDiagnoseCommand2(program2);
19757
+ program2.commandsGroup("Workflows & ops:");
19502
19758
  registerWorkflowsCommand2(program2);
19503
19759
  registerWorkflowCommand2(program2);
19504
19760
  registerApprovalsCommand2(program2);
@@ -19506,14 +19762,16 @@ async function registerAllCommands(program2) {
19506
19762
  registerRejectCommand2(program2);
19507
19763
  registerHealCommand2(program2);
19508
19764
  registerReportsCommand2(program2);
19765
+ program2.commandsGroup("Integrations & storage:");
19509
19766
  registerWebhookCommand2(program2);
19510
19767
  registerMarketplaceCommand2(program2);
19511
19768
  registerKvCommand2(program2);
19769
+ program2.commandsGroup("Export & introspection:");
19512
19770
  registerExportCommand2(program2);
19513
19771
  registerOpenApiCommand2(program2);
19514
19772
  return program2;
19515
19773
  }
19516
- var CLI_VERSION = "0.3.2";
19774
+ var CLI_VERSION = "0.4.0";
19517
19775
  var init_src3 = __esm(() => {
19518
19776
  init_esm();
19519
19777
  if (import.meta.url === `file://${realpathSync(process.argv[1] ?? "").replace(/\\/g, "/")}`) {
@@ -19530,4 +19788,4 @@ export {
19530
19788
  CLI_VERSION
19531
19789
  };
19532
19790
 
19533
- //# debugId=BE2D824A7368E16F64756E2164756E21
19791
+ //# debugId=BACD50D24CC529CE64756E2164756E21
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dura-run/cli",
3
- "version": "0.3.2",
3
+ "version": "0.4.0",
4
4
  "description": "CLI for the dura.run managed automation platform",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -2,6 +2,36 @@
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
+ - `crypto.subtle.*` — full WebCrypto: `digest`, `sign`/`verify`, `encrypt`/`decrypt`, `generateKey`, `importKey`/`exportKey`, `deriveKey`/`deriveBits`, `wrapKey`/`unwrapKey`
14
+ - `TextEncoder` and `TextDecoder` (UTF-8 only)
15
+ - The fetch API (via `dura.fetch`)
16
+ - The `dura.*` SDK surface: `fetch`, `log`, `env`, `kv`, `trigger`, `triggerAndWait`
17
+
18
+ **Note on `crypto.subtle`:** it is bridged to the host runtime, so each call blocks the isolate while it runs. `await`ing a few signs/hashes per request is fine, but `Promise.all([...subtle calls])` executes serially, not concurrently — don't rely on parallelism for throughput, and avoid bulk hashing in tight loops.
19
+
20
+ **Use these replacements for common Node-isms:**
21
+
22
+ | Don't use | Use instead |
23
+ | --------------------- | ------------------------------------------------------------------------------------------ |
24
+ | `node:fs` | `dura.kv` or the artifact APIs |
25
+ | `node:crypto` for IDs | `crypto.randomUUID()` |
26
+ | `node:crypto` for random bytes | `crypto.getRandomValues(new Uint8Array(n))` |
27
+ | `node:crypto` for hashing/signing | `crypto.subtle.digest` / `crypto.subtle.sign` / `crypto.subtle.verify` (WebCrypto) |
28
+ | `Buffer` | `Uint8Array` with `TextEncoder` / `TextDecoder` |
29
+ | `node:child_process` | Not supported — there is no process boundary to use |
30
+ | `node:http` / `https` | `dura.fetch` |
31
+ | `process.env.X` | `dura.env.get("X")` |
32
+
33
+ If `dura deploy` warns about Node built-ins in your bundle, swap to the equivalent above before shipping.
34
+
5
35
  ## Scaffolding a New Project
6
36
 
7
37
  ### `dura new <name>` — Create a New Project