@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 +15 -9
- package/dist/dura.js +353 -95
- package/package.json +1 -1
- package/skills/dura-develop.md +30 -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
|
@@ -2262,9 +2262,20 @@ function formatObject(data) {
|
|
|
2262
2262
|
}
|
|
2263
2263
|
|
|
2264
2264
|
// src/lib/config-store.ts
|
|
2265
|
-
import {
|
|
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
|
-
|
|
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 {
|
|
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
|
|
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().
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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(
|
|
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 (!
|
|
13954
|
+
if (!existsSync5(path)) {
|
|
13838
13955
|
throw new ManifestError(`No dura.json found in ${dir}`);
|
|
13839
13956
|
}
|
|
13840
|
-
return JSON.parse(
|
|
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
|
|
14026
|
-
readFileSync as
|
|
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
|
|
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 (
|
|
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 (!
|
|
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 =
|
|
14063
|
-
const xformed = await
|
|
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(
|
|
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,
|
|
14539
|
+
function buildWsUrl(apiUrl, projectId, filters, ticket = "") {
|
|
14344
14540
|
const wsBase = apiUrl.replace(/^http:\/\//, "ws://").replace(/^https:\/\//, "wss://");
|
|
14345
|
-
const params = new URLSearchParams({
|
|
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
|
-
|
|
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
|
|
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
|
|
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 (!
|
|
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`
|
|
14820
|
-
"
|
|
14821
|
-
"
|
|
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
|
|
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 (!
|
|
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.
|
|
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:
|
|
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 (
|
|
15523
|
-
const content =
|
|
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
|
|
16098
|
+
existsSync as existsSync9,
|
|
15875
16099
|
mkdirSync as mkdirSync5,
|
|
15876
|
-
readFileSync as
|
|
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 (!
|
|
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 (!
|
|
16128
|
+
if (!existsSync9(path))
|
|
15905
16129
|
return null;
|
|
15906
16130
|
try {
|
|
15907
|
-
const raw =
|
|
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 (!
|
|
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 =
|
|
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 (
|
|
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
|
|
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 (
|
|
16196
|
+
if (existsSync10(durajsonPath)) {
|
|
15973
16197
|
try {
|
|
15974
|
-
const raw = JSON.parse(
|
|
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 (
|
|
16204
|
+
if (existsSync10(testjsonPath)) {
|
|
15981
16205
|
try {
|
|
15982
|
-
const raw = JSON.parse(
|
|
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
|
|
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 :
|
|
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
|
|
17027
|
-
readFileSync as
|
|
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 =
|
|
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 (!
|
|
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 =
|
|
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
|
|
18174
|
+
existsSync as existsSync12,
|
|
17950
18175
|
mkdirSync as mkdirSync6,
|
|
17951
18176
|
writeFileSync as writeFileSync9,
|
|
17952
|
-
readFileSync as
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
17979
|
-
|
|
17980
|
-
|
|
17981
|
-
|
|
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(
|
|
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 (
|
|
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
|
-
|
|
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 (!
|
|
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(
|
|
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
|
-
|
|
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
|
|
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 (!
|
|
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 =
|
|
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
|
|
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 =
|
|
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 (
|
|
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 (!
|
|
19496
|
+
if (!existsSync14(gitignorePath)) {
|
|
19249
19497
|
writeFileSync11(gitignorePath, GITIGNORE_CONTENT2);
|
|
19250
19498
|
}
|
|
19251
19499
|
const pkgPath = join14(projectDir, "package.json");
|
|
19252
|
-
if (!
|
|
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.
|
|
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=
|
|
19791
|
+
//# debugId=BACD50D24CC529CE64756E2164756E21
|
package/package.json
CHANGED
package/skills/dura-develop.md
CHANGED
|
@@ -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
|