@ai-ecoverse/slicc-agent 6.11.0 → 6.11.2
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 +16 -6
- package/dist/control.js.map +1 -1
- package/dist/host.js.map +1 -1
- package/dist/providers.js +4 -4
- package/dist/providers.js.map +1 -1
- package/dist/secrets/extension.js +35 -15
- package/dist/secrets/extension.js.map +2 -2
- package/dist/secrets/names.js +175 -26
- package/dist/secrets/names.js.map +3 -3
- package/dist/secrets/view.js +12 -2
- package/dist/secrets/view.js.map +2 -2
- package/dist/spectrum/messages.js +6 -2
- package/dist/spectrum/messages.js.map +2 -2
- package/dist/types/control.d.ts +2 -2
- package/dist/types/host.d.ts +2 -2
- package/dist/types/secrets/extension.d.ts +10 -6
- package/dist/types/secrets/names.d.ts +34 -6
- package/dist/types/secrets/view.d.ts +7 -2
- package/package.json +5 -3
|
@@ -5,11 +5,11 @@ import {
|
|
|
5
5
|
section,
|
|
6
6
|
ToolTask
|
|
7
7
|
} from "@earendil-works/pi-durable";
|
|
8
|
-
import { listedLine, SECRETS_KIND } from "./view.js";
|
|
8
|
+
import { exposedLine, listedLine, SECRETS_KIND, unavailableLine } from "./view.js";
|
|
9
9
|
const SECRETS = "slicc-secrets";
|
|
10
10
|
const SECRETS_COMMAND = "/secrets";
|
|
11
11
|
const SHELL_GRANTS = ["fnox", "exec", "--shell-grants", "--"];
|
|
12
|
-
const MODEL_LINE = "Credentials are env vars (`$NAME`) holding placeholders
|
|
12
|
+
const MODEL_LINE = "Credentials are env vars (`$NAME`) holding placeholders, listed below with where each works (`fnox list` shows the fnox config). The vault fills in the real value only in the listed header, for the listed domain, method and path, over HTTPS (curl, git, fetch). Adding a rule means editing `/etc/fnox/config.toml` with sudo, which the user approves. `fnox exec` gives real values only for secrets the user enabled for exec, and the secrets listed as 'in every bash' already hold real values in your commands. Everything is logged, and stored values are replaced by their placeholder in everything you see; edit around placeholders, since files show them too. Never ask the user to paste a credential into the chat.";
|
|
13
13
|
function leaves(value, out, set) {
|
|
14
14
|
if (typeof value === "string") {
|
|
15
15
|
out.push({ get: () => value, set });
|
|
@@ -72,33 +72,49 @@ async function scrubResult(result, scrub) {
|
|
|
72
72
|
...output === void 0 ? {} : { structuredOutput: output }
|
|
73
73
|
};
|
|
74
74
|
}
|
|
75
|
-
function where(name) {
|
|
75
|
+
function where(name, granted) {
|
|
76
76
|
const rules = name.rules.map(
|
|
77
77
|
(rule) => [
|
|
78
78
|
rule.domain,
|
|
79
79
|
rule.header ? `header ${rule.header}` : "",
|
|
80
80
|
rule.methods?.length ? rule.methods.join(" ") : "",
|
|
81
|
-
rule.paths?.length ? rule.paths.join(" ") : ""
|
|
81
|
+
rule.paths?.length ? rule.paths.join(" ") : "",
|
|
82
|
+
rule.proposed ? "proposed in the fnox config, not yet accepted by the vault" : ""
|
|
82
83
|
].filter(Boolean).join(", ")
|
|
83
84
|
);
|
|
84
|
-
const shell = name.exec === "shell" ? ["in every bash"] : [];
|
|
85
|
-
const
|
|
85
|
+
const shell = name.exec === "shell" && granted ? ["in every bash"] : [];
|
|
86
|
+
const sealed = name.placeholder === null ? ["not sealed"] : [];
|
|
87
|
+
const config = name.fnox ? [] : ["not in the fnox config"];
|
|
88
|
+
const parts = [...rules, ...shell, ...sealed, ...config];
|
|
86
89
|
return parts.length ? `: ${parts.join("; ")}` : "";
|
|
87
90
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
const NO_SECRETS = "No secrets are configured: the fnox config has no keys and the vault no sealed secrets.";
|
|
92
|
+
function renderSecrets(listing) {
|
|
93
|
+
if (listing.error !== null) return unavailableLine(listing.error);
|
|
94
|
+
if (!listing.names.length) return NO_SECRETS;
|
|
95
|
+
const granted = !listing.exposed.length;
|
|
96
|
+
const lines = listing.names.map(
|
|
97
|
+
(name) => `- ${name.name} = ${name.placeholder ?? `$${name.name}`}${where(name, granted)}`
|
|
98
|
+
);
|
|
99
|
+
const off = granted ? [] : ["", exposedLine(listing.exposed)];
|
|
100
|
+
return [MODEL_LINE, "", ...lines, ...off].join("\n");
|
|
92
101
|
}
|
|
93
|
-
function secretsEntry(
|
|
102
|
+
function secretsEntry(listing) {
|
|
94
103
|
return {
|
|
95
104
|
kind: SECRETS_KIND,
|
|
96
|
-
data: {
|
|
97
|
-
|
|
105
|
+
data: {
|
|
106
|
+
names: structuredClone(listing.names),
|
|
107
|
+
error: listing.error,
|
|
108
|
+
exposed: [...listing.exposed]
|
|
109
|
+
},
|
|
110
|
+
model: [{ role: "user", content: listedLine(listing), timestamp: Date.now() }]
|
|
98
111
|
};
|
|
99
112
|
}
|
|
113
|
+
function granting(listing) {
|
|
114
|
+
return listing.error === null && !listing.exposed.length && listing.names.some((name) => name.exec === "shell");
|
|
115
|
+
}
|
|
100
116
|
function shellGrants(names) {
|
|
101
|
-
return (argv) => names.state.value
|
|
117
|
+
return (argv) => granting(names.state.value) ? [...SHELL_GRANTS, ...argv] : argv;
|
|
102
118
|
}
|
|
103
119
|
function secretsExtension(deps) {
|
|
104
120
|
return defineExtension({
|
|
@@ -117,15 +133,19 @@ function secretsExtension(deps) {
|
|
|
117
133
|
}
|
|
118
134
|
export {
|
|
119
135
|
MODEL_LINE,
|
|
136
|
+
NO_SECRETS,
|
|
120
137
|
SECRETS,
|
|
121
138
|
SECRETS_COMMAND,
|
|
122
139
|
SECRETS_KIND,
|
|
123
140
|
SHELL_GRANTS,
|
|
141
|
+
exposedLine,
|
|
142
|
+
granting,
|
|
124
143
|
listedLine,
|
|
125
144
|
renderSecrets,
|
|
126
145
|
scrubResult,
|
|
127
146
|
secretsEntry,
|
|
128
147
|
secretsExtension,
|
|
129
|
-
shellGrants
|
|
148
|
+
shellGrants,
|
|
149
|
+
unavailableLine
|
|
130
150
|
};
|
|
131
151
|
//# sourceMappingURL=extension.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/secrets/extension.ts"],
|
|
4
|
-
"sourcesContent": ["import type { JsonValue } from '@earendil-works/chord';\nimport {\n defineExtension,\n type Extension,\n GenerationTask,\n hook,\n section,\n type ToolExecutionResult,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Names, SecretName } from './names.ts';\nimport type { Scrub } from './scrub.ts';\nimport type { Vault } from './vault.ts';\nimport { listedLine, SECRETS_KIND } from './view.ts';\n\nexport { listedLine, SECRETS_KIND };\n\nexport const SECRETS = 'slicc-secrets';\nexport const SECRETS_COMMAND = '/secrets';\nexport const SHELL_GRANTS = ['fnox', 'exec', '--shell-grants', '--'] as const;\n\nexport const MODEL_LINE =\n \"Credentials are env vars (`$NAME`) holding placeholders
|
|
5
|
-
"mappings": "AACA;AAAA,EACE;AAAA,EAEA;AAAA,EACA;AAAA,EACA;AAAA,EAEA;AAAA,OACK;AAIP,SAAS,YAAY,
|
|
4
|
+
"sourcesContent": ["import type { JsonValue } from '@earendil-works/chord';\nimport {\n defineExtension,\n type Extension,\n GenerationTask,\n hook,\n section,\n type ToolExecutionResult,\n ToolTask,\n} from '@earendil-works/pi-durable';\nimport type { Names, SecretListing, SecretName } from './names.ts';\nimport type { Scrub } from './scrub.ts';\nimport type { Vault } from './vault.ts';\nimport { exposedLine, listedLine, SECRETS_KIND, unavailableLine } from './view.ts';\n\nexport { exposedLine, listedLine, SECRETS_KIND, unavailableLine };\n\nexport const SECRETS = 'slicc-secrets';\nexport const SECRETS_COMMAND = '/secrets';\nexport const SHELL_GRANTS = ['fnox', 'exec', '--shell-grants', '--'] as const;\n\nexport const MODEL_LINE =\n \"Credentials are env vars (`$NAME`) holding placeholders, listed below with where each works (`fnox list` shows the fnox config). The vault fills in the real value only in the listed header, for the listed domain, method and path, over HTTPS (curl, git, fetch). Adding a rule means editing `/etc/fnox/config.toml` with sudo, which the user approves. `fnox exec` gives real values only for secrets the user enabled for exec, and the secrets listed as 'in every bash' already hold real values in your commands. Everything is logged, and stored values are replaced by their placeholder in everything you see; edit around placeholders, since files show them too. Never ask the user to paste a credential into the chat.\";\n\ntype Leaf = { get(): string; set(text: string): void };\n\nfunction leaves(value: JsonValue, out: Leaf[], set: (next: JsonValue) => void): void {\n if (typeof value === 'string') {\n out.push({ get: () => value, set });\n return;\n }\n if (Array.isArray(value)) {\n value.forEach((item, at) => {\n leaves(item, out, (next) => {\n value[at] = next;\n });\n });\n return;\n }\n if (value && typeof value === 'object')\n for (const [key, item] of Object.entries(value))\n leaves(item as JsonValue, out, (next) => {\n (value as Record<string, JsonValue>)[key] = next;\n });\n}\n\nexport async function scrubResult(\n result: ToolExecutionResult,\n scrub: Pick<Scrub, 'texts'>\n): Promise<ToolExecutionResult | undefined> {\n const content = [...(result.output ?? [])];\n const diagnostics = [...(result.diagnostics ?? [])];\n const copy = (value: JsonValue | undefined) =>\n value === undefined ? undefined : (structuredClone(value) as JsonValue);\n const details = copy(result.details);\n const structured = copy(result.structuredOutput);\n const found: Leaf[] = [];\n content.forEach((item, at) => {\n if (item.type === 'text')\n found.push({ get: () => item.text, set: (text) => void (content[at] = { ...item, text }) });\n });\n diagnostics.forEach((item, at) => {\n found.push({\n get: () => item.message,\n set: (message) => void (diagnostics[at] = { ...item, message }),\n });\n });\n let replaced = details;\n if (details !== undefined)\n leaves(details, found, (next) => {\n replaced = next;\n });\n let output = structured;\n if (structured !== undefined)\n leaves(structured, found, (next) => {\n output = next;\n });\n const before = found.map((leaf) => leaf.get());\n const after = await scrub.texts(before);\n if (after.every((text, at) => text === before[at])) return undefined;\n found.forEach((leaf, at) => {\n leaf.set(after[at] as string);\n });\n const kept = result.output as ToolExecutionResult['output'] & unknown[];\n if (kept) kept.splice(0, kept.length, ...content);\n return {\n ...result,\n ...(kept ? { output: kept } : {}),\n ...(result.diagnostics ? { diagnostics } : {}),\n ...(replaced === undefined ? {} : { details: replaced }),\n ...(output === undefined ? {} : { structuredOutput: output }),\n };\n}\n\nfunction where(name: SecretName, granted: boolean): string {\n const rules = name.rules.map((rule) =>\n [\n rule.domain,\n rule.header ? `header ${rule.header}` : '',\n rule.methods?.length ? rule.methods.join(' ') : '',\n rule.paths?.length ? rule.paths.join(' ') : '',\n rule.proposed ? 'proposed in the fnox config, not yet accepted by the vault' : '',\n ]\n .filter(Boolean)\n .join(', ')\n );\n const shell = name.exec === 'shell' && granted ? ['in every bash'] : [];\n const sealed = name.placeholder === null ? ['not sealed'] : [];\n const config = name.fnox ? [] : ['not in the fnox config'];\n const parts = [...rules, ...shell, ...sealed, ...config];\n return parts.length ? `: ${parts.join('; ')}` : '';\n}\n\nexport const NO_SECRETS =\n 'No secrets are configured: the fnox config has no keys and the vault no sealed secrets.';\n\nexport function renderSecrets(listing: SecretListing): string {\n if (listing.error !== null) return unavailableLine(listing.error);\n if (!listing.names.length) return NO_SECRETS;\n const granted = !listing.exposed.length;\n const lines = listing.names.map(\n (name) => `- ${name.name} = ${name.placeholder ?? `$${name.name}`}${where(name, granted)}`\n );\n const off = granted ? [] : ['', exposedLine(listing.exposed)];\n return [MODEL_LINE, '', ...lines, ...off].join('\\n');\n}\n\nexport function secretsEntry(listing: SecretListing) {\n return {\n kind: SECRETS_KIND,\n data: {\n names: structuredClone(listing.names) as unknown as JsonValue,\n error: listing.error,\n exposed: [...listing.exposed],\n },\n model: [{ role: 'user' as const, content: listedLine(listing), timestamp: Date.now() }],\n };\n}\n\nexport function granting(listing: SecretListing): boolean {\n return (\n listing.error === null &&\n !listing.exposed.length &&\n listing.names.some((name) => name.exec === 'shell')\n );\n}\n\nexport function shellGrants(names: Pick<Names, 'state'>) {\n return (argv: readonly string[]): readonly string[] =>\n granting(names.state.value) ? [...SHELL_GRANTS, ...argv] : argv;\n}\n\nexport function secretsExtension(deps: {\n names: Pick<Names, 'state'>;\n vault: Pick<Vault, 'ready' | 'texts'>;\n}): Extension {\n return defineExtension({\n name: SECRETS,\n sections: [section('secrets', () => renderSecrets(deps.names.state.value))],\n hooks: [\n hook(ToolTask, { afterTool: (_call, result) => scrubResult(result, deps.vault) }),\n hook(GenerationTask, {\n beforeRequest: async () => {\n await deps.vault.ready();\n return undefined;\n },\n }),\n ],\n });\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/secrets/names.js
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
import { replicatedState } from "@earendil-works/chord";
|
|
2
|
-
const
|
|
2
|
+
const DESCRIBE = "fnox env --json --describe";
|
|
3
|
+
const RULES = "fnox proxy rules --json";
|
|
4
|
+
const SEALED_LIST = `exec 3<>/dev/sealed && printf '%s' '{"v":1,"op":"list"}' >&3 && cat <&3`;
|
|
5
|
+
const NO_PROXY = "No [proxy] configuration found";
|
|
6
|
+
const MISSING = "fnox isn't installed";
|
|
7
|
+
const EXPOSED = "shell grants are off: fnox exec would also give every command the unsealed fnox secrets";
|
|
8
|
+
const FNOX_ENV = /(^|[\s;&|(])fnox\s+env(\s|$)/;
|
|
9
|
+
const DESCRIBING = /(^|\s)--describe(\s|$)/;
|
|
10
|
+
function guarded(command) {
|
|
11
|
+
if (FNOX_ENV.test(command) && !DESCRIBING.test(command))
|
|
12
|
+
throw new Error(
|
|
13
|
+
`refused to run ${JSON.stringify(command)}: without --describe it prints values`
|
|
14
|
+
);
|
|
15
|
+
return command;
|
|
16
|
+
}
|
|
3
17
|
function strings(value) {
|
|
4
18
|
return Array.isArray(value) ? value.filter((item) => typeof item === "string") : void 0;
|
|
5
19
|
}
|
|
@@ -11,50 +25,175 @@ function ruleOf(value) {
|
|
|
11
25
|
return [
|
|
12
26
|
{
|
|
13
27
|
domain: rule.domain,
|
|
14
|
-
...typeof rule.header === "string" ? { header: rule.header } : {},
|
|
15
|
-
...methods ? { methods } : {},
|
|
16
|
-
...paths ? { paths } : {}
|
|
28
|
+
...typeof rule.header === "string" && rule.header ? { header: rule.header } : {},
|
|
29
|
+
...methods?.length ? { methods } : {},
|
|
30
|
+
...paths?.length ? { paths } : {}
|
|
17
31
|
}
|
|
18
32
|
];
|
|
19
33
|
}
|
|
20
34
|
const EXEC = /* @__PURE__ */ new Set(["any", "argv", "shell"]);
|
|
21
|
-
function
|
|
22
|
-
let parsed;
|
|
35
|
+
function json(text) {
|
|
23
36
|
try {
|
|
24
|
-
|
|
37
|
+
return JSON.parse(text);
|
|
25
38
|
} catch {
|
|
26
|
-
return
|
|
39
|
+
return void 0;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
const line = (text) => text.replace(/\s+/g, " ").trim();
|
|
43
|
+
class Unavailable extends Error {
|
|
44
|
+
detail;
|
|
45
|
+
constructor(shown, detail) {
|
|
46
|
+
super(shown);
|
|
47
|
+
this.detail = detail;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
const word = (value, shape) => typeof value === "string" && shape.test(value) ? value : void 0;
|
|
51
|
+
function parseDescribe(text) {
|
|
52
|
+
const keys = json(text)?.keys;
|
|
53
|
+
if (!Array.isArray(keys)) throw new Unavailable("fnox env --describe printed unparseable output");
|
|
54
|
+
return keys.flatMap(
|
|
55
|
+
(item) => item?.kind === "secret" && typeof item.key === "string" ? [
|
|
56
|
+
{
|
|
57
|
+
name: item.key,
|
|
58
|
+
injected: item.injectable?.exec !== false
|
|
59
|
+
}
|
|
60
|
+
] : []
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
function parseRules(text) {
|
|
64
|
+
const rules2 = json(text)?.rules;
|
|
65
|
+
if (!Array.isArray(rules2)) throw new Unavailable("fnox proxy rules printed unparseable output");
|
|
66
|
+
const out = /* @__PURE__ */ new Map();
|
|
67
|
+
for (const rule of rules2) {
|
|
68
|
+
if (typeof rule?.secret !== "string") continue;
|
|
69
|
+
const found = ruleOf(rule).map((item) => ({ ...item, proposed: true }));
|
|
70
|
+
out.set(rule.secret, [...out.get(rule.secret) ?? [], ...found]);
|
|
71
|
+
}
|
|
72
|
+
return out;
|
|
73
|
+
}
|
|
74
|
+
function parseSealed(text) {
|
|
75
|
+
const reply = json(text);
|
|
76
|
+
if (reply?.error) {
|
|
77
|
+
const code = word(reply.error.code, /^[A-Z][A-Z0-9_]{0,31}$/);
|
|
78
|
+
throw new Unavailable(
|
|
79
|
+
code ? `the vault refused to list secrets (${code})` : "the vault refused to list secrets",
|
|
80
|
+
`${String(reply.error.code ?? "")} ${String(reply.error.message ?? "")}`
|
|
81
|
+
);
|
|
27
82
|
}
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
return
|
|
83
|
+
if (!Array.isArray(reply?.secrets))
|
|
84
|
+
throw new Unavailable("the vault's list reply is unparseable");
|
|
85
|
+
return reply.secrets.flatMap((item) => {
|
|
31
86
|
if (typeof item?.name !== "string" || typeof item.placeholder !== "string") return [];
|
|
32
87
|
const exec = EXEC.has(item.exec) ? item.exec : false;
|
|
33
|
-
const
|
|
34
|
-
return [{ name: item.name, placeholder: item.placeholder, exec, rules }];
|
|
88
|
+
const rules2 = Array.isArray(item.rules) ? item.rules.flatMap(ruleOf) : [];
|
|
89
|
+
return [{ name: item.name, placeholder: item.placeholder, exec, rules: rules2 }];
|
|
35
90
|
});
|
|
36
91
|
}
|
|
37
|
-
|
|
38
|
-
|
|
92
|
+
function joinNames(keys, proposed, sealed2) {
|
|
93
|
+
const vault = new Map(sealed2.map((item) => [item.name, item]));
|
|
94
|
+
const rules2 = (name, own) => own.length ? own : proposed.get(name) ?? [];
|
|
95
|
+
const named = keys.map(({ name }) => {
|
|
96
|
+
const held = vault.get(name);
|
|
97
|
+
return held ? { ...held, rules: rules2(name, held.rules), fnox: true } : { name, placeholder: null, exec: false, rules: rules2(name, []), fnox: true };
|
|
98
|
+
});
|
|
99
|
+
const known = new Set(keys.map((key) => key.name));
|
|
100
|
+
const extra = sealed2.filter((item) => !known.has(item.name)).map((item) => ({ ...item, rules: rules2(item.name, item.rules), fnox: false }));
|
|
101
|
+
const exposed = keys.filter((key) => key.injected && !vault.has(key.name)).map((key) => key.name);
|
|
102
|
+
return { names: [...named, ...extra], error: null, exposed };
|
|
103
|
+
}
|
|
104
|
+
async function run(env, label, command, context) {
|
|
105
|
+
const out = { stdout: "", stderr: "" };
|
|
39
106
|
const ran = await env.exec(
|
|
40
|
-
|
|
107
|
+
guarded(command),
|
|
41
108
|
{
|
|
42
109
|
onOutput: (text, _using, info) => {
|
|
43
|
-
|
|
110
|
+
out[info?.stream === "stderr" ? "stderr" : "stdout"] += text;
|
|
44
111
|
}
|
|
45
112
|
},
|
|
46
113
|
context
|
|
47
|
-
)
|
|
48
|
-
|
|
114
|
+
);
|
|
115
|
+
if (!ran.ok)
|
|
116
|
+
throw new Unavailable(`${label} couldn't run (${ran.error.code})`, ran.error.message);
|
|
117
|
+
return { code: ran.value.exitCode ?? 1, ...out };
|
|
49
118
|
}
|
|
50
|
-
|
|
51
|
-
|
|
119
|
+
const KINDS = {
|
|
120
|
+
config: "the fnox config has an error; check /etc/fnox/config.toml"
|
|
121
|
+
};
|
|
122
|
+
function failed(label, ran) {
|
|
123
|
+
if (ran.code === 127) return new Unavailable(MISSING);
|
|
124
|
+
const error = json(ran.stdout)?.error;
|
|
125
|
+
const kind = word(error?.kind, /^[a-z][a-z_]{0,31}$/);
|
|
126
|
+
const detail = typeof error?.message === "string" ? error.message : void 0;
|
|
127
|
+
if (kind) return new Unavailable(KINDS[kind] ?? `${label} failed (${kind})`, detail);
|
|
128
|
+
return new Unavailable(`${label} exited ${ran.code}`, detail);
|
|
129
|
+
}
|
|
130
|
+
async function describe(env, context) {
|
|
131
|
+
const label = "fnox env --describe";
|
|
132
|
+
const ran = await run(env, label, DESCRIBE, context);
|
|
133
|
+
if (ran.code !== 0) throw failed(label, ran);
|
|
134
|
+
return parseDescribe(ran.stdout);
|
|
135
|
+
}
|
|
136
|
+
async function rules(env, context) {
|
|
137
|
+
const ran = await run(env, "fnox proxy rules", RULES, context);
|
|
138
|
+
if (ran.code === 1 && ran.stderr.includes(NO_PROXY)) return /* @__PURE__ */ new Map();
|
|
139
|
+
if (ran.code !== 0) throw failed("fnox proxy rules", ran);
|
|
140
|
+
return parseRules(ran.stdout);
|
|
141
|
+
}
|
|
142
|
+
async function sealed(env, context) {
|
|
143
|
+
const ran = await run(env, "/dev/sealed", SEALED_LIST, context);
|
|
144
|
+
if (ran.code !== 0) throw new Unavailable(`/dev/sealed couldn't be read (exit ${ran.code})`);
|
|
145
|
+
return parseSealed(ran.stdout);
|
|
146
|
+
}
|
|
147
|
+
async function listNames(env, context) {
|
|
148
|
+
const [keys, proposed, held] = await Promise.allSettled([
|
|
149
|
+
describe(env, context),
|
|
150
|
+
rules(env, context),
|
|
151
|
+
sealed(env, context)
|
|
152
|
+
]);
|
|
153
|
+
if (keys.status === "fulfilled" && proposed.status === "fulfilled" && held.status === "fulfilled")
|
|
154
|
+
return { listing: joinNames(keys.value, proposed.value, held.value), details: [] };
|
|
155
|
+
const failures = [keys, proposed, held].flatMap(
|
|
156
|
+
(item) => item.status === "rejected" ? [
|
|
157
|
+
item.reason instanceof Unavailable ? item.reason : new Unavailable("listing failed", String(item.reason))
|
|
158
|
+
] : []
|
|
159
|
+
);
|
|
160
|
+
return {
|
|
161
|
+
listing: {
|
|
162
|
+
names: [],
|
|
163
|
+
error: [...new Set(failures.map((item) => item.message))].join("; "),
|
|
164
|
+
exposed: []
|
|
165
|
+
},
|
|
166
|
+
details: failures.flatMap((item) => item.detail ? [line(item.detail)] : [])
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function secretNames(env, vault, warn = (message) => console.warn(message)) {
|
|
170
|
+
const state = replicatedState({ names: [], error: null, exposed: [] });
|
|
52
171
|
let version = vault.state.value.version;
|
|
172
|
+
let warned = null;
|
|
53
173
|
let lock = Promise.resolve();
|
|
174
|
+
const report = async (listing, details) => {
|
|
175
|
+
const said = listing.error ?? (listing.exposed.length ? EXPOSED : null);
|
|
176
|
+
const fresh = said !== warned;
|
|
177
|
+
warned = said;
|
|
178
|
+
if (said && fresh) {
|
|
179
|
+
const shown = details.length ? await vault.texts(details).then(
|
|
180
|
+
(texts) => texts.join("; ").slice(0, 200),
|
|
181
|
+
() => ""
|
|
182
|
+
) : "";
|
|
183
|
+
const named = listing.error ? "" : listing.exposed.join(", ");
|
|
184
|
+
warn(
|
|
185
|
+
`slicc-secrets: ${listing.error ? `secrets are unavailable: ${said}` : `${said}: ${named}`}${shown ? ` (${shown})` : ""}`
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
};
|
|
54
189
|
const refresh = (context) => {
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
|
|
190
|
+
const run2 = lock.then(async () => {
|
|
191
|
+
const { listing, details } = await listNames(env, context);
|
|
192
|
+
state.replace(context, listing);
|
|
193
|
+
void report(listing, details);
|
|
194
|
+
});
|
|
195
|
+
lock = run2.catch(() => void 0);
|
|
196
|
+
return run2;
|
|
58
197
|
};
|
|
59
198
|
const off = vault.state.subscribe((value, context) => {
|
|
60
199
|
if (value.version === version) return;
|
|
@@ -64,9 +203,19 @@ function secretNames(env, vault) {
|
|
|
64
203
|
return { state, refresh, close: off };
|
|
65
204
|
}
|
|
66
205
|
export {
|
|
67
|
-
|
|
206
|
+
DESCRIBE,
|
|
207
|
+
EXPOSED,
|
|
208
|
+
MISSING,
|
|
209
|
+
NO_PROXY,
|
|
210
|
+
RULES,
|
|
211
|
+
SEALED_LIST,
|
|
212
|
+
Unavailable,
|
|
213
|
+
guarded,
|
|
214
|
+
joinNames,
|
|
68
215
|
listNames,
|
|
69
|
-
|
|
216
|
+
parseDescribe,
|
|
217
|
+
parseRules,
|
|
218
|
+
parseSealed,
|
|
70
219
|
secretNames
|
|
71
220
|
};
|
|
72
221
|
//# sourceMappingURL=names.js.map
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/secrets/names.ts"],
|
|
4
|
-
"sourcesContent": ["import { type Context, type MutableReplicatedState, replicatedState } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport type { Vault } from './vault.ts';\n\nexport interface SecretRule {\n domain: string;\n header?: string;\n methods?: string[];\n paths?: string[];\n}\n\nexport interface SecretName {\n name: string;\n placeholder: string;\n exec: false | 'any' | 'argv' | 'shell';\n rules: SecretRule[];\n}\n\nexport const LIST = 'secret list --json';\n\nfunction strings(value: unknown): string[] | undefined {\n return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : undefined;\n}\n\nfunction ruleOf(value: unknown): SecretRule[] {\n const rule = value as Record<string, unknown> | null;\n if (typeof rule?.domain !== 'string') return [];\n const methods = strings(rule.methods);\n const paths = strings(rule.paths);\n return [\n {\n domain: rule.domain,\n ...(typeof rule.header === 'string' ? { header: rule.header } : {}),\n ...(methods ? { methods } : {}),\n ...(paths ? { paths } : {}),\n },\n ];\n}\n\nconst EXEC = new Set(['any', 'argv', 'shell']);\n\nexport function parseNames(text: string): SecretName[] {\n let parsed: unknown;\n try {\n parsed = JSON.parse(text);\n } catch {\n return [];\n }\n const list = (parsed as { secrets?: unknown } | null)?.secrets;\n if (!Array.isArray(list)) return [];\n return list.flatMap((item: Record<string, unknown> | null) => {\n if (typeof item?.name !== 'string' || typeof item.placeholder !== 'string') return [];\n const exec = EXEC.has(item.exec as string) ? (item.exec as SecretName['exec']) : false;\n const rules = Array.isArray(item.rules) ? item.rules.flatMap(ruleOf) : [];\n return [{ name: item.name, placeholder: item.placeholder, exec, rules }];\n });\n}\n\nexport async function listNames(env: ExecutionEnv, context: Context): Promise<SecretName[]> {\n let out = '';\n const ran = await env\n .exec(\n LIST,\n {\n onOutput: (text, _using, info) => {\n if (info?.stream !== 'stderr') out += text;\n },\n },\n context\n )\n .catch(() => undefined);\n return ran?.ok && ran.value.exitCode === 0 ? parseNames(out) : [];\n}\n\nexport interface Names {\n readonly state: MutableReplicatedState<SecretName[]>;\n refresh(context: Context): Promise<void>;\n close(): void;\n}\n\nexport function secretNames(env: ExecutionEnv, vault: Pick<Vault, 'state'>): Names {\n const state = replicatedState<SecretName[]>([]);\n let version = vault.state.value.version;\n let lock: Promise<unknown> = Promise.resolve();\n const refresh = (context: Context) => {\n const run = lock.then(async () => state.replace(context, await listNames(env, context)));\n lock = run.catch(() => undefined);\n return run;\n };\n const off = vault.state.subscribe((value, context) => {\n if (value.version === version) return;\n version = value.version;\n void refresh(context).catch(() => undefined);\n });\n return { state, refresh, close: off };\n}\n"],
|
|
5
|
-
"mappings": "AAAA,SAAoD,uBAAuB;
|
|
6
|
-
"names": []
|
|
4
|
+
"sourcesContent": ["import { type Context, type MutableReplicatedState, replicatedState } from '@earendil-works/chord';\nimport type { ExecutionEnv } from '@earendil-works/pi-durable/env';\nimport type { Vault } from './vault.ts';\n\nexport interface SecretRule {\n domain: string;\n header?: string;\n methods?: string[];\n paths?: string[];\n proposed?: true;\n}\n\nexport interface SecretName {\n name: string;\n placeholder: string | null;\n exec: false | 'any' | 'argv' | 'shell';\n rules: SecretRule[];\n fnox: boolean;\n}\n\nexport interface SecretListing {\n names: SecretName[];\n error: string | null;\n exposed: string[];\n}\n\nexport const DESCRIBE = 'fnox env --json --describe';\nexport const RULES = 'fnox proxy rules --json';\nexport const SEALED_LIST = `exec 3<>/dev/sealed && printf '%s' '{\"v\":1,\"op\":\"list\"}' >&3 && cat <&3`;\nexport const NO_PROXY = 'No [proxy] configuration found';\nexport const MISSING = \"fnox isn't installed\";\nexport const EXPOSED =\n 'shell grants are off: fnox exec would also give every command the unsealed fnox secrets';\n\nconst FNOX_ENV = /(^|[\\s;&|(])fnox\\s+env(\\s|$)/;\nconst DESCRIBING = /(^|\\s)--describe(\\s|$)/;\n\nexport function guarded(command: string): string {\n if (FNOX_ENV.test(command) && !DESCRIBING.test(command))\n throw new Error(\n `refused to run ${JSON.stringify(command)}: without --describe it prints values`\n );\n return command;\n}\n\nfunction strings(value: unknown): string[] | undefined {\n return Array.isArray(value) ? value.filter((item) => typeof item === 'string') : undefined;\n}\n\nfunction ruleOf(value: unknown): SecretRule[] {\n const rule = value as Record<string, unknown> | null;\n if (typeof rule?.domain !== 'string') return [];\n const methods = strings(rule.methods);\n const paths = strings(rule.paths);\n return [\n {\n domain: rule.domain,\n ...(typeof rule.header === 'string' && rule.header ? { header: rule.header } : {}),\n ...(methods?.length ? { methods } : {}),\n ...(paths?.length ? { paths } : {}),\n },\n ];\n}\n\nconst EXEC = new Set(['any', 'argv', 'shell']);\n\nfunction json(text: string): unknown {\n try {\n return JSON.parse(text);\n } catch {\n return undefined;\n }\n}\n\nconst line = (text: string) => text.replace(/\\s+/g, ' ').trim();\n\nexport class Unavailable extends Error {\n readonly detail: string | undefined;\n constructor(shown: string, detail?: string) {\n super(shown);\n this.detail = detail;\n }\n}\n\nconst word = (value: unknown, shape: RegExp) =>\n typeof value === 'string' && shape.test(value) ? value : undefined;\n\nexport interface FnoxKey {\n name: string;\n injected: boolean;\n}\n\nexport function parseDescribe(text: string): FnoxKey[] {\n const keys = (json(text) as { keys?: unknown } | null | undefined)?.keys;\n if (!Array.isArray(keys)) throw new Unavailable('fnox env --describe printed unparseable output');\n return keys.flatMap((item: Record<string, unknown> | null) =>\n item?.kind === 'secret' && typeof item.key === 'string'\n ? [\n {\n name: item.key,\n injected: (item.injectable as { exec?: unknown } | undefined)?.exec !== false,\n },\n ]\n : []\n );\n}\n\nexport function parseRules(text: string): Map<string, SecretRule[]> {\n const rules = (json(text) as { rules?: unknown } | null | undefined)?.rules;\n if (!Array.isArray(rules)) throw new Unavailable('fnox proxy rules printed unparseable output');\n const out = new Map<string, SecretRule[]>();\n for (const rule of rules as (Record<string, unknown> | null)[]) {\n if (typeof rule?.secret !== 'string') continue;\n const found = ruleOf(rule).map((item) => ({ ...item, proposed: true as const }));\n out.set(rule.secret, [...(out.get(rule.secret) ?? []), ...found]);\n }\n return out;\n}\n\nexport function parseSealed(text: string): Omit<SecretName, 'fnox'>[] {\n const reply = json(text) as { secrets?: unknown; error?: { code?: unknown; message?: unknown } };\n if (reply?.error) {\n const code = word(reply.error.code, /^[A-Z][A-Z0-9_]{0,31}$/);\n throw new Unavailable(\n code ? `the vault refused to list secrets (${code})` : 'the vault refused to list secrets',\n `${String(reply.error.code ?? '')} ${String(reply.error.message ?? '')}`\n );\n }\n if (!Array.isArray(reply?.secrets))\n throw new Unavailable(\"the vault's list reply is unparseable\");\n return reply.secrets.flatMap((item: Record<string, unknown> | null) => {\n if (typeof item?.name !== 'string' || typeof item.placeholder !== 'string') return [];\n const exec = EXEC.has(item.exec as string) ? (item.exec as SecretName['exec']) : false;\n const rules = Array.isArray(item.rules) ? item.rules.flatMap(ruleOf) : [];\n return [{ name: item.name, placeholder: item.placeholder, exec, rules }];\n });\n}\n\nexport function joinNames(\n keys: readonly FnoxKey[],\n proposed: ReadonlyMap<string, SecretRule[]>,\n sealed: readonly Omit<SecretName, 'fnox'>[]\n): SecretListing {\n const vault = new Map(sealed.map((item) => [item.name, item]));\n const rules = (name: string, own: SecretRule[]) =>\n own.length ? own : (proposed.get(name) ?? []);\n const named = keys.map(({ name }): SecretName => {\n const held = vault.get(name);\n return held\n ? { ...held, rules: rules(name, held.rules), fnox: true }\n : { name, placeholder: null, exec: false, rules: rules(name, []), fnox: true };\n });\n const known = new Set(keys.map((key) => key.name));\n const extra = sealed\n .filter((item) => !known.has(item.name))\n .map((item) => ({ ...item, rules: rules(item.name, item.rules), fnox: false }));\n const exposed = keys.filter((key) => key.injected && !vault.has(key.name)).map((key) => key.name);\n return { names: [...named, ...extra], error: null, exposed };\n}\n\ninterface Ran {\n code: number;\n stdout: string;\n stderr: string;\n}\n\nasync function run(\n env: ExecutionEnv,\n label: string,\n command: string,\n context: Context\n): Promise<Ran> {\n const out = { stdout: '', stderr: '' };\n const ran = await env.exec(\n guarded(command),\n {\n onOutput: (text, _using, info) => {\n out[info?.stream === 'stderr' ? 'stderr' : 'stdout'] += text;\n },\n },\n context\n );\n if (!ran.ok)\n throw new Unavailable(`${label} couldn't run (${ran.error.code})`, ran.error.message);\n return { code: ran.value.exitCode ?? 1, ...out };\n}\n\nconst KINDS: Record<string, string> = {\n config: 'the fnox config has an error; check /etc/fnox/config.toml',\n};\n\nfunction failed(label: string, ran: Ran): Unavailable {\n if (ran.code === 127) return new Unavailable(MISSING);\n const error = (json(ran.stdout) as { error?: { kind?: unknown; message?: unknown } } | undefined)\n ?.error;\n const kind = word(error?.kind, /^[a-z][a-z_]{0,31}$/);\n const detail = typeof error?.message === 'string' ? error.message : undefined;\n if (kind) return new Unavailable(KINDS[kind] ?? `${label} failed (${kind})`, detail);\n return new Unavailable(`${label} exited ${ran.code}`, detail);\n}\n\nasync function describe(env: ExecutionEnv, context: Context) {\n const label = 'fnox env --describe';\n const ran = await run(env, label, DESCRIBE, context);\n if (ran.code !== 0) throw failed(label, ran);\n return parseDescribe(ran.stdout);\n}\n\nasync function rules(env: ExecutionEnv, context: Context) {\n const ran = await run(env, 'fnox proxy rules', RULES, context);\n if (ran.code === 1 && ran.stderr.includes(NO_PROXY)) return new Map<string, SecretRule[]>();\n if (ran.code !== 0) throw failed('fnox proxy rules', ran);\n return parseRules(ran.stdout);\n}\n\nasync function sealed(env: ExecutionEnv, context: Context) {\n const ran = await run(env, '/dev/sealed', SEALED_LIST, context);\n if (ran.code !== 0) throw new Unavailable(`/dev/sealed couldn't be read (exit ${ran.code})`);\n return parseSealed(ran.stdout);\n}\n\nexport interface Listed {\n listing: SecretListing;\n details: string[];\n}\n\nexport async function listNames(env: ExecutionEnv, context: Context): Promise<Listed> {\n const [keys, proposed, held] = await Promise.allSettled([\n describe(env, context),\n rules(env, context),\n sealed(env, context),\n ]);\n if (keys.status === 'fulfilled' && proposed.status === 'fulfilled' && held.status === 'fulfilled')\n return { listing: joinNames(keys.value, proposed.value, held.value), details: [] };\n const failures = [keys, proposed, held].flatMap((item) =>\n item.status === 'rejected'\n ? [\n item.reason instanceof Unavailable\n ? item.reason\n : new Unavailable('listing failed', String(item.reason)),\n ]\n : []\n );\n return {\n listing: {\n names: [],\n error: [...new Set(failures.map((item) => item.message))].join('; '),\n exposed: [],\n },\n details: failures.flatMap((item) => (item.detail ? [line(item.detail)] : [])),\n };\n}\n\nexport interface Names {\n readonly state: MutableReplicatedState<SecretListing>;\n refresh(context: Context): Promise<void>;\n close(): void;\n}\n\nexport function secretNames(\n env: ExecutionEnv,\n vault: Pick<Vault, 'state' | 'texts'>,\n warn: (message: string) => void = (message) => console.warn(message)\n): Names {\n const state = replicatedState<SecretListing>({ names: [], error: null, exposed: [] });\n let version = vault.state.value.version;\n let warned: string | null = null;\n let lock: Promise<unknown> = Promise.resolve();\n const report = async (listing: SecretListing, details: string[]) => {\n const said = listing.error ?? (listing.exposed.length ? EXPOSED : null);\n const fresh = said !== warned;\n warned = said;\n if (said && fresh) {\n const shown = details.length\n ? await vault.texts(details).then(\n (texts) => texts.join('; ').slice(0, 200),\n () => ''\n )\n : '';\n const named = listing.error ? '' : listing.exposed.join(', ');\n warn(\n `slicc-secrets: ${listing.error ? `secrets are unavailable: ${said}` : `${said}: ${named}`}${shown ? ` (${shown})` : ''}`\n );\n }\n };\n const refresh = (context: Context) => {\n const run = lock.then(async () => {\n const { listing, details } = await listNames(env, context);\n state.replace(context, listing);\n void report(listing, details);\n });\n lock = run.catch(() => undefined);\n return run;\n };\n const off = vault.state.subscribe((value, context) => {\n if (value.version === version) return;\n version = value.version;\n void refresh(context).catch(() => undefined);\n });\n return { state, refresh, close: off };\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
|
+
"names": ["rules", "sealed", "run"]
|
|
7
7
|
}
|
package/dist/secrets/view.js
CHANGED
|
@@ -4,12 +4,22 @@ function vaultHold(state) {
|
|
|
4
4
|
if (state.locked === "passkey") return { reason: "locked", held: state.held };
|
|
5
5
|
return state.port === "ready" ? null : { reason: "waiting", held: state.held };
|
|
6
6
|
}
|
|
7
|
-
function
|
|
8
|
-
return `
|
|
7
|
+
function unavailableLine(reason) {
|
|
8
|
+
return `Secrets are unavailable: ${reason.replace(/\.+$/, "")}.`;
|
|
9
|
+
}
|
|
10
|
+
function exposedLine(exposed) {
|
|
11
|
+
return `Shell grants are off: fnox exec would also give every command the unsealed fnox secrets ${exposed.join(", ")}; seal them or set env = false for them.`;
|
|
12
|
+
}
|
|
13
|
+
function listedLine(listing) {
|
|
14
|
+
if (listing.error) return unavailableLine(listing.error);
|
|
15
|
+
const listed = `The user listed secrets: ${listing.names.length ? listing.names.map((name) => name.name).join(", ") : "none"}.`;
|
|
16
|
+
return listing.exposed?.length ? `${listed} ${exposedLine(listing.exposed)}` : listed;
|
|
9
17
|
}
|
|
10
18
|
export {
|
|
11
19
|
SECRETS_KIND,
|
|
20
|
+
exposedLine,
|
|
12
21
|
listedLine,
|
|
22
|
+
unavailableLine,
|
|
13
23
|
vaultHold
|
|
14
24
|
};
|
|
15
25
|
//# sourceMappingURL=view.js.map
|
package/dist/secrets/view.js.map
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../src/secrets/view.ts"],
|
|
4
|
-
"sourcesContent": ["import type {
|
|
5
|
-
"mappings": "AAGO,MAAM,eAAe;AAOrB,SAAS,UAAU,OAAiD;AACzE,MAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,MAAI,MAAM,WAAW,UAAW,QAAO,EAAE,QAAQ,UAAU,MAAM,MAAM,KAAK;AAC5E,SAAO,MAAM,SAAS,UAAU,OAAO,EAAE,QAAQ,WAAW,MAAM,MAAM,KAAK;AAC/E;AAEO,SAAS,
|
|
4
|
+
"sourcesContent": ["import type { SecretListing } from './names.ts';\nimport type { VaultState } from './vault.ts';\n\nexport const SECRETS_KIND = 'slicc.secrets';\n\nexport interface VaultHold {\n reason: 'waiting' | 'locked';\n held: number;\n}\n\nexport function vaultHold(state: VaultState | undefined): VaultHold | null {\n if (!state?.needed) return null;\n if (state.locked === 'passkey') return { reason: 'locked', held: state.held };\n return state.port === 'ready' ? null : { reason: 'waiting', held: state.held };\n}\n\nexport function unavailableLine(reason: string): string {\n return `Secrets are unavailable: ${reason.replace(/\\.+$/, '')}.`;\n}\n\nexport function exposedLine(exposed: readonly string[]): string {\n return `Shell grants are off: fnox exec would also give every command the unsealed fnox secrets ${exposed.join(', ')}; seal them or set env = false for them.`;\n}\n\nexport function listedLine(\n listing: Pick<SecretListing, 'names'> & { error?: string | null; exposed?: string[] }\n): string {\n if (listing.error) return unavailableLine(listing.error);\n const listed = `The user listed secrets: ${listing.names.length ? listing.names.map((name) => name.name).join(', ') : 'none'}.`;\n return listing.exposed?.length ? `${listed} ${exposedLine(listing.exposed)}` : listed;\n}\n"],
|
|
5
|
+
"mappings": "AAGO,MAAM,eAAe;AAOrB,SAAS,UAAU,OAAiD;AACzE,MAAI,CAAC,OAAO,OAAQ,QAAO;AAC3B,MAAI,MAAM,WAAW,UAAW,QAAO,EAAE,QAAQ,UAAU,MAAM,MAAM,KAAK;AAC5E,SAAO,MAAM,SAAS,UAAU,OAAO,EAAE,QAAQ,WAAW,MAAM,MAAM,KAAK;AAC/E;AAEO,SAAS,gBAAgB,QAAwB;AACtD,SAAO,4BAA4B,OAAO,QAAQ,QAAQ,EAAE,CAAC;AAC/D;AAEO,SAAS,YAAY,SAAoC;AAC9D,SAAO,2FAA2F,QAAQ,KAAK,IAAI,CAAC;AACtH;AAEO,SAAS,WACd,SACQ;AACR,MAAI,QAAQ,MAAO,QAAO,gBAAgB,QAAQ,KAAK;AACvD,QAAM,SAAS,4BAA4B,QAAQ,MAAM,SAAS,QAAQ,MAAM,IAAI,CAAC,SAAS,KAAK,IAAI,EAAE,KAAK,IAAI,IAAI,MAAM;AAC5H,SAAO,QAAQ,SAAS,SAAS,GAAG,MAAM,IAAI,YAAY,QAAQ,OAAO,CAAC,KAAK;AACjF;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -413,8 +413,12 @@ function secrets(id, entry) {
|
|
|
413
413
|
id,
|
|
414
414
|
role: "system",
|
|
415
415
|
kind: "notice",
|
|
416
|
-
title: "Secrets listed",
|
|
417
|
-
text: listedLine(
|
|
416
|
+
title: data.error ? "Secrets unavailable" : "Secrets listed",
|
|
417
|
+
text: listedLine({
|
|
418
|
+
names: data.names ?? [],
|
|
419
|
+
error: data.error ?? null,
|
|
420
|
+
exposed: data.exposed ?? []
|
|
421
|
+
}),
|
|
418
422
|
createdAt: entry.model?.[0]?.timestamp ?? 0
|
|
419
423
|
};
|
|
420
424
|
}
|