@ouro.bot/cli 0.1.0-alpha.465 → 0.1.0-alpha.467

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.
@@ -212,9 +212,9 @@ exports.COMMAND_REGISTRY = {
212
212
  vault: {
213
213
  category: "Auth",
214
214
  description: "Create, replace, recover, unlock, inspect, and populate the agent credential vault",
215
- usage: "ouro vault <create|replace|recover|unlock|status|config|ops> [--agent <name>]",
215
+ usage: "ouro vault <create|replace|recover|unlock|status|config|item|ops> [--agent <name>]",
216
216
  example: "ouro vault status",
217
- subcommands: ["create", "replace", "recover", "unlock", "status", "config set", "config status", "ops porkbun set", "ops porkbun status"],
217
+ subcommands: ["create", "replace", "recover", "unlock", "status", "config set", "config status", "vault item set", "vault item status", "vault item list", "vault ops porkbun set", "vault ops porkbun status"],
218
218
  },
219
219
  thoughts: {
220
220
  category: "Internal",
@@ -366,13 +366,28 @@ const SUBCOMMAND_HELP = {
366
366
  usage: "ouro vault config status [--agent <name>] [--scope agent|machine|all]",
367
367
  example: "ouro vault config status --scope all",
368
368
  },
369
+ "vault item set": {
370
+ description: "Store an ordinary vault item / credential with no assumed use. Prompts for hidden secret fields, stores optional public fields and notes, and secret values are not printed.",
371
+ usage: "ouro vault item set [--agent <name>] --item <path> (--secret-field <name>...|--template <template>) [--public-field <key=value>] [--note <text>]",
372
+ example: "ouro vault item set --agent slugger --item ops/porkbun/ari@mendelow.me --template porkbun-api",
373
+ },
374
+ "vault item status": {
375
+ description: "Show metadata for an ordinary vault item without printing secret values",
376
+ usage: "ouro vault item status [--agent <name>] --item <path>",
377
+ example: "ouro vault item status --agent slugger --item ops/porkbun/ari@mendelow.me",
378
+ },
379
+ "vault item list": {
380
+ description: "List ordinary vault item names and metadata without printing secret values",
381
+ usage: "ouro vault item list [--agent <name>] [--prefix <path-prefix>]",
382
+ example: "ouro vault item list --agent slugger --prefix ops/",
383
+ },
369
384
  "vault ops porkbun set": {
370
- description: "Store account-scoped Porkbun API credentials as an ops vault item, outside connect/runtime config",
385
+ description: "deprecated compatibility alias for `ouro vault item set --template porkbun-api`; stores an ordinary vault item, not a special credential kind",
371
386
  usage: "ouro vault ops porkbun set [--agent <name>] --account <account>",
372
387
  example: "ouro vault ops porkbun set --agent slugger --account ari@mendelow.me",
373
388
  },
374
389
  "vault ops porkbun status": {
375
- description: "Check whether Porkbun ops credentials are present without printing secret values",
390
+ description: "deprecated compatibility alias for checking the ordinary vault item used by the Porkbun API template",
376
391
  usage: "ouro vault ops porkbun status [--agent <name>] [--account <account>]",
377
392
  example: "ouro vault ops porkbun status --agent slugger --account ari@mendelow.me",
378
393
  },
@@ -16,7 +16,7 @@ exports.parseMcpServeCommand = parseMcpServeCommand;
16
16
  exports.parseOuroCommand = parseOuroCommand;
17
17
  const types_1 = require("../../mind/friends/types");
18
18
  const cli_help_1 = require("./cli-help");
19
- const porkbun_ops_1 = require("./porkbun-ops");
19
+ const vault_items_1 = require("./vault-items");
20
20
  // ── Shared helpers ──
21
21
  function extractAgentFlag(args) {
22
22
  const idx = args.indexOf("--agent");
@@ -96,8 +96,12 @@ function usage() {
96
96
  " ouro vault status [--agent <name>] [--store auto|macos-keychain|windows-dpapi|linux-secret-service|plaintext-file]",
97
97
  " ouro vault config set [--agent <name>] --key <path> [--value <value>] [--scope agent|machine]",
98
98
  " ouro vault config status [--agent <name>] [--scope agent|machine|all]",
99
+ " ouro vault item set [--agent <name>] --item <path> --secret-field <name> [--public-field <key=value>] [--note <text>]",
100
+ " ouro vault item status [--agent <name>] --item <path>",
101
+ " ouro vault item list [--agent <name>] [--prefix <path-prefix>]",
99
102
  " ouro vault ops porkbun set [--agent <name>] --account <account>",
100
103
  " ouro vault ops porkbun status [--agent <name>] [--account <account>]",
104
+ " ouro dns backup|plan|apply|verify|rollback|certificate [--agent <name>] --binding <path> [--output <path>] [--backup <path>] [--yes]",
101
105
  " ouro chat <agent>",
102
106
  " ouro msg --to <agent> [--session <id>] [--task <ref>] <message>",
103
107
  " ouro poke <agent> --task <task-id>",
@@ -461,6 +465,8 @@ function parseVaultCommand(args) {
461
465
  const sub = args[0];
462
466
  if (sub === "config")
463
467
  return parseVaultConfigCommand(args.slice(1));
468
+ if (sub === "item")
469
+ return parseVaultItemCommand(args.slice(1));
464
470
  if (sub === "ops")
465
471
  return parseVaultOpsCommand(args.slice(1));
466
472
  const { agent, rest } = extractAgentFlag(args.slice(1));
@@ -549,6 +555,87 @@ function parseVaultCommand(args) {
549
555
  }
550
556
  return { kind: "vault.status", ...(agent ? { agent } : {}), ...(store ? { store } : {}) };
551
557
  }
558
+ function parseVaultItemCommand(args) {
559
+ const action = args[0];
560
+ if (action !== "set" && action !== "status" && action !== "list") {
561
+ throw new Error("Usage: ouro vault item set|status|list [--agent <name>] --item <path>");
562
+ }
563
+ const { agent, rest } = extractAgentFlag(args.slice(1));
564
+ let item;
565
+ let prefix;
566
+ let template;
567
+ let note;
568
+ const secretFields = [];
569
+ const publicFields = [];
570
+ for (let i = 0; i < rest.length; i += 1) {
571
+ const token = rest[i];
572
+ if (token === "--item") {
573
+ item = (0, vault_items_1.normalizeVaultItemName)(rest[i + 1]);
574
+ i += 1;
575
+ continue;
576
+ }
577
+ if (token === "--prefix") {
578
+ const value = rest[i + 1]?.trim() ?? "";
579
+ if (!value || /[\r\n\t]/.test(value) || value.startsWith("/")) {
580
+ throw new Error("Vault item prefix must be non-empty, relative, and free of control characters.");
581
+ }
582
+ prefix = value;
583
+ i += 1;
584
+ continue;
585
+ }
586
+ if (token === "--template") {
587
+ const value = rest[i + 1];
588
+ if (!(0, vault_items_1.isVaultItemTemplate)(value)) {
589
+ throw new Error("vault item --template must be porkbun-api");
590
+ }
591
+ template = value;
592
+ i += 1;
593
+ continue;
594
+ }
595
+ if (token === "--secret-field") {
596
+ secretFields.push((0, vault_items_1.normalizeVaultItemFieldName)(rest[i + 1]));
597
+ i += 1;
598
+ continue;
599
+ }
600
+ if (token === "--public-field") {
601
+ const value = rest[i + 1]?.trim() ?? "";
602
+ const separator = value.indexOf("=");
603
+ if (separator <= 0 || separator === value.length - 1) {
604
+ throw new Error("vault item --public-field must be key=value");
605
+ }
606
+ (0, vault_items_1.normalizeVaultItemFieldName)(value.slice(0, separator));
607
+ publicFields.push(value);
608
+ i += 1;
609
+ continue;
610
+ }
611
+ if (token === "--note") {
612
+ note = rest[i + 1] ?? "";
613
+ i += 1;
614
+ continue;
615
+ }
616
+ throw new Error(`Usage: ouro vault item ${action} [--agent <name>] --item <path>`);
617
+ }
618
+ if (action === "list") {
619
+ return { kind: "vault.item.list", ...(agent ? { agent } : {}), ...(prefix ? { prefix } : {}) };
620
+ }
621
+ if (!item)
622
+ throw new Error(`Usage: ouro vault item ${action} [--agent <name>] --item <path>`);
623
+ if (action === "status") {
624
+ return { kind: "vault.item.status", ...(agent ? { agent } : {}), item };
625
+ }
626
+ if (!template && secretFields.length === 0) {
627
+ throw new Error("ouro vault item set requires --secret-field or --template");
628
+ }
629
+ return {
630
+ kind: "vault.item.set",
631
+ ...(agent ? { agent } : {}),
632
+ item,
633
+ ...(template ? { template } : {}),
634
+ ...(secretFields.length > 0 ? { secretFields } : {}),
635
+ ...(publicFields.length > 0 ? { publicFields } : {}),
636
+ ...(note !== undefined ? { note } : {}),
637
+ };
638
+ }
552
639
  function parseVaultOpsCommand(args) {
553
640
  const provider = args[0];
554
641
  const action = args[1];
@@ -563,7 +650,7 @@ function parseVaultOpsCommand(args) {
563
650
  for (let i = 0; i < rest.length; i += 1) {
564
651
  const token = rest[i];
565
652
  if (token === "--account") {
566
- account = (0, porkbun_ops_1.normalizePorkbunOpsAccount)(rest[i + 1]);
653
+ account = (0, vault_items_1.normalizePorkbunOpsAccount)(rest[i + 1]);
567
654
  i += 1;
568
655
  continue;
569
656
  }
@@ -572,9 +659,96 @@ function parseVaultOpsCommand(args) {
572
659
  if (action === "set") {
573
660
  if (!account)
574
661
  throw new Error("Usage: ouro vault ops porkbun set [--agent <name>] --account <account>");
575
- return { kind: "vault.ops.porkbun.set", ...(agent ? { agent } : {}), account };
662
+ return {
663
+ kind: "vault.item.set",
664
+ ...(agent ? { agent } : {}),
665
+ item: (0, vault_items_1.porkbunOpsCredentialItemName)(account),
666
+ template: "porkbun-api",
667
+ compatibilityAlias: vault_items_1.PORKBUN_OPS_COMPATIBILITY_ALIAS,
668
+ };
669
+ }
670
+ if (account) {
671
+ return {
672
+ kind: "vault.item.status",
673
+ ...(agent ? { agent } : {}),
674
+ item: (0, vault_items_1.porkbunOpsCredentialItemName)(account),
675
+ compatibilityAlias: vault_items_1.PORKBUN_OPS_COMPATIBILITY_ALIAS,
676
+ };
677
+ }
678
+ return {
679
+ kind: "vault.item.list",
680
+ ...(agent ? { agent } : {}),
681
+ prefix: vault_items_1.PORKBUN_OPS_CREDENTIAL_PREFIX,
682
+ compatibilityAlias: vault_items_1.PORKBUN_OPS_COMPATIBILITY_ALIAS,
683
+ };
684
+ }
685
+ function isDnsWorkflowAction(value) {
686
+ return value === "backup" || value === "plan" || value === "apply" || value === "verify" || value === "rollback" || value === "certificate";
687
+ }
688
+ function normalizeWorkflowPath(value, label) {
689
+ const trimmed = value?.trim() ?? "";
690
+ if (!trimmed || /[\r\n\t]/.test(trimmed)) {
691
+ throw new Error(`${label} must be a non-empty path without control characters.`);
576
692
  }
577
- return { kind: "vault.ops.porkbun.status", ...(agent ? { agent } : {}), ...(account ? { account } : {}) };
693
+ return trimmed;
694
+ }
695
+ function parseDnsCommand(args) {
696
+ const action = args[0];
697
+ if (!isDnsWorkflowAction(action)) {
698
+ throw new Error("Usage: ouro dns backup|plan|apply|verify|rollback|certificate [--agent <name>] --binding <path>");
699
+ }
700
+ const { agent, rest } = extractAgentFlag(args.slice(1));
701
+ let bindingPath;
702
+ let outputPath;
703
+ let backupPath;
704
+ let yes = false;
705
+ for (let i = 0; i < rest.length; i += 1) {
706
+ const token = rest[i];
707
+ if (token === "--binding") {
708
+ bindingPath = normalizeWorkflowPath(rest[i + 1], "dns --binding");
709
+ i += 1;
710
+ continue;
711
+ }
712
+ if (token === "--output") {
713
+ outputPath = normalizeWorkflowPath(rest[i + 1], "dns --output");
714
+ i += 1;
715
+ continue;
716
+ }
717
+ if (token === "--backup") {
718
+ backupPath = normalizeWorkflowPath(rest[i + 1], "dns --backup");
719
+ i += 1;
720
+ continue;
721
+ }
722
+ if (token === "--yes") {
723
+ yes = true;
724
+ continue;
725
+ }
726
+ if (token === "--credential-item") {
727
+ throw new Error("credential item belongs in the DNS workflow binding");
728
+ }
729
+ throw new Error(`Usage: ouro dns ${action} [--agent <name>] --binding <path>`);
730
+ }
731
+ if (!bindingPath) {
732
+ throw new Error(`Usage: ouro dns ${action} [--agent <name>] --binding <path>`);
733
+ }
734
+ if (action === "apply" && !yes) {
735
+ throw new Error("dns apply requires --yes after a reviewed dry-run");
736
+ }
737
+ if (action === "rollback") {
738
+ if (!backupPath)
739
+ throw new Error("dns rollback requires --backup <path>");
740
+ if (!yes)
741
+ throw new Error("dns rollback requires --yes after choosing a backup");
742
+ }
743
+ return {
744
+ kind: "dns.workflow",
745
+ action,
746
+ ...(agent ? { agent } : {}),
747
+ bindingPath,
748
+ ...(outputPath ? { outputPath } : {}),
749
+ ...(backupPath ? { backupPath } : {}),
750
+ ...(yes ? { yes: true } : {}),
751
+ };
578
752
  }
579
753
  function parseVaultConfigCommand(args) {
580
754
  const sub = args[0];
@@ -1225,6 +1399,8 @@ function parseOuroCommand(args) {
1225
1399
  return parseProviderCommand(args.slice(1));
1226
1400
  if (head === "mail")
1227
1401
  return parseMailCommand(args.slice(1));
1402
+ if (head === "dns")
1403
+ return parseDnsCommand(args.slice(1));
1228
1404
  if (head === "logs") {
1229
1405
  if (second === "prune")
1230
1406
  return { kind: "daemon.logs.prune" };
@@ -0,0 +1,365 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.loadDnsWorkflowBinding = loadDnsWorkflowBinding;
4
+ exports.resolveDnsWorkflowSecrets = resolveDnsWorkflowSecrets;
5
+ exports.createPorkbunDnsDriver = createPorkbunDnsDriver;
6
+ exports.planDnsWorkflow = planDnsWorkflow;
7
+ exports.planDnsRollback = planDnsRollback;
8
+ exports.applyDnsWorkflowPlan = applyDnsWorkflowPlan;
9
+ exports.redactDnsWorkflowArtifact = redactDnsWorkflowArtifact;
10
+ const runtime_1 = require("../../nerves/runtime");
11
+ function isRecordType(value) {
12
+ return value === "A" || value === "AAAA" || value === "CNAME" || value === "MX" || value === "TXT";
13
+ }
14
+ function requireString(value, label) {
15
+ if (typeof value !== "string" || value.trim() === "")
16
+ throw new Error(`${label} is required`);
17
+ return value.trim();
18
+ }
19
+ function requireRecordType(value, label) {
20
+ if (!isRecordType(value))
21
+ throw new Error(`${label} must be A, AAAA, CNAME, MX, or TXT`);
22
+ return value;
23
+ }
24
+ function parseCertificateSource(value) {
25
+ if (value === undefined || value === "porkbun-ssl")
26
+ return "porkbun-ssl";
27
+ if (value === "acme-dns-01")
28
+ return "acme-dns-01";
29
+ throw new Error("certificate.source must be porkbun-ssl or acme-dns-01");
30
+ }
31
+ function recordKey(record) {
32
+ return `${record.type}:${record.name}`;
33
+ }
34
+ function parseRecord(input, label) {
35
+ const value = input;
36
+ const record = {
37
+ ...(typeof value.id === "string" ? { id: value.id } : {}),
38
+ type: requireRecordType(value.type, `${label}.type`),
39
+ name: requireString(value.name, `${label}.name`),
40
+ content: requireString(value.content, `${label}.content`),
41
+ ...(typeof value.ttl === "number" ? { ttl: value.ttl } : {}),
42
+ ...(typeof value.priority === "number" ? { priority: value.priority } : {}),
43
+ };
44
+ return record;
45
+ }
46
+ function parseResourceRecord(input, label) {
47
+ const value = input;
48
+ return {
49
+ type: requireRecordType(value.type, `${label}.type`),
50
+ name: requireString(value.name, `${label}.name`),
51
+ };
52
+ }
53
+ function assertNoCredentialOntology(input) {
54
+ if ("credentialItemNoteQuery" in input || "noteQuery" in input || "notes" in input) {
55
+ throw new Error("notes are not machine contracts");
56
+ }
57
+ if ("authority" in input || "kind" in input) {
58
+ throw new Error("workflow binding must not give a vault item assumed use");
59
+ }
60
+ }
61
+ function loadDnsWorkflowBinding(input) {
62
+ if (!input || typeof input !== "object")
63
+ throw new Error("DNS workflow binding must be an object");
64
+ const value = input;
65
+ assertNoCredentialOntology(value);
66
+ if (value.workflow !== "dns")
67
+ throw new Error("DNS workflow binding must set workflow to dns");
68
+ if (value.driver !== "porkbun")
69
+ throw new Error("DNS workflow binding driver must be porkbun");
70
+ const resources = value.resources;
71
+ const desired = value.desired;
72
+ if (!Array.isArray(resources?.records) || resources.records.length === 0) {
73
+ throw new Error("DNS workflow binding requires a resource allowlist");
74
+ }
75
+ if (!Array.isArray(desired?.records))
76
+ throw new Error("DNS workflow binding requires desired records");
77
+ const certificate = value.certificate;
78
+ return {
79
+ workflow: "dns",
80
+ domain: requireString(value.domain, "domain"),
81
+ driver: "porkbun",
82
+ credentialItem: requireString(value.credentialItem, "credentialItem"),
83
+ resources: {
84
+ records: resources.records.map((record, index) => parseResourceRecord(record, `resources.records[${index}]`)),
85
+ },
86
+ desired: {
87
+ records: desired.records.map((record, index) => parseRecord(record, `desired.records[${index}]`)),
88
+ },
89
+ ...(certificate ? {
90
+ certificate: {
91
+ host: requireString(certificate.host, "certificate.host"),
92
+ source: parseCertificateSource(certificate.source),
93
+ storeItem: requireString(certificate.storeItem, "certificate.storeItem"),
94
+ ...(certificate.acmeChallengeRecord
95
+ ? {
96
+ acmeChallengeRecord: parseResourceRecord(certificate.acmeChallengeRecord, "certificate.acmeChallengeRecord"),
97
+ }
98
+ : {}),
99
+ },
100
+ } : {}),
101
+ };
102
+ }
103
+ async function resolveDnsWorkflowSecrets(binding, reader) {
104
+ return {
105
+ apiKey: await reader.readSecretField(binding.credentialItem, "apiKey"),
106
+ secretApiKey: await reader.readSecretField(binding.credentialItem, "secretApiKey"),
107
+ };
108
+ }
109
+ async function readPorkbunJson(response) {
110
+ const payload = await response.json();
111
+ if (!response.ok || payload.status === "ERROR") {
112
+ throw new Error(payload.message ?? `Porkbun request failed with status ${response.status}`);
113
+ }
114
+ return payload;
115
+ }
116
+ function porkbunHeaders(secrets) {
117
+ return {
118
+ "X-API-Key": secrets.apiKey,
119
+ "X-Secret-API-Key": secrets.secretApiKey,
120
+ };
121
+ }
122
+ function porkbunRecordBody(record) {
123
+ return {
124
+ type: record.type,
125
+ name: record.name === "@" ? "" : record.name,
126
+ content: record.content,
127
+ ttl: record.ttl ?? 600,
128
+ prio: record.priority ?? 0,
129
+ };
130
+ }
131
+ function normalizePorkbunRecordName(domain, name) {
132
+ const suffix = `.${domain}`;
133
+ if (name === domain)
134
+ return "@";
135
+ if (name.endsWith(suffix))
136
+ return name.slice(0, -suffix.length);
137
+ return name;
138
+ }
139
+ function normalizePorkbunNumber(value) {
140
+ const parsed = value === null || value === undefined || value === "" ? Number.NaN : Number(value);
141
+ return Number.isFinite(parsed) ? parsed : undefined;
142
+ }
143
+ function normalizePorkbunRecord(domain, input) {
144
+ const value = input;
145
+ const ttl = normalizePorkbunNumber(value.ttl);
146
+ const priority = normalizePorkbunNumber(value.priority ?? value.prio);
147
+ return {
148
+ ...(typeof value.id === "string" ? { id: value.id } : {}),
149
+ type: requireString(value.type, "provider record type"),
150
+ name: normalizePorkbunRecordName(domain, requireString(value.name, "provider record name")),
151
+ content: requireString(value.content, "provider record content"),
152
+ ...(ttl === undefined ? {} : { ttl }),
153
+ ...(priority === undefined ? {} : { priority }),
154
+ };
155
+ }
156
+ async function emitPorkbunRequest(input) {
157
+ (0, runtime_1.emitNervesEvent)({
158
+ event: "daemon.dns_provider_request_start",
159
+ component: "daemon",
160
+ message: `DNS provider ${input.method} ${input.path} started`,
161
+ meta: {
162
+ driver: "porkbun",
163
+ method: input.method,
164
+ path: input.path,
165
+ },
166
+ });
167
+ try {
168
+ const result = await input.execute();
169
+ (0, runtime_1.emitNervesEvent)({
170
+ event: "daemon.dns_provider_request_end",
171
+ component: "daemon",
172
+ message: `DNS provider ${input.method} ${input.path} completed`,
173
+ meta: {
174
+ driver: "porkbun",
175
+ method: input.method,
176
+ path: input.path,
177
+ },
178
+ });
179
+ return result;
180
+ }
181
+ catch (error) {
182
+ (0, runtime_1.emitNervesEvent)({
183
+ level: "error",
184
+ event: "daemon.dns_provider_request_error",
185
+ component: "daemon",
186
+ message: `DNS provider ${input.method} ${input.path} failed`,
187
+ meta: {
188
+ driver: "porkbun",
189
+ method: input.method,
190
+ path: input.path,
191
+ error: String(error),
192
+ },
193
+ });
194
+ throw error;
195
+ }
196
+ }
197
+ function createPorkbunDnsDriver(options) {
198
+ const baseUrl = (options.baseUrl ?? "https://api.porkbun.com/api/json/v3").replace(/\/+$/, "");
199
+ const readOnly = async (path, secrets) => {
200
+ return emitPorkbunRequest({
201
+ method: "GET",
202
+ path,
203
+ execute: async () => readPorkbunJson(await options.fetchImpl(`${baseUrl}${path}`, {
204
+ method: "GET",
205
+ headers: porkbunHeaders(secrets),
206
+ })),
207
+ });
208
+ };
209
+ const mutate = async (path, secrets, body = {}) => {
210
+ return emitPorkbunRequest({
211
+ method: "POST",
212
+ path,
213
+ execute: async () => readPorkbunJson(await options.fetchImpl(`${baseUrl}${path}`, {
214
+ method: "POST",
215
+ headers: {
216
+ ...porkbunHeaders(secrets),
217
+ "Content-Type": "application/json",
218
+ },
219
+ body: JSON.stringify(body),
220
+ })),
221
+ });
222
+ };
223
+ return {
224
+ async ping(secrets) {
225
+ const payload = await readOnly("/ping", secrets);
226
+ return { credentialsValid: payload.credentialsValid === true };
227
+ },
228
+ async retrieveRecords({ domain, secrets }) {
229
+ const payload = await readOnly(`/dns/retrieve/${encodeURIComponent(domain)}`, secrets);
230
+ return (payload.records ?? []).map((record) => normalizePorkbunRecord(domain, record));
231
+ },
232
+ async retrieveCertificate({ domain, secrets }) {
233
+ const payload = await readOnly(`/ssl/retrieve/${encodeURIComponent(domain)}`, secrets);
234
+ return {
235
+ certificatechain: requireString(payload.certificatechain, "certificatechain"),
236
+ publickey: requireString(payload.publickey, "publickey"),
237
+ privatekey: requireString(payload.privatekey, "privatekey"),
238
+ };
239
+ },
240
+ async createRecord({ domain, secrets, record }) {
241
+ const payload = await mutate(`/dns/create/${encodeURIComponent(domain)}`, secrets, porkbunRecordBody(record));
242
+ return typeof payload.id === "string" ? { id: payload.id } : {};
243
+ },
244
+ async editRecord({ domain, secrets, id, record }) {
245
+ await mutate(`/dns/edit/${encodeURIComponent(domain)}/${encodeURIComponent(id)}`, secrets, porkbunRecordBody(record));
246
+ },
247
+ async deleteRecord({ domain, secrets, id }) {
248
+ await mutate(`/dns/delete/${encodeURIComponent(domain)}/${encodeURIComponent(id)}`, secrets);
249
+ },
250
+ };
251
+ }
252
+ function assertDesiredRecordsAllowed(binding) {
253
+ const allowed = new Set(binding.resources.records.map(recordKey));
254
+ for (const desired of binding.desired.records) {
255
+ if (!allowed.has(recordKey(desired)))
256
+ throw new Error("desired DNS record is outside DNS workflow allowlist");
257
+ }
258
+ }
259
+ function recordsEqual(left, right) {
260
+ const priorityEqual = left.type === "MX"
261
+ ? (left.priority ?? 0) === (right.priority ?? 0)
262
+ : true;
263
+ return left.type === right.type &&
264
+ left.name === right.name &&
265
+ left.content === right.content &&
266
+ left.ttl === right.ttl &&
267
+ priorityEqual;
268
+ }
269
+ function planDnsWorkflow(input) {
270
+ assertDesiredRecordsAllowed(input.binding);
271
+ const allowedKeys = new Set(input.binding.resources.records.map(recordKey));
272
+ const desiredKeys = new Set(input.binding.desired.records.map(recordKey));
273
+ const changes = [];
274
+ for (const desired of input.binding.desired.records) {
275
+ const current = input.currentRecords.find((record) => recordKey(record) === recordKey(desired));
276
+ if (!current) {
277
+ changes.push({ action: "create", record: desired, reason: "desired record is missing" });
278
+ }
279
+ else if (!recordsEqual(current, desired)) {
280
+ changes.push({ action: "update", record: desired, currentRecord: current, reason: "desired record differs from current provider record" });
281
+ }
282
+ }
283
+ if (input.deleteExtraAllowedRecords) {
284
+ for (const current of input.currentRecords) {
285
+ if (allowedKeys.has(recordKey(current)) && !desiredKeys.has(recordKey(current))) {
286
+ changes.push({ action: "delete", record: current, currentRecord: current, reason: "allowlisted record is absent from rollback backup" });
287
+ }
288
+ }
289
+ }
290
+ const preservedRecords = input.currentRecords.filter((record) => !desiredKeys.has(recordKey(record)));
291
+ return {
292
+ backup: { domain: input.binding.domain, records: input.currentRecords },
293
+ changes,
294
+ preservedRecords,
295
+ certificateActions: input.binding.certificate
296
+ ? [{
297
+ action: "retrieve-and-store",
298
+ host: input.binding.certificate.host,
299
+ secretItem: input.binding.certificate.storeItem,
300
+ }]
301
+ : [],
302
+ };
303
+ }
304
+ function planDnsRollback(input) {
305
+ const allowedKeys = new Set(input.binding.resources.records.map(recordKey));
306
+ const rollbackBinding = {
307
+ ...input.binding,
308
+ desired: {
309
+ records: input.backupRecords.filter((record) => allowedKeys.has(recordKey(record))),
310
+ },
311
+ };
312
+ return planDnsWorkflow({
313
+ binding: rollbackBinding,
314
+ currentRecords: input.currentRecords,
315
+ deleteExtraAllowedRecords: true,
316
+ });
317
+ }
318
+ async function applyDnsWorkflowPlan(input) {
319
+ const applied = [];
320
+ for (const change of input.plan.changes) {
321
+ if (change.action === "create") {
322
+ const result = await input.driver.createRecord({ domain: input.domain, secrets: input.secrets, record: change.record });
323
+ applied.push({ action: "create", record: change.record, ...(result.id ? { id: result.id } : {}) });
324
+ continue;
325
+ }
326
+ const id = change.currentRecord?.id ?? change.record.id;
327
+ if (!id)
328
+ throw new Error(`cannot ${change.action} ${change.record.type} ${change.record.name} without provider record id`);
329
+ if (change.action === "update") {
330
+ await input.driver.editRecord({ domain: input.domain, secrets: input.secrets, id, record: change.record });
331
+ applied.push({ action: "update", record: change.record, id });
332
+ continue;
333
+ }
334
+ await input.driver.deleteRecord({ domain: input.domain, secrets: input.secrets, id });
335
+ applied.push({ action: "delete", record: change.record, id });
336
+ }
337
+ return applied;
338
+ }
339
+ function redactedValueForKey(key, value) {
340
+ const normalized = key.toLowerCase();
341
+ if (normalized === "apikey" || normalized === "secretapikey" || normalized === "x-api-key" || normalized === "x-secret-api-key") {
342
+ return "[redacted]";
343
+ }
344
+ if (normalized === "privatekey" || normalized === "privatekeypem" || normalized === "privatekeypath") {
345
+ return "[redacted]";
346
+ }
347
+ if (typeof value === "string" && value.includes("BEGIN PRIVATE KEY")) {
348
+ return "[redacted]";
349
+ }
350
+ return undefined;
351
+ }
352
+ function redactDnsWorkflowArtifact(input) {
353
+ if (Array.isArray(input))
354
+ return input.map((item) => redactDnsWorkflowArtifact(item));
355
+ if (input && typeof input === "object") {
356
+ const output = {};
357
+ for (const [key, value] of Object.entries(input)) {
358
+ output[key] = redactedValueForKey(key, value) ?? redactDnsWorkflowArtifact(value);
359
+ }
360
+ return output;
361
+ }
362
+ if (typeof input === "string" && input.includes("BEGIN PRIVATE KEY"))
363
+ return "[redacted]";
364
+ return input;
365
+ }