@nextclaw/channel-plugin-feishu 0.2.13 → 0.2.15
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 +3 -1
- package/index.ts +65 -0
- package/openclaw.plugin.json +3 -7
- package/package.json +32 -9
- package/skills/feishu-doc/SKILL.md +211 -0
- package/skills/feishu-doc/references/block-types.md +103 -0
- package/skills/feishu-drive/SKILL.md +97 -0
- package/skills/feishu-perm/SKILL.md +119 -0
- package/skills/feishu-wiki/SKILL.md +111 -0
- package/src/accounts.test.ts +371 -0
- package/src/accounts.ts +244 -0
- package/src/async.ts +62 -0
- package/src/bitable.ts +725 -0
- package/src/bot.card-action.test.ts +63 -0
- package/src/bot.checkBotMentioned.test.ts +193 -0
- package/src/bot.stripBotMention.test.ts +134 -0
- package/src/bot.test.ts +2107 -0
- package/src/bot.ts +1556 -0
- package/src/card-action.ts +79 -0
- package/src/channel.test.ts +48 -0
- package/src/channel.ts +369 -0
- package/src/chat-schema.ts +24 -0
- package/src/chat.test.ts +89 -0
- package/src/chat.ts +130 -0
- package/src/client.test.ts +324 -0
- package/src/client.ts +196 -0
- package/src/config-schema.test.ts +247 -0
- package/src/config-schema.ts +306 -0
- package/src/dedup.ts +203 -0
- package/src/directory.test.ts +40 -0
- package/src/directory.ts +156 -0
- package/src/doc-schema.ts +182 -0
- package/src/docx-batch-insert.test.ts +90 -0
- package/src/docx-batch-insert.ts +187 -0
- package/src/docx-color-text.ts +149 -0
- package/src/docx-table-ops.ts +298 -0
- package/src/docx.account-selection.test.ts +70 -0
- package/src/docx.test.ts +445 -0
- package/src/docx.ts +1460 -0
- package/src/drive-schema.ts +46 -0
- package/src/drive.ts +228 -0
- package/src/dynamic-agent.ts +131 -0
- package/src/external-keys.test.ts +20 -0
- package/src/external-keys.ts +19 -0
- package/src/feishu-command-handler.ts +59 -0
- package/src/media.test.ts +523 -0
- package/src/media.ts +484 -0
- package/src/mention.ts +133 -0
- package/src/monitor.account.ts +562 -0
- package/src/monitor.reaction.test.ts +653 -0
- package/src/monitor.startup.test.ts +190 -0
- package/src/monitor.startup.ts +64 -0
- package/src/monitor.state.defaults.test.ts +46 -0
- package/src/monitor.state.ts +155 -0
- package/src/monitor.test-mocks.ts +45 -0
- package/src/monitor.transport.ts +264 -0
- package/src/monitor.ts +95 -0
- package/src/monitor.webhook-e2e.test.ts +214 -0
- package/src/monitor.webhook-security.test.ts +142 -0
- package/src/monitor.webhook.test-helpers.ts +98 -0
- package/src/nextclaw-sdk/account-id.ts +31 -0
- package/src/nextclaw-sdk/compat.ts +8 -0
- package/src/nextclaw-sdk/core-channel.ts +296 -0
- package/src/nextclaw-sdk/core-pairing.ts +224 -0
- package/src/nextclaw-sdk/core.ts +26 -0
- package/src/nextclaw-sdk/dedupe.ts +246 -0
- package/src/nextclaw-sdk/feishu.ts +77 -0
- package/src/nextclaw-sdk/history.ts +127 -0
- package/src/nextclaw-sdk/network-body.ts +245 -0
- package/src/nextclaw-sdk/network-fetch.ts +129 -0
- package/src/nextclaw-sdk/network-webhook.ts +182 -0
- package/src/nextclaw-sdk/network.ts +13 -0
- package/src/nextclaw-sdk/runtime-store.ts +26 -0
- package/src/nextclaw-sdk/secrets-config.ts +109 -0
- package/src/nextclaw-sdk/secrets-core.ts +170 -0
- package/src/nextclaw-sdk/secrets-prompt.ts +305 -0
- package/src/nextclaw-sdk/secrets.ts +18 -0
- package/src/nextclaw-sdk/types.ts +300 -0
- package/src/onboarding.status.test.ts +25 -0
- package/src/onboarding.test.ts +143 -0
- package/src/onboarding.ts +489 -0
- package/src/outbound.test.ts +356 -0
- package/src/outbound.ts +176 -0
- package/src/perm-schema.ts +52 -0
- package/src/perm.ts +176 -0
- package/src/policy.test.ts +154 -0
- package/src/policy.ts +123 -0
- package/src/post.test.ts +105 -0
- package/src/post.ts +274 -0
- package/src/probe.test.ts +270 -0
- package/src/probe.ts +156 -0
- package/src/reactions.ts +153 -0
- package/src/reply-dispatcher.test.ts +513 -0
- package/src/reply-dispatcher.ts +397 -0
- package/src/runtime.ts +6 -0
- package/src/secret-input.ts +13 -0
- package/src/send-message.ts +71 -0
- package/src/send-result.ts +29 -0
- package/src/send-target.test.ts +74 -0
- package/src/send-target.ts +29 -0
- package/src/send.reply-fallback.test.ts +189 -0
- package/src/send.test.ts +168 -0
- package/src/send.ts +481 -0
- package/src/streaming-card.test.ts +54 -0
- package/src/streaming-card.ts +374 -0
- package/src/targets.test.ts +70 -0
- package/src/targets.ts +107 -0
- package/src/tool-account-routing.test.ts +129 -0
- package/src/tool-account.ts +70 -0
- package/src/tool-factory-test-harness.ts +76 -0
- package/src/tool-result.test.ts +32 -0
- package/src/tool-result.ts +14 -0
- package/src/tools-config.test.ts +21 -0
- package/src/tools-config.ts +22 -0
- package/src/types.ts +103 -0
- package/src/typing.test.ts +144 -0
- package/src/typing.ts +210 -0
- package/src/wiki-schema.ts +55 -0
- package/src/wiki.ts +233 -0
- package/index.js +0 -27
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
import type { SecretRef, SecretRefSource } from "./types.js";
|
|
3
|
+
|
|
4
|
+
export const DEFAULT_SECRET_PROVIDER_ALIAS = "default";
|
|
5
|
+
export const ENV_SECRET_REF_ID_RE = /^[A-Z][A-Z0-9_]{0,127}$/;
|
|
6
|
+
export const SECRET_PROVIDER_ALIAS_PATTERN = /^[a-z][a-z0-9_-]{0,63}$/;
|
|
7
|
+
const ENV_SECRET_TEMPLATE_RE = /^\$\{([A-Z][A-Z0-9_]{0,127})\}$/;
|
|
8
|
+
const FILE_SECRET_REF_SEGMENT_PATTERN = /^(?:[^~]|~0|~1)*$/;
|
|
9
|
+
const EXEC_SECRET_REF_ID_PATTERN = /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/;
|
|
10
|
+
|
|
11
|
+
type SecretDefaults = {
|
|
12
|
+
env?: string;
|
|
13
|
+
file?: string;
|
|
14
|
+
exec?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
18
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function isSecretRef(value: unknown): value is SecretRef {
|
|
22
|
+
return (
|
|
23
|
+
isRecord(value) &&
|
|
24
|
+
(value.source === "env" || value.source === "file" || value.source === "exec") &&
|
|
25
|
+
typeof value.provider === "string" &&
|
|
26
|
+
value.provider.trim().length > 0 &&
|
|
27
|
+
typeof value.id === "string" &&
|
|
28
|
+
value.id.trim().length > 0
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function coerceSecretRef(value: unknown, defaults?: SecretDefaults): SecretRef | null {
|
|
33
|
+
if (isSecretRef(value)) {
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
36
|
+
if (typeof value === "string") {
|
|
37
|
+
const match = ENV_SECRET_TEMPLATE_RE.exec(value.trim());
|
|
38
|
+
if (match) {
|
|
39
|
+
return {
|
|
40
|
+
source: "env",
|
|
41
|
+
provider: defaults?.env ?? DEFAULT_SECRET_PROVIDER_ALIAS,
|
|
42
|
+
id: match[1],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (!isRecord(value)) {
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
if (
|
|
50
|
+
(value.source === "env" || value.source === "file" || value.source === "exec") &&
|
|
51
|
+
typeof value.id === "string" &&
|
|
52
|
+
value.id.trim().length > 0 &&
|
|
53
|
+
value.provider === undefined
|
|
54
|
+
) {
|
|
55
|
+
const source = value.source as SecretRefSource;
|
|
56
|
+
return {
|
|
57
|
+
source,
|
|
58
|
+
provider:
|
|
59
|
+
source === "env"
|
|
60
|
+
? (defaults?.env ?? DEFAULT_SECRET_PROVIDER_ALIAS)
|
|
61
|
+
: source === "file"
|
|
62
|
+
? (defaults?.file ?? DEFAULT_SECRET_PROVIDER_ALIAS)
|
|
63
|
+
: (defaults?.exec ?? DEFAULT_SECRET_PROVIDER_ALIAS),
|
|
64
|
+
id: value.id,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
return null;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function resolveSecretInputRef(params: {
|
|
71
|
+
value: unknown;
|
|
72
|
+
refValue?: unknown;
|
|
73
|
+
defaults?: SecretDefaults;
|
|
74
|
+
}): SecretRef | null {
|
|
75
|
+
return coerceSecretRef(params.refValue, params.defaults) ?? coerceSecretRef(params.value, params.defaults);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function normalizeSecretInputString(value: unknown): string | undefined {
|
|
79
|
+
if (typeof value !== "string") {
|
|
80
|
+
return undefined;
|
|
81
|
+
}
|
|
82
|
+
const trimmed = value.trim();
|
|
83
|
+
return trimmed || undefined;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function hasConfiguredSecretInput(value: unknown, defaults?: SecretDefaults): boolean {
|
|
87
|
+
return Boolean(normalizeSecretInputString(value) || coerceSecretRef(value, defaults));
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function normalizeResolvedSecretInputString(params: {
|
|
91
|
+
value: unknown;
|
|
92
|
+
refValue?: unknown;
|
|
93
|
+
defaults?: SecretDefaults;
|
|
94
|
+
path: string;
|
|
95
|
+
}): string | undefined {
|
|
96
|
+
const normalized = normalizeSecretInputString(params.value);
|
|
97
|
+
if (normalized) {
|
|
98
|
+
return normalized;
|
|
99
|
+
}
|
|
100
|
+
const ref = resolveSecretInputRef(params);
|
|
101
|
+
if (ref) {
|
|
102
|
+
throw new Error(
|
|
103
|
+
`${params.path}: unresolved SecretRef "${ref.source}:${ref.provider}:${ref.id}".`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function isValidFileSecretRefId(value: string): boolean {
|
|
110
|
+
if (value === "value") {
|
|
111
|
+
return true;
|
|
112
|
+
}
|
|
113
|
+
if (!value.startsWith("/")) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return value
|
|
117
|
+
.slice(1)
|
|
118
|
+
.split("/")
|
|
119
|
+
.every((segment) => FILE_SECRET_REF_SEGMENT_PATTERN.test(segment));
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function isValidExecSecretRefId(value: string): boolean {
|
|
123
|
+
if (!EXEC_SECRET_REF_ID_PATTERN.test(value)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
return value.split("/").every((segment) => segment !== "." && segment !== "..");
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
export function formatExecSecretRefIdValidationMessage(): string {
|
|
130
|
+
return [
|
|
131
|
+
"Exec secret reference id must match /^[A-Za-z0-9][A-Za-z0-9._:/-]{0,255}$/",
|
|
132
|
+
'and must not include "." or ".." path segments',
|
|
133
|
+
'(example: "vault/openai/api-key").',
|
|
134
|
+
].join(" ");
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export function buildSecretInputSchema() {
|
|
138
|
+
const providerSchema = z
|
|
139
|
+
.string()
|
|
140
|
+
.regex(
|
|
141
|
+
SECRET_PROVIDER_ALIAS_PATTERN,
|
|
142
|
+
'Secret reference provider must match /^[a-z][a-z0-9_-]{0,63}$/ (example: "default").',
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
return z.union([
|
|
146
|
+
z.string(),
|
|
147
|
+
z.discriminatedUnion("source", [
|
|
148
|
+
z.object({
|
|
149
|
+
source: z.literal("env"),
|
|
150
|
+
provider: providerSchema,
|
|
151
|
+
id: z
|
|
152
|
+
.string()
|
|
153
|
+
.regex(
|
|
154
|
+
ENV_SECRET_REF_ID_RE,
|
|
155
|
+
'Env secret reference id must match /^[A-Z][A-Z0-9_]{0,127}$/ (example: "OPENAI_API_KEY").',
|
|
156
|
+
),
|
|
157
|
+
}),
|
|
158
|
+
z.object({
|
|
159
|
+
source: z.literal("file"),
|
|
160
|
+
provider: providerSchema,
|
|
161
|
+
id: z.string().refine(isValidFileSecretRefId, "Invalid file secret reference id."),
|
|
162
|
+
}),
|
|
163
|
+
z.object({
|
|
164
|
+
source: z.literal("exec"),
|
|
165
|
+
provider: providerSchema,
|
|
166
|
+
id: z.string().refine(isValidExecSecretRefId, formatExecSecretRefIdValidationMessage()),
|
|
167
|
+
}),
|
|
168
|
+
]),
|
|
169
|
+
]);
|
|
170
|
+
}
|
|
@@ -0,0 +1,305 @@
|
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import {
|
|
3
|
+
DEFAULT_SECRET_PROVIDER_ALIAS,
|
|
4
|
+
ENV_SECRET_REF_ID_RE,
|
|
5
|
+
formatExecSecretRefIdValidationMessage,
|
|
6
|
+
isValidExecSecretRefId,
|
|
7
|
+
isValidFileSecretRefId,
|
|
8
|
+
} from "./secrets-core.js";
|
|
9
|
+
import type { ClawdbotConfig, SecretInput, SecretRefSource, WizardPrompter } from "./types.js";
|
|
10
|
+
|
|
11
|
+
type SecretPromptResult =
|
|
12
|
+
| { action: "keep" }
|
|
13
|
+
| { action: "use-env" }
|
|
14
|
+
| { action: "set"; value: SecretInput; resolvedValue: string };
|
|
15
|
+
|
|
16
|
+
function resolveDefaultSecretProviderAlias(
|
|
17
|
+
cfg: ClawdbotConfig,
|
|
18
|
+
source: SecretRefSource,
|
|
19
|
+
): string {
|
|
20
|
+
const configured =
|
|
21
|
+
source === "env"
|
|
22
|
+
? cfg.secrets?.defaults?.env
|
|
23
|
+
: source === "file"
|
|
24
|
+
? cfg.secrets?.defaults?.file
|
|
25
|
+
: cfg.secrets?.defaults?.exec;
|
|
26
|
+
if (configured?.trim()) {
|
|
27
|
+
return configured.trim();
|
|
28
|
+
}
|
|
29
|
+
const providers = cfg.secrets?.providers;
|
|
30
|
+
if (providers) {
|
|
31
|
+
for (const [name, provider] of Object.entries(providers)) {
|
|
32
|
+
if (provider?.source === source) {
|
|
33
|
+
return name;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return DEFAULT_SECRET_PROVIDER_ALIAS;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
41
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function readJsonPointerValue(payload: unknown, pointer: string): string | null {
|
|
45
|
+
if (pointer === "value") {
|
|
46
|
+
return typeof payload === "string" ? payload : null;
|
|
47
|
+
}
|
|
48
|
+
const segments = pointer
|
|
49
|
+
.slice(1)
|
|
50
|
+
.split("/")
|
|
51
|
+
.map((segment) => segment.replace(/~1/g, "/").replace(/~0/g, "~"));
|
|
52
|
+
let cursor: unknown = payload;
|
|
53
|
+
for (const segment of segments) {
|
|
54
|
+
if (!isRecord(cursor) && !Array.isArray(cursor)) {
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
cursor = (cursor as Record<string, unknown>)[segment];
|
|
58
|
+
}
|
|
59
|
+
return typeof cursor === "string" ? cursor : null;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function tryResolveFileProviderValue(cfg: ClawdbotConfig, provider: string, id: string): string | null {
|
|
63
|
+
const providerConfig = cfg.secrets?.providers?.[provider];
|
|
64
|
+
if (!providerConfig || providerConfig.source !== "file" || !providerConfig.path) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
try {
|
|
68
|
+
const raw = fs.readFileSync(providerConfig.path, "utf-8");
|
|
69
|
+
if (providerConfig.mode === "singleValue") {
|
|
70
|
+
return raw.trim();
|
|
71
|
+
}
|
|
72
|
+
const parsed = JSON.parse(raw) as unknown;
|
|
73
|
+
return readJsonPointerValue(parsed, id);
|
|
74
|
+
} catch {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function promptSingleChannelToken(params: {
|
|
80
|
+
prompter: Pick<WizardPrompter, "confirm" | "text">;
|
|
81
|
+
accountConfigured: boolean;
|
|
82
|
+
canUseEnv: boolean;
|
|
83
|
+
hasConfigToken: boolean;
|
|
84
|
+
envPrompt: string;
|
|
85
|
+
keepPrompt: string;
|
|
86
|
+
inputPrompt: string;
|
|
87
|
+
}): Promise<{ useEnv: boolean; token: string | null }> {
|
|
88
|
+
const promptToken = async (): Promise<string> =>
|
|
89
|
+
String(
|
|
90
|
+
await params.prompter.text({
|
|
91
|
+
message: params.inputPrompt,
|
|
92
|
+
validate: (value) => (value?.trim() ? undefined : "Required"),
|
|
93
|
+
}),
|
|
94
|
+
).trim();
|
|
95
|
+
|
|
96
|
+
return (async () => {
|
|
97
|
+
if (params.canUseEnv) {
|
|
98
|
+
const keepEnv = await params.prompter.confirm({
|
|
99
|
+
message: params.envPrompt,
|
|
100
|
+
initialValue: true,
|
|
101
|
+
});
|
|
102
|
+
if (keepEnv) {
|
|
103
|
+
return { useEnv: true, token: null };
|
|
104
|
+
}
|
|
105
|
+
return { useEnv: false, token: await promptToken() };
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
if (params.hasConfigToken && params.accountConfigured) {
|
|
109
|
+
const keep = await params.prompter.confirm({
|
|
110
|
+
message: params.keepPrompt,
|
|
111
|
+
initialValue: true,
|
|
112
|
+
});
|
|
113
|
+
if (keep) {
|
|
114
|
+
return { useEnv: false, token: null };
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
return { useEnv: false, token: await promptToken() };
|
|
119
|
+
})();
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async function resolveSecretInputMode(params: {
|
|
123
|
+
prompter: Pick<WizardPrompter, "select">;
|
|
124
|
+
explicitMode?: "plaintext" | "ref";
|
|
125
|
+
credentialLabel: string;
|
|
126
|
+
}): Promise<"plaintext" | "ref"> {
|
|
127
|
+
if (params.explicitMode) {
|
|
128
|
+
return params.explicitMode;
|
|
129
|
+
}
|
|
130
|
+
return await params.prompter.select({
|
|
131
|
+
message: `How do you want to provide this ${params.credentialLabel}?`,
|
|
132
|
+
initialValue: "plaintext",
|
|
133
|
+
options: [
|
|
134
|
+
{
|
|
135
|
+
value: "plaintext",
|
|
136
|
+
label: `Enter ${params.credentialLabel}`,
|
|
137
|
+
hint: "Stores the credential directly in NextClaw config",
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
value: "ref",
|
|
141
|
+
label: "Use secret reference",
|
|
142
|
+
hint: "Stores a reference to env or configured secret providers",
|
|
143
|
+
},
|
|
144
|
+
],
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function promptSecretRefForSetup(params: {
|
|
149
|
+
cfg: ClawdbotConfig;
|
|
150
|
+
prompter: Pick<WizardPrompter, "note" | "select" | "text">;
|
|
151
|
+
preferredEnvVar?: string;
|
|
152
|
+
}): Promise<{ value: SecretInput; resolvedValue: string }> {
|
|
153
|
+
const providers = Object.entries(params.cfg.secrets?.providers ?? {}).filter(
|
|
154
|
+
([, provider]) => provider?.source === "file" || provider?.source === "exec",
|
|
155
|
+
);
|
|
156
|
+
const source = await params.prompter.select({
|
|
157
|
+
message: "Where is this secret stored?",
|
|
158
|
+
initialValue: "env",
|
|
159
|
+
options: [
|
|
160
|
+
{ value: "env", label: "Environment variable", hint: "Reference an env var" },
|
|
161
|
+
...(providers.length > 0
|
|
162
|
+
? [{ value: "provider", label: "Configured provider", hint: "Reference file/exec provider" }]
|
|
163
|
+
: []),
|
|
164
|
+
],
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
if (source === "env") {
|
|
168
|
+
const envVar = String(
|
|
169
|
+
await params.prompter.text({
|
|
170
|
+
message: "Environment variable name",
|
|
171
|
+
initialValue: params.preferredEnvVar,
|
|
172
|
+
placeholder: params.preferredEnvVar ?? "NEXTCLAW_SECRET",
|
|
173
|
+
validate: (value) => {
|
|
174
|
+
const candidate = value.trim();
|
|
175
|
+
if (!ENV_SECRET_REF_ID_RE.test(candidate)) {
|
|
176
|
+
return 'Use an env var name like "OPENAI_API_KEY".';
|
|
177
|
+
}
|
|
178
|
+
if (!process.env[candidate]?.trim()) {
|
|
179
|
+
return `Environment variable "${candidate}" is missing or empty in this session.`;
|
|
180
|
+
}
|
|
181
|
+
return undefined;
|
|
182
|
+
},
|
|
183
|
+
}),
|
|
184
|
+
).trim();
|
|
185
|
+
return {
|
|
186
|
+
value: {
|
|
187
|
+
source: "env",
|
|
188
|
+
provider: resolveDefaultSecretProviderAlias(params.cfg, "env"),
|
|
189
|
+
id: envVar,
|
|
190
|
+
},
|
|
191
|
+
resolvedValue: process.env[envVar]?.trim() ?? "",
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const provider = await params.prompter.select({
|
|
196
|
+
message: "Select secret provider",
|
|
197
|
+
initialValue: providers[0]?.[0],
|
|
198
|
+
options: providers.map(([name, providerConfig]) => ({
|
|
199
|
+
value: name,
|
|
200
|
+
label: name,
|
|
201
|
+
hint: providerConfig?.source === "exec" ? "Exec provider" : "File provider",
|
|
202
|
+
})),
|
|
203
|
+
});
|
|
204
|
+
const providerConfig = params.cfg.secrets?.providers?.[provider];
|
|
205
|
+
const id = String(
|
|
206
|
+
await params.prompter.text({
|
|
207
|
+
message:
|
|
208
|
+
providerConfig?.source === "file"
|
|
209
|
+
? "Secret id (JSON pointer, or 'value' for singleValue mode)"
|
|
210
|
+
: "Secret id for the exec provider",
|
|
211
|
+
initialValue: providerConfig?.source === "file" ? "/providers/feishu/appSecret" : undefined,
|
|
212
|
+
validate: (value) => {
|
|
213
|
+
const candidate = value.trim();
|
|
214
|
+
if (!candidate) {
|
|
215
|
+
return "Required";
|
|
216
|
+
}
|
|
217
|
+
if (providerConfig?.source === "file") {
|
|
218
|
+
return isValidFileSecretRefId(candidate) ? undefined : "Invalid file secret reference id.";
|
|
219
|
+
}
|
|
220
|
+
return isValidExecSecretRefId(candidate)
|
|
221
|
+
? undefined
|
|
222
|
+
: formatExecSecretRefIdValidationMessage();
|
|
223
|
+
},
|
|
224
|
+
}),
|
|
225
|
+
).trim();
|
|
226
|
+
|
|
227
|
+
const resolvedValue =
|
|
228
|
+
providerConfig?.source === "file" ? (tryResolveFileProviderValue(params.cfg, provider, id) ?? "") : "";
|
|
229
|
+
if (!resolvedValue && providerConfig?.source === "exec") {
|
|
230
|
+
await params.prompter.note(
|
|
231
|
+
"Exec provider reference saved. Connection probe will rely on runtime secret resolution later.",
|
|
232
|
+
"Secret reference saved",
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return {
|
|
237
|
+
value: {
|
|
238
|
+
source: providerConfig?.source === "exec" ? "exec" : "file",
|
|
239
|
+
provider,
|
|
240
|
+
id,
|
|
241
|
+
},
|
|
242
|
+
resolvedValue,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export async function promptSingleChannelSecretInput(params: {
|
|
247
|
+
cfg: ClawdbotConfig;
|
|
248
|
+
prompter: Pick<WizardPrompter, "confirm" | "note" | "select" | "text">;
|
|
249
|
+
providerHint: string;
|
|
250
|
+
credentialLabel: string;
|
|
251
|
+
secretInputMode?: "plaintext" | "ref";
|
|
252
|
+
accountConfigured: boolean;
|
|
253
|
+
canUseEnv: boolean;
|
|
254
|
+
hasConfigToken: boolean;
|
|
255
|
+
envPrompt: string;
|
|
256
|
+
keepPrompt: string;
|
|
257
|
+
inputPrompt: string;
|
|
258
|
+
preferredEnvVar?: string;
|
|
259
|
+
}): Promise<SecretPromptResult> {
|
|
260
|
+
const selectedMode = await resolveSecretInputMode({
|
|
261
|
+
prompter: params.prompter,
|
|
262
|
+
explicitMode: params.secretInputMode,
|
|
263
|
+
credentialLabel: params.credentialLabel,
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
if (selectedMode === "plaintext") {
|
|
267
|
+
const plainResult = await promptSingleChannelToken({
|
|
268
|
+
prompter: params.prompter,
|
|
269
|
+
accountConfigured: params.accountConfigured,
|
|
270
|
+
canUseEnv: params.canUseEnv,
|
|
271
|
+
hasConfigToken: params.hasConfigToken,
|
|
272
|
+
envPrompt: params.envPrompt,
|
|
273
|
+
keepPrompt: params.keepPrompt,
|
|
274
|
+
inputPrompt: params.inputPrompt,
|
|
275
|
+
});
|
|
276
|
+
if (plainResult.useEnv) {
|
|
277
|
+
return { action: "use-env" };
|
|
278
|
+
}
|
|
279
|
+
if (plainResult.token) {
|
|
280
|
+
return { action: "set", value: plainResult.token, resolvedValue: plainResult.token };
|
|
281
|
+
}
|
|
282
|
+
return { action: "keep" };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (params.hasConfigToken && params.accountConfigured) {
|
|
286
|
+
const keep = await params.prompter.confirm({
|
|
287
|
+
message: params.keepPrompt,
|
|
288
|
+
initialValue: true,
|
|
289
|
+
});
|
|
290
|
+
if (keep) {
|
|
291
|
+
return { action: "keep" };
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
const refResult = await promptSecretRefForSetup({
|
|
296
|
+
cfg: params.cfg,
|
|
297
|
+
prompter: params.prompter,
|
|
298
|
+
preferredEnvVar: params.preferredEnvVar,
|
|
299
|
+
});
|
|
300
|
+
return {
|
|
301
|
+
action: "set",
|
|
302
|
+
value: refResult.value,
|
|
303
|
+
resolvedValue: refResult.resolvedValue,
|
|
304
|
+
};
|
|
305
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
export {
|
|
2
|
+
buildSecretInputSchema,
|
|
3
|
+
formatExecSecretRefIdValidationMessage,
|
|
4
|
+
hasConfiguredSecretInput,
|
|
5
|
+
isValidExecSecretRefId,
|
|
6
|
+
isValidFileSecretRefId,
|
|
7
|
+
normalizeResolvedSecretInputString,
|
|
8
|
+
normalizeSecretInputString,
|
|
9
|
+
} from "./secrets-core.js";
|
|
10
|
+
export {
|
|
11
|
+
buildSingleChannelSecretPromptState,
|
|
12
|
+
mergeAllowFromEntries,
|
|
13
|
+
setTopLevelChannelAllowFrom,
|
|
14
|
+
setTopLevelChannelDmPolicyWithAllowFrom,
|
|
15
|
+
setTopLevelChannelGroupPolicy,
|
|
16
|
+
splitOnboardingEntries,
|
|
17
|
+
} from "./secrets-config.js";
|
|
18
|
+
export { promptSingleChannelSecretInput } from "./secrets-prompt.js";
|