@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.
- package/README.md +4 -0
- package/changelog.json +16 -0
- package/dist/heart/daemon/cli-exec.js +422 -76
- package/dist/heart/daemon/cli-help.js +19 -4
- package/dist/heart/daemon/cli-parse.js +180 -4
- package/dist/heart/daemon/dns-workflow.js +365 -0
- package/dist/heart/daemon/vault-items.js +56 -0
- package/dist/heart/outlook/readers/mail.js +34 -1
- package/dist/mailroom/attention.js +13 -0
- package/dist/mailroom/core.js +27 -0
- package/dist/mailroom/outbound.js +4 -0
- package/dist/nerves/coverage/file-completeness.js +1 -1
- package/dist/repertoire/tools-credential.js +37 -17
- package/dist/repertoire/tools-mail.js +22 -1
- package/dist/senses/mail.js +33 -25
- package/package.json +1 -1
- package/dist/heart/daemon/porkbun-ops.js +0 -25
|
@@ -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: "
|
|
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: "
|
|
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
|
|
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,
|
|
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 {
|
|
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
|
|
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
|
+
}
|