@aspalagin/openclaw-max 0.7.3 → 0.8.1
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/LICENSE +2 -0
- package/README.md +412 -344
- package/README_EN.md +492 -0
- package/SKILL.md +45 -37
- package/assets/icon.png +0 -0
- package/dist/secret-contract-api.js +6 -0
- package/dist/src/access-policy.js +123 -0
- package/dist/src/accounts.js +170 -23
- package/dist/src/action-scope.js +213 -0
- package/dist/src/actions.js +159 -31
- package/dist/src/api.js +99 -83
- package/dist/src/callbacks.js +27 -7
- package/dist/src/channel-agent-prompt.js +8 -19
- package/dist/src/channel-config.js +25 -3
- package/dist/src/channel-lifecycle.js +28 -5
- package/dist/src/channel-outbound.js +43 -9
- package/dist/src/channel-policy.js +19 -6
- package/dist/src/channel.js +16 -0
- package/dist/src/command-menu.js +199 -0
- package/dist/src/config-schema.js +106 -10
- package/dist/src/deliver.js +144 -48
- package/dist/src/dispatch.js +12 -11
- package/dist/src/gateway-drain.js +40 -0
- package/dist/src/group-mention.js +129 -0
- package/dist/src/inbound-attachments.js +162 -20
- package/dist/src/inbound.js +388 -134
- package/dist/src/media-temp.js +31 -20
- package/dist/src/network.js +216 -0
- package/dist/src/onboarding.js +4 -2
- package/dist/src/polling.js +180 -12
- package/dist/src/presentation.js +11 -0
- package/dist/src/progress-draft.js +168 -0
- package/dist/src/runtime.js +15 -0
- package/dist/src/secret-contract.js +149 -0
- package/dist/src/send.js +250 -46
- package/dist/src/stream-draft.js +83 -41
- package/dist/src/turn-adoption.js +24 -0
- package/dist/src/typing.js +55 -0
- package/dist/src/update-journal.js +267 -0
- package/dist/src/webhook-queue.js +151 -26
- package/dist/src/webhook-runner.js +61 -9
- package/dist/src/webhook.js +34 -15
- package/openclaw.plugin.json +983 -8
- package/package.json +17 -4
- package/src/access-policy.ts +154 -0
- package/src/accounts.ts +260 -25
- package/src/action-scope.ts +274 -0
- package/src/actions.ts +188 -35
- package/src/api.ts +158 -101
- package/src/callbacks.ts +26 -7
- package/src/channel-agent-prompt.ts +8 -19
- package/src/channel-config.ts +32 -3
- package/src/channel-lifecycle.ts +33 -6
- package/src/channel-outbound.ts +50 -7
- package/src/channel-policy.ts +27 -7
- package/src/channel.ts +16 -0
- package/src/command-menu.ts +277 -0
- package/src/config-schema.ts +111 -10
- package/src/deliver.ts +174 -55
- package/src/dispatch.ts +12 -11
- package/src/gateway-drain.ts +45 -0
- package/src/group-mention.ts +191 -0
- package/src/inbound-attachments.ts +173 -22
- package/src/inbound.ts +443 -130
- package/src/media-temp.ts +35 -23
- package/src/network.ts +233 -0
- package/src/onboarding.ts +4 -2
- package/src/polling.ts +221 -15
- package/src/presentation.ts +14 -1
- package/src/progress-draft.ts +226 -0
- package/src/runtime.ts +23 -0
- package/src/secret-contract.ts +183 -0
- package/src/send.ts +346 -59
- package/src/stream-draft.ts +102 -41
- package/src/turn-adoption.ts +33 -0
- package/src/types.ts +9 -3
- package/src/typing.ts +77 -0
- package/src/update-journal.ts +302 -0
- package/src/webhook-queue.ts +205 -33
- package/src/webhook-runner.ts +65 -8
- package/src/webhook.ts +39 -18
- package/scripts/check-cycles.mjs +0 -38
- package/scripts/test-api.mjs +0 -169
- package/scripts/test-send.mjs +0 -162
- package/scripts/update-schema.mjs +0 -48
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Turn status (streaming.mode "progress"): one MAX message that core's
|
|
3
|
+
* progress compositor keeps up to date while the agent works — status
|
|
4
|
+
* headline, plan, approvals and, with streaming.progress.toolProgress, a
|
|
5
|
+
* rolling tool log — and that is deleted once the final answer landed.
|
|
6
|
+
*
|
|
7
|
+
* Core owns what the status says (createChannelProgressDraftCompositor, the
|
|
8
|
+
* channels.max.streaming.progress settings) and when the preview may go away
|
|
9
|
+
* (createLivePreviewLifecycle: deleted after a delivered final, kept after
|
|
10
|
+
* an error final or a failed delivery). This module is the MAX transport:
|
|
11
|
+
* send once, then edit in place, at most one call per second
|
|
12
|
+
* (createDraftStreamLoop) — half of MAX's two edits per second per chat.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import type { ChannelLogSink } from 'openclaw/plugin-sdk/channel-contract';
|
|
16
|
+
import {
|
|
17
|
+
createChannelProgressDraftCompositor,
|
|
18
|
+
createDraftStreamLoop,
|
|
19
|
+
createLivePreviewLifecycle,
|
|
20
|
+
} from 'openclaw/plugin-sdk/channel-outbound';
|
|
21
|
+
|
|
22
|
+
import type { ResolvedMaxAccount } from './accounts.js';
|
|
23
|
+
import { deleteMaxMessage, editMaxMessage, resolveMaxSendFlags, sendMaxMessage } from './send.js';
|
|
24
|
+
|
|
25
|
+
/** One send or edit per second for the status message. */
|
|
26
|
+
export const MAX_PROGRESS_THROTTLE_MS = 1000;
|
|
27
|
+
/** MAX text limit; the compositor stays far below it (8 lines × 120 chars). */
|
|
28
|
+
const MAX_PROGRESS_MAX_CHARS = 4000;
|
|
29
|
+
|
|
30
|
+
type Compositor = ReturnType<typeof createChannelProgressDraftCompositor>;
|
|
31
|
+
|
|
32
|
+
/** Progress callbacks of the reply dispatch (GetReplyOptions subset). */
|
|
33
|
+
export interface MaxProgressReplyOptions {
|
|
34
|
+
suppressDefaultToolProgressMessages: true;
|
|
35
|
+
preserveProgressCallbackStartOrder: true;
|
|
36
|
+
onVerboseProgressVisibility: (isActive: () => boolean) => void;
|
|
37
|
+
onToolStart: (payload: Parameters<Compositor['pushToolEvent']>[0]) => Promise<boolean>;
|
|
38
|
+
onItemEvent: (payload: Parameters<Compositor['pushItemEvent']>[0]) => Promise<boolean>;
|
|
39
|
+
onPlanUpdate: (payload: {
|
|
40
|
+
phase?: string;
|
|
41
|
+
explanation?: string;
|
|
42
|
+
explanationFormat?: 'plain';
|
|
43
|
+
steps?: Parameters<Compositor['pushPlanProgress']>[0];
|
|
44
|
+
}) => Promise<boolean>;
|
|
45
|
+
onApprovalEvent: (payload: Parameters<Compositor['pushApprovalEvent']>[0]) => Promise<boolean>;
|
|
46
|
+
onAssistantMessageStart: () => boolean;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface MaxProgressDraft {
|
|
50
|
+
readonly replyOptions: MaxProgressReplyOptions;
|
|
51
|
+
/** Mid of the status message while it is shown. */
|
|
52
|
+
readonly messageId: string | undefined;
|
|
53
|
+
/**
|
|
54
|
+
* Deliver a final answer: status updates stop, `send` delivers it, and the
|
|
55
|
+
* status message is deleted once a non-error final landed. Errors of `send`
|
|
56
|
+
* propagate (the status then stays as the record of the failed turn).
|
|
57
|
+
*/
|
|
58
|
+
deliverFinal(params: { isError: boolean; send: () => Promise<void> }): Promise<void>;
|
|
59
|
+
/**
|
|
60
|
+
* End of the turn: stop timers and updates; an unused status message (no
|
|
61
|
+
* final, e.g. NO_REPLY or an answer sent by the message tool) is deleted
|
|
62
|
+
* unless the turn failed.
|
|
63
|
+
*/
|
|
64
|
+
close(params?: { failed?: boolean }): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function createMaxProgressDraft(params: {
|
|
68
|
+
account: ResolvedMaxAccount;
|
|
69
|
+
chatId: string;
|
|
70
|
+
replyToId?: string;
|
|
71
|
+
log?: ChannelLogSink;
|
|
72
|
+
statusSink?: (patch: { lastOutboundAt?: number }) => void;
|
|
73
|
+
}): MaxProgressDraft {
|
|
74
|
+
const { account, chatId, log, statusSink } = params;
|
|
75
|
+
let messageId: string | undefined;
|
|
76
|
+
let stopped = false;
|
|
77
|
+
let verboseActive: () => boolean = () => false;
|
|
78
|
+
|
|
79
|
+
const sendOrEdit = async (text: string): Promise<boolean> => {
|
|
80
|
+
const body =
|
|
81
|
+
text.length > MAX_PROGRESS_MAX_CHARS ? text.slice(0, MAX_PROGRESS_MAX_CHARS) : text;
|
|
82
|
+
if (!messageId) {
|
|
83
|
+
// No status appears once the final answer is on its way.
|
|
84
|
+
if (lifecycle.finalStarted) return false;
|
|
85
|
+
// Plain text (tool names and paths carry markdown characters); no push
|
|
86
|
+
// notification for a temporary message — the final answer notifies.
|
|
87
|
+
const res = await sendMaxMessage(chatId, body, {
|
|
88
|
+
token: account.token,
|
|
89
|
+
replyToMessageId: params.replyToId,
|
|
90
|
+
...resolveMaxSendFlags(account.config, { silent: true }),
|
|
91
|
+
});
|
|
92
|
+
messageId = res.messageId || undefined;
|
|
93
|
+
statusSink?.({ lastOutboundAt: Date.now() });
|
|
94
|
+
return Boolean(messageId);
|
|
95
|
+
}
|
|
96
|
+
await editMaxMessage(messageId, body, { token: account.token });
|
|
97
|
+
return true;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const stopOnError = (err: unknown) => {
|
|
101
|
+
// A failed status never fails the turn: stop updating and keep going.
|
|
102
|
+
stopped = true;
|
|
103
|
+
log?.debug?.(`[${account.accountId}] MAX progress status failed: ${String(err)}`);
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
const loop = createDraftStreamLoop<string>({
|
|
107
|
+
throttleMs: MAX_PROGRESS_THROTTLE_MS,
|
|
108
|
+
coalesceInFlight: true,
|
|
109
|
+
isStopped: () => stopped,
|
|
110
|
+
sendOrEditStreamMessage: sendOrEdit,
|
|
111
|
+
onBackgroundFlushError: stopOnError,
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
const stopUpdates = async () => {
|
|
115
|
+
stopped = true;
|
|
116
|
+
loop.stop();
|
|
117
|
+
await loop.waitForInFlight().catch(() => undefined);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const lifecycle = createLivePreviewLifecycle<void, string>({
|
|
121
|
+
draft: {
|
|
122
|
+
flush: () => loop.flush().catch(stopOnError),
|
|
123
|
+
id: () => messageId,
|
|
124
|
+
discardPending: stopUpdates,
|
|
125
|
+
clear: async () => {
|
|
126
|
+
if (!messageId) return true;
|
|
127
|
+
await deleteMaxMessage(messageId, { token: account.token });
|
|
128
|
+
messageId = undefined;
|
|
129
|
+
return true;
|
|
130
|
+
},
|
|
131
|
+
},
|
|
132
|
+
// Like Discord: an error final keeps the status as the record of the turn.
|
|
133
|
+
retainOnError: true,
|
|
134
|
+
cleanupUndelivered: true,
|
|
135
|
+
onFinalStarted: () => {
|
|
136
|
+
compositor.markFinalReplyStarted();
|
|
137
|
+
loop.resetPending();
|
|
138
|
+
},
|
|
139
|
+
onFinalDelivered: () => compositor.markFinalReplyDelivered(),
|
|
140
|
+
onCleanupFailure: (err) => {
|
|
141
|
+
log?.warn(`[${account.accountId}] MAX progress status delete failed: ${String(err)}`);
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
const compositor = createChannelProgressDraftCompositor({
|
|
146
|
+
preparedItems: true,
|
|
147
|
+
entry: account.config as Parameters<typeof createChannelProgressDraftCompositor>[0]['entry'],
|
|
148
|
+
mode: 'progress',
|
|
149
|
+
active: true,
|
|
150
|
+
seed: `${account.accountId}:${chatId}`,
|
|
151
|
+
update: async (text, options) => {
|
|
152
|
+
if (stopped) return false;
|
|
153
|
+
loop.update(text);
|
|
154
|
+
if (options.flush) await loop.flush().catch(stopOnError);
|
|
155
|
+
return !stopped;
|
|
156
|
+
},
|
|
157
|
+
// Every line was retracted: remove the message; new progress sends a new one.
|
|
158
|
+
deleteCurrent: async () => {
|
|
159
|
+
loop.resetPending();
|
|
160
|
+
await loop.waitForInFlight().catch(() => undefined);
|
|
161
|
+
const current = messageId;
|
|
162
|
+
if (!current) return;
|
|
163
|
+
messageId = undefined;
|
|
164
|
+
await deleteMaxMessage(current, { token: account.token }).catch((err: unknown) => {
|
|
165
|
+
log?.warn(`[${account.accountId}] MAX progress status delete failed: ${String(err)}`);
|
|
166
|
+
});
|
|
167
|
+
},
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
/** Progress may change until the final answer starts; verbose mode owns it otherwise. */
|
|
171
|
+
const canPush = () => !stopped && !lifecycle.finalStarted && !verboseActive();
|
|
172
|
+
const push = async (event: () => Promise<boolean>): Promise<boolean> => {
|
|
173
|
+
if (!canPush()) return false;
|
|
174
|
+
try {
|
|
175
|
+
return await event();
|
|
176
|
+
} catch (err) {
|
|
177
|
+
stopOnError(err);
|
|
178
|
+
return false;
|
|
179
|
+
}
|
|
180
|
+
};
|
|
181
|
+
|
|
182
|
+
return {
|
|
183
|
+
get messageId() {
|
|
184
|
+
return messageId;
|
|
185
|
+
},
|
|
186
|
+
replyOptions: {
|
|
187
|
+
suppressDefaultToolProgressMessages: true,
|
|
188
|
+
preserveProgressCallbackStartOrder: true,
|
|
189
|
+
onVerboseProgressVisibility: (isActive) => {
|
|
190
|
+
verboseActive = isActive;
|
|
191
|
+
},
|
|
192
|
+
onToolStart: (payload) => push(() => compositor.pushToolEvent(payload)),
|
|
193
|
+
onItemEvent: (payload) => push(() => compositor.pushItemEvent(payload)),
|
|
194
|
+
onPlanUpdate: (payload) =>
|
|
195
|
+
payload.phase === 'update'
|
|
196
|
+
? push(() =>
|
|
197
|
+
compositor.pushPlanProgress(payload.steps, {
|
|
198
|
+
explanation: payload.explanation,
|
|
199
|
+
explanationFormat: payload.explanationFormat,
|
|
200
|
+
}),
|
|
201
|
+
)
|
|
202
|
+
: Promise.resolve(false),
|
|
203
|
+
onApprovalEvent: (payload) => push(() => compositor.pushApprovalEvent(payload)),
|
|
204
|
+
onAssistantMessageStart: () => {
|
|
205
|
+
compositor.beginAssistantMessage();
|
|
206
|
+
return false;
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
deliverFinal: async ({ isError, send }) => {
|
|
210
|
+
await lifecycle.deliver({
|
|
211
|
+
kind: 'final',
|
|
212
|
+
payload: undefined,
|
|
213
|
+
isError,
|
|
214
|
+
deliverNormally: async () => {
|
|
215
|
+
await send();
|
|
216
|
+
return { visibleReplySent: true };
|
|
217
|
+
},
|
|
218
|
+
});
|
|
219
|
+
},
|
|
220
|
+
close: async (options) => {
|
|
221
|
+
compositor.cancel();
|
|
222
|
+
await lifecycle.cleanup({ failed: options?.failed === true });
|
|
223
|
+
await stopUpdates();
|
|
224
|
+
},
|
|
225
|
+
};
|
|
226
|
+
}
|
package/src/runtime.ts
CHANGED
|
@@ -13,6 +13,29 @@ export function getMaxRuntime(): PluginRuntime {
|
|
|
13
13
|
return runtime;
|
|
14
14
|
}
|
|
15
15
|
|
|
16
|
+
/** The log levels plugin modules without an account log sink use. */
|
|
17
|
+
export type MaxLogger = {
|
|
18
|
+
info: (message: string) => void;
|
|
19
|
+
warn: (message: string) => void;
|
|
20
|
+
error: (message: string) => void;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
let pluginLogger: { owner: PluginRuntime; logger: MaxLogger } | undefined;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Logger for modules with no account log sink at hand (API client, send
|
|
27
|
+
* helpers): the plugin's child logger of the gateway runtime, or the console
|
|
28
|
+
* before the runtime is set (CLI, tests).
|
|
29
|
+
*/
|
|
30
|
+
export function getMaxLogger(): MaxLogger {
|
|
31
|
+
const logging = runtime?.logging;
|
|
32
|
+
if (!runtime || typeof logging?.getChildLogger !== 'function') return console;
|
|
33
|
+
if (pluginLogger?.owner !== runtime) {
|
|
34
|
+
pluginLogger = { owner: runtime, logger: logging.getChildLogger({ plugin: 'max' }) };
|
|
35
|
+
}
|
|
36
|
+
return pluginLogger.logger;
|
|
37
|
+
}
|
|
38
|
+
|
|
16
39
|
/**
|
|
17
40
|
* Legacy runtime config API (OpenClaw < 2026.9.3).
|
|
18
41
|
* Since 2026.9.3 `loadConfig`/`writeConfigFile` are removed from PluginRuntime;
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SecretRef contract of the MAX channel. `secretTargetRegistryEntries` tells
|
|
3
|
+
* `openclaw secrets configure/apply/audit` which options may hold a SecretRef;
|
|
4
|
+
* `collectRuntimeConfigAssignments` tells the gateway which of them each
|
|
5
|
+
* account uses, so it resolves them into the runtime snapshot before the
|
|
6
|
+
* account starts and isolates an account whose ref does not resolve (owner
|
|
7
|
+
* `max:<accountId>`). Loaded from `secret-contract-api.js` before the plugin
|
|
8
|
+
* runtime: keep this module free of runtime imports.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
collectSecretInputAssignment,
|
|
13
|
+
createChannelSecretTargetRegistryEntries,
|
|
14
|
+
getChannelRecord,
|
|
15
|
+
hasOwnProperty,
|
|
16
|
+
isEnabledFlag,
|
|
17
|
+
isRecord,
|
|
18
|
+
type ResolverContext,
|
|
19
|
+
type SecretDefaults,
|
|
20
|
+
} from 'openclaw/plugin-sdk/channel-secret-basic-runtime';
|
|
21
|
+
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from 'openclaw/plugin-sdk/core';
|
|
22
|
+
|
|
23
|
+
/** Options accepting a SecretRef, at the channel level and under accounts.<id>. */
|
|
24
|
+
export const MAX_SECRET_FIELDS = ['botToken', 'webhookSecret', 'httpProxy'] as const;
|
|
25
|
+
export type MaxSecretField = (typeof MAX_SECRET_FIELDS)[number];
|
|
26
|
+
|
|
27
|
+
export const secretTargetRegistryEntries = createChannelSecretTargetRegistryEntries({
|
|
28
|
+
channelKey: 'max',
|
|
29
|
+
account: [...MAX_SECRET_FIELDS],
|
|
30
|
+
channel: [...MAX_SECRET_FIELDS],
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
interface MaxAccountEntry {
|
|
34
|
+
/** Config key under channels.max.accounts */
|
|
35
|
+
key: string;
|
|
36
|
+
accountId: string;
|
|
37
|
+
account: Record<string, unknown>;
|
|
38
|
+
enabled: boolean;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Webhook mode as resolveMaxTransport decides it (transport, else webhookUrl). */
|
|
42
|
+
function usesWebhook(config: Record<string, unknown>): boolean {
|
|
43
|
+
if (config.transport === 'polling' || config.transport === 'webhook') {
|
|
44
|
+
return config.transport === 'webhook';
|
|
45
|
+
}
|
|
46
|
+
return typeof config.webhookUrl === 'string' && config.webhookUrl.trim().length > 0;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Whether an account reads the option at all: the token always, the webhook
|
|
51
|
+
* secret only in webhook mode, the proxy always. `config` holds the account's
|
|
52
|
+
* own transport/webhookUrl (both are per-bot keys, never inherited).
|
|
53
|
+
*/
|
|
54
|
+
function usesField(field: MaxSecretField, config: Record<string, unknown>): boolean {
|
|
55
|
+
return field === 'webhookSecret' ? usesWebhook(config) : true;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** The default account exists as listMaxAccountIds decides it. */
|
|
59
|
+
function hasDefaultAccount(
|
|
60
|
+
channel: Record<string, unknown>,
|
|
61
|
+
named: MaxAccountEntry[],
|
|
62
|
+
env: NodeJS.ProcessEnv,
|
|
63
|
+
): boolean {
|
|
64
|
+
if (channel.botToken || channel.tokenFile || env.MAX_BOT_TOKEN) return true;
|
|
65
|
+
return named.length === 0 && channel.enabled !== false;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function listNamedAccounts(channel: Record<string, unknown>): MaxAccountEntry[] {
|
|
69
|
+
const accounts = channel.accounts;
|
|
70
|
+
if (!isRecord(accounts)) return [];
|
|
71
|
+
const entries: MaxAccountEntry[] = [];
|
|
72
|
+
for (const [key, account] of Object.entries(accounts)) {
|
|
73
|
+
if (!isRecord(account)) continue;
|
|
74
|
+
const accountId = normalizeAccountId(key);
|
|
75
|
+
if (accountId === DEFAULT_ACCOUNT_ID) continue;
|
|
76
|
+
entries.push({
|
|
77
|
+
key,
|
|
78
|
+
accountId,
|
|
79
|
+
account,
|
|
80
|
+
enabled: isEnabledFlag(channel) && isEnabledFlag(account),
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
return entries;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Register every SecretRef the enabled MAX accounts read. A channel-level
|
|
88
|
+
* value belongs to the default account and, for httpProxy only, to each named
|
|
89
|
+
* account that does not set its own; the other options are per-bot.
|
|
90
|
+
*/
|
|
91
|
+
export function collectRuntimeConfigAssignments(params: {
|
|
92
|
+
config: { channels?: Record<string, unknown> };
|
|
93
|
+
defaults?: SecretDefaults;
|
|
94
|
+
context: ResolverContext;
|
|
95
|
+
}): void {
|
|
96
|
+
const channel = getChannelRecord(params.config, 'max');
|
|
97
|
+
if (!channel) return;
|
|
98
|
+
// The owner contract: every non-secret channel setting the accounts use.
|
|
99
|
+
const channelDefaults = Object.fromEntries(
|
|
100
|
+
Object.entries(channel).filter(([key]) => key !== 'accounts'),
|
|
101
|
+
);
|
|
102
|
+
const named = listNamedAccounts(channel);
|
|
103
|
+
const defaultActive =
|
|
104
|
+
isEnabledFlag(channel) && hasDefaultAccount(channel, named, params.context.env);
|
|
105
|
+
|
|
106
|
+
for (const field of MAX_SECRET_FIELDS) {
|
|
107
|
+
const consumers: Array<{ accountId: string; account: Record<string, unknown> }> = [];
|
|
108
|
+
if (defaultActive && usesField(field, channel)) {
|
|
109
|
+
consumers.push({ accountId: DEFAULT_ACCOUNT_ID, account: {} });
|
|
110
|
+
}
|
|
111
|
+
if (field === 'httpProxy') {
|
|
112
|
+
for (const entry of named) {
|
|
113
|
+
if (entry.enabled && !hasOwnProperty(entry.account, field)) consumers.push(entry);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const applyChannel = (value: unknown) => {
|
|
117
|
+
channel[field] = value;
|
|
118
|
+
};
|
|
119
|
+
const channelPath = `channels.max.${field}`;
|
|
120
|
+
if (consumers.length === 0) {
|
|
121
|
+
collectSecretInputAssignment({
|
|
122
|
+
value: channel[field],
|
|
123
|
+
path: channelPath,
|
|
124
|
+
expected: 'string',
|
|
125
|
+
defaults: params.defaults,
|
|
126
|
+
context: params.context,
|
|
127
|
+
active: false,
|
|
128
|
+
inactiveReason: `no enabled MAX account uses this channel-level ${field}.`,
|
|
129
|
+
apply: applyChannel,
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const contract = { channel: channelDefaults, consumers: consumers.map((c) => c.accountId) };
|
|
133
|
+
for (const consumer of consumers) {
|
|
134
|
+
collectSecretInputAssignment({
|
|
135
|
+
value: channel[field],
|
|
136
|
+
path: channelPath,
|
|
137
|
+
expected: 'string',
|
|
138
|
+
defaults: params.defaults,
|
|
139
|
+
context: params.context,
|
|
140
|
+
owner: {
|
|
141
|
+
ownerKind: 'account',
|
|
142
|
+
ownerId: `max:${consumer.accountId}`,
|
|
143
|
+
requiredForGateway: false,
|
|
144
|
+
disposition: 'isolate',
|
|
145
|
+
contract,
|
|
146
|
+
},
|
|
147
|
+
apply: applyChannel,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
for (const entry of named) {
|
|
152
|
+
if (!hasOwnProperty(entry.account, field)) continue;
|
|
153
|
+
collectSecretInputAssignment({
|
|
154
|
+
value: entry.account[field],
|
|
155
|
+
path: `channels.max.accounts.${entry.key}.${field}`,
|
|
156
|
+
expected: 'string',
|
|
157
|
+
defaults: params.defaults,
|
|
158
|
+
context: params.context,
|
|
159
|
+
active: entry.enabled && usesField(field, entry.account),
|
|
160
|
+
inactiveReason:
|
|
161
|
+
field === 'webhookSecret'
|
|
162
|
+
? 'MAX account is disabled or does not use webhook mode.'
|
|
163
|
+
: 'MAX account is disabled.',
|
|
164
|
+
owner: {
|
|
165
|
+
ownerKind: 'account',
|
|
166
|
+
ownerId: `max:${entry.accountId}`,
|
|
167
|
+
requiredForGateway: false,
|
|
168
|
+
disposition: 'isolate',
|
|
169
|
+
contract: { channel: channelDefaults, account: entry.account },
|
|
170
|
+
},
|
|
171
|
+
apply: (value) => {
|
|
172
|
+
entry.account[field] = value;
|
|
173
|
+
},
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** Channel secrets adapter (ChannelPlugin.secrets) and secret-contract-api.js export. */
|
|
180
|
+
export const maxChannelSecrets = {
|
|
181
|
+
secretTargetRegistryEntries,
|
|
182
|
+
collectRuntimeConfigAssignments,
|
|
183
|
+
};
|