@invago/mixin 1.0.9 → 1.0.11

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.
@@ -0,0 +1,353 @@
1
+ import type { OpenClawConfig } from "openclaw/plugin-sdk";
2
+ import { getMixpayConfig } from "./config.js";
3
+ import type { MixinMixpayConfig } from "./config-schema.js";
4
+ import { createMixpayPayment, getMixpayPaymentResult, type MixpayPaymentResult } from "./mixpay-service.js";
5
+ import {
6
+ createMixpayOrder,
7
+ findMixpayOrder,
8
+ getMixpayStoreSnapshot,
9
+ listPendingMixpayOrders,
10
+ listRecentMixpayOrders,
11
+ type MixpayOrderRecord,
12
+ type MixpayOrderStatus,
13
+ updateMixpayOrder,
14
+ } from "./mixpay-store.js";
15
+ import { sendTextMessage } from "./send-service.js";
16
+ import { sleep, type SendLog } from "./shared.js";
17
+
18
+ export type MixinCollectRequest = {
19
+ amount: string;
20
+ assetId?: string;
21
+ settlementAssetId?: string;
22
+ memo?: string;
23
+ orderId?: string;
24
+ expireMinutes?: number;
25
+ };
26
+
27
+ const state: {
28
+ started: boolean;
29
+ cfg: OpenClawConfig | null;
30
+ log: SendLog | null;
31
+ } = {
32
+ started: false,
33
+ cfg: null,
34
+ log: null,
35
+ };
36
+
37
+ function getPendingPollDelayMs(cfg: OpenClawConfig): number {
38
+ const configured = Object.values((cfg.channels?.mixin as { accounts?: Record<string, { mixpay?: MixinMixpayConfig }> } | undefined)?.accounts ?? {})
39
+ .map((account) => account?.mixpay?.pollIntervalSec)
40
+ .filter((value): value is number => typeof value === "number" && Number.isFinite(value) && value > 0);
41
+ const topLevel = (cfg.channels?.mixin as { mixpay?: MixinMixpayConfig } | undefined)?.mixpay?.pollIntervalSec;
42
+ if (typeof topLevel === "number" && Number.isFinite(topLevel) && topLevel > 0) {
43
+ configured.push(topLevel);
44
+ }
45
+ return Math.max(5_000, Math.min(...(configured.length > 0 ? configured : [30])) * 1000);
46
+ }
47
+
48
+ function formatStatusLabel(status: MixpayOrderStatus): string {
49
+ switch (status) {
50
+ case "unpaid":
51
+ return "unpaid";
52
+ case "pending":
53
+ return "pending";
54
+ case "paid_less":
55
+ return "paid less";
56
+ case "success":
57
+ return "success";
58
+ case "failed":
59
+ return "failed";
60
+ case "expired":
61
+ return "expired";
62
+ default:
63
+ return "unknown";
64
+ }
65
+ }
66
+
67
+ export function formatMixpayOrderSummary(order: MixpayOrderRecord): string {
68
+ const lines = [
69
+ `MixPay order: ${order.orderId}`,
70
+ `Status: ${formatStatusLabel(order.status)}`,
71
+ `Amount: ${order.quoteAmount} ${order.quoteAssetId}`,
72
+ ];
73
+ if (order.paymentUrl) {
74
+ lines.push(`Pay: ${order.paymentUrl}`);
75
+ }
76
+ if (order.memo) {
77
+ lines.push(`Memo: ${order.memo}`);
78
+ }
79
+ if (order.expireAt) {
80
+ lines.push(`Expires: ${order.expireAt}`);
81
+ }
82
+ if (order.latestError) {
83
+ lines.push(`Latest error: ${order.latestError}`);
84
+ }
85
+ return lines.join("\n");
86
+ }
87
+
88
+ function shouldNotifyStatus(config: MixinMixpayConfig, status: MixpayOrderStatus): boolean {
89
+ if (status === "success" || status === "failed" || status === "expired") {
90
+ return true;
91
+ }
92
+ if (status === "pending") {
93
+ return config.notifyOnPending === true;
94
+ }
95
+ if (status === "paid_less") {
96
+ return config.notifyOnPaidLess !== false;
97
+ }
98
+ return false;
99
+ }
100
+
101
+ function validateMixpayResult(
102
+ order: MixpayOrderRecord,
103
+ payment: MixpayPaymentResult,
104
+ config: MixinMixpayConfig,
105
+ ): string | null {
106
+ if (config.payeeId?.trim() && payment.payeeId?.trim() && config.payeeId.trim() !== payment.payeeId.trim()) {
107
+ return "MixPay payeeId mismatch";
108
+ }
109
+ if (payment.quoteAssetId?.trim() && payment.quoteAssetId.trim() !== order.quoteAssetId.trim()) {
110
+ return "MixPay quoteAssetId mismatch";
111
+ }
112
+ if (payment.quoteAmount?.trim() && payment.quoteAmount.trim() !== order.quoteAmount.trim()) {
113
+ return "MixPay quoteAmount mismatch";
114
+ }
115
+ return null;
116
+ }
117
+
118
+ async function notifyOrderStatus(
119
+ cfg: OpenClawConfig,
120
+ order: MixpayOrderRecord,
121
+ nextStatus: MixpayOrderStatus,
122
+ log: SendLog,
123
+ ): Promise<void> {
124
+ const recipientId = order.recipientId || undefined;
125
+ const message = [
126
+ `MixPay order update: ${order.orderId}`,
127
+ `Status: ${formatStatusLabel(nextStatus)}`,
128
+ `Amount: ${order.quoteAmount} ${order.quoteAssetId}`,
129
+ ];
130
+ if (order.paymentUrl) {
131
+ message.push(`Pay: ${order.paymentUrl}`);
132
+ }
133
+ const result = await sendTextMessage(cfg, order.accountId, order.conversationId, recipientId, message.join("\n"), log);
134
+ if (!result.ok) {
135
+ throw new Error(result.error ?? "failed to notify MixPay order status");
136
+ }
137
+ }
138
+
139
+ async function pollPendingOrders(): Promise<void> {
140
+ const cfg = state.cfg;
141
+ const log = state.log;
142
+ if (!cfg || !log) {
143
+ return;
144
+ }
145
+
146
+ const pendingOrders = await listPendingMixpayOrders();
147
+ for (const order of pendingOrders) {
148
+ const mixpayConfig = getMixpayConfig(cfg, order.accountId);
149
+ if (!mixpayConfig?.enabled) {
150
+ continue;
151
+ }
152
+
153
+ try {
154
+ const payment = await getMixpayPaymentResult({ config: mixpayConfig, orderId: order.orderId, traceId: order.traceId });
155
+ const mismatch = validateMixpayResult(order, payment, mixpayConfig);
156
+ if (mismatch) {
157
+ throw new Error(mismatch);
158
+ }
159
+ const nextStatus = payment.status;
160
+ const updated = await updateMixpayOrder(order.orderId, (current) => ({
161
+ ...current,
162
+ paymentId: current.paymentId ?? payment.raw.id as string | undefined,
163
+ rawStatus: payment.rawStatus,
164
+ status: nextStatus,
165
+ lastPolledAt: new Date().toISOString(),
166
+ updatedAt: new Date().toISOString(),
167
+ }));
168
+
169
+ if (!updated) {
170
+ continue;
171
+ }
172
+
173
+ if (updated.lastNotifyStatus !== nextStatus && shouldNotifyStatus(mixpayConfig, nextStatus)) {
174
+ await notifyOrderStatus(cfg, updated, nextStatus, log);
175
+ await updateMixpayOrder(order.orderId, (current) => ({
176
+ ...current,
177
+ lastNotifyStatus: nextStatus,
178
+ updatedAt: new Date().toISOString(),
179
+ }));
180
+ }
181
+ } catch (err) {
182
+ log.warn(`[mixin] MixPay poll failed: orderId=${order.orderId}, error=${err instanceof Error ? err.message : String(err)}`);
183
+ await updateMixpayOrder(order.orderId, (current) => ({
184
+ ...current,
185
+ latestError: err instanceof Error ? err.message : String(err),
186
+ lastPolledAt: new Date().toISOString(),
187
+ updatedAt: new Date().toISOString(),
188
+ }));
189
+ }
190
+ }
191
+ }
192
+
193
+ export async function startMixpayWorker(cfg: OpenClawConfig, log: SendLog): Promise<void> {
194
+ state.cfg = cfg;
195
+ state.log = log;
196
+ if (state.started) {
197
+ return;
198
+ }
199
+ state.started = true;
200
+
201
+ void (async () => {
202
+ while (true) {
203
+ try {
204
+ await pollPendingOrders();
205
+ } catch (err) {
206
+ log.error("[mixin] MixPay worker loop failed", err);
207
+ }
208
+ await sleep(getPendingPollDelayMs(state.cfg ?? cfg));
209
+ }
210
+ })();
211
+ }
212
+
213
+ export async function createMixinCollectOrder(params: {
214
+ cfg: OpenClawConfig;
215
+ accountId: string;
216
+ conversationId: string;
217
+ recipientId?: string;
218
+ creatorId: string;
219
+ request: MixinCollectRequest;
220
+ }): Promise<MixpayOrderRecord> {
221
+ const mixpayConfig = getMixpayConfig(params.cfg, params.accountId);
222
+ if (!mixpayConfig?.enabled) {
223
+ throw new Error("MixPay is not enabled for this Mixin account");
224
+ }
225
+
226
+ const normalizedCreatorId = params.creatorId.trim().toLowerCase();
227
+ const allowedCreators = (mixpayConfig.allowedCreators ?? []).map((item) => item.trim().toLowerCase()).filter(Boolean);
228
+ if (allowedCreators.length > 0 && !allowedCreators.includes(normalizedCreatorId)) {
229
+ throw new Error("MixPay collect creation is not allowed for this sender");
230
+ }
231
+
232
+ const created = await createMixpayPayment({
233
+ config: mixpayConfig,
234
+ orderId: params.request.orderId,
235
+ quoteAmount: params.request.amount,
236
+ quoteAssetId: params.request.assetId?.trim() || mixpayConfig.defaultQuoteAssetId?.trim() || "",
237
+ settlementAssetId: params.request.settlementAssetId,
238
+ memo: params.request.memo,
239
+ expireMinutes: params.request.expireMinutes,
240
+ });
241
+
242
+ const quoteAssetId = params.request.assetId?.trim() || mixpayConfig.defaultQuoteAssetId?.trim();
243
+ if (!quoteAssetId) {
244
+ throw new Error("MixPay quote asset is not configured");
245
+ }
246
+
247
+ const record: MixpayOrderRecord = {
248
+ orderId: created.orderId,
249
+ traceId: created.traceId,
250
+ paymentId: created.paymentId,
251
+ code: created.code,
252
+ paymentUrl: created.paymentUrl,
253
+ accountId: params.accountId,
254
+ conversationId: params.conversationId,
255
+ recipientId: params.recipientId,
256
+ creatorId: params.creatorId,
257
+ quoteAssetId,
258
+ quoteAmount: params.request.amount,
259
+ settlementAssetId: params.request.settlementAssetId,
260
+ memo: params.request.memo,
261
+ status: "unpaid",
262
+ createdAt: created.createdAt,
263
+ updatedAt: created.createdAt,
264
+ expireAt: created.expireAt,
265
+ };
266
+
267
+ await createMixpayOrder(record);
268
+ return record;
269
+ }
270
+
271
+ export async function getMixpayOrderStatusText(orderId: string): Promise<string> {
272
+ const order = await findMixpayOrder(orderId);
273
+ if (!order) {
274
+ return `MixPay order not found: ${orderId}`;
275
+ }
276
+ return formatMixpayOrderSummary(order);
277
+ }
278
+
279
+ export async function refreshMixpayOrderStatus(params: {
280
+ cfg: OpenClawConfig;
281
+ accountId: string;
282
+ orderId: string;
283
+ }): Promise<MixpayOrderRecord | null> {
284
+ const order = await findMixpayOrder(params.orderId);
285
+ if (!order || order.accountId !== params.accountId) {
286
+ return order;
287
+ }
288
+
289
+ const mixpayConfig = getMixpayConfig(params.cfg, params.accountId);
290
+ if (!mixpayConfig?.enabled) {
291
+ return order;
292
+ }
293
+
294
+ try {
295
+ const payment = await getMixpayPaymentResult({
296
+ config: mixpayConfig,
297
+ orderId: order.orderId,
298
+ traceId: order.traceId,
299
+ });
300
+ const mismatch = validateMixpayResult(order, payment, mixpayConfig);
301
+ if (mismatch) {
302
+ throw new Error(mismatch);
303
+ }
304
+ return await updateMixpayOrder(order.orderId, (current) => ({
305
+ ...current,
306
+ rawStatus: payment.rawStatus,
307
+ status: payment.status,
308
+ lastPolledAt: new Date().toISOString(),
309
+ updatedAt: new Date().toISOString(),
310
+ latestError: undefined,
311
+ }));
312
+ } catch (err) {
313
+ return await updateMixpayOrder(order.orderId, (current) => ({
314
+ ...current,
315
+ latestError: err instanceof Error ? err.message : String(err),
316
+ lastPolledAt: new Date().toISOString(),
317
+ updatedAt: new Date().toISOString(),
318
+ }));
319
+ }
320
+ }
321
+
322
+ export async function getRecentMixpayOrdersText(params: {
323
+ accountId: string;
324
+ conversationId: string;
325
+ limit?: number;
326
+ }): Promise<string> {
327
+ const items = await listRecentMixpayOrders({
328
+ accountId: params.accountId,
329
+ conversationId: params.conversationId,
330
+ limit: params.limit ?? 5,
331
+ });
332
+
333
+ if (items.length === 0) {
334
+ return "No MixPay orders found for this conversation.";
335
+ }
336
+
337
+ return items
338
+ .map((item) => `${item.orderId} | ${formatStatusLabel(item.status)} | ${item.quoteAmount} ${item.quoteAssetId}`)
339
+ .join("\n");
340
+ }
341
+
342
+ export async function getMixpayStatusSnapshot(): Promise<{
343
+ pendingOrders: number;
344
+ storeDir: string;
345
+ storeFile: string;
346
+ }> {
347
+ const snapshot = await getMixpayStoreSnapshot();
348
+ return {
349
+ pendingOrders: snapshot.pending,
350
+ storeDir: snapshot.storeDir,
351
+ storeFile: snapshot.storeFile,
352
+ };
353
+ }
@@ -0,0 +1,342 @@
1
+ import type {
2
+ ChannelOnboardingAdapter,
3
+ ChannelOnboardingDmPolicy,
4
+ DmPolicy,
5
+ OpenClawConfig,
6
+ WizardPrompter,
7
+ } from "openclaw/plugin-sdk";
8
+ import { DEFAULT_ACCOUNT_ID, promptAccountId } from "openclaw/plugin-sdk";
9
+ import { getAccountConfig, listAccountIds, resolveAccount, resolveDefaultAccountId } from "./config.js";
10
+ import type { MixinAccountConfig } from "./config-schema.js";
11
+
12
+ const channel = "mixin" as const;
13
+
14
+ type MixinConfigRoot = Partial<MixinAccountConfig> & {
15
+ defaultAccount?: string;
16
+ accounts?: Record<string, Partial<MixinAccountConfig> | undefined>;
17
+ };
18
+
19
+ type MixinGroupPolicy = NonNullable<MixinAccountConfig["groupPolicy"]>;
20
+
21
+ type MixinDmPolicy = NonNullable<MixinAccountConfig["dmPolicy"]>;
22
+
23
+ function isRecord(value: unknown): value is Record<string, unknown> {
24
+ return typeof value === "object" && value !== null && !Array.isArray(value);
25
+ }
26
+
27
+ function getMixinRoot(cfg: OpenClawConfig): MixinConfigRoot {
28
+ const root = cfg as unknown as Record<string, unknown>;
29
+ const channels = isRecord(root.channels) ? root.channels : undefined;
30
+ const channelConfig = channels && isRecord(channels.mixin) ? channels.mixin : undefined;
31
+ if (channelConfig) {
32
+ return channelConfig as MixinConfigRoot;
33
+ }
34
+
35
+ const legacyNamedConfig = isRecord(root.mixin) ? root.mixin : undefined;
36
+ if (legacyNamedConfig) {
37
+ return legacyNamedConfig as MixinConfigRoot;
38
+ }
39
+
40
+ const plugins = isRecord(root.plugins) ? root.plugins : undefined;
41
+ const entries = plugins && isRecord(plugins.entries) ? plugins.entries : undefined;
42
+ const mixinEntry = entries && isRecord(entries.mixin) ? entries.mixin : undefined;
43
+ const pluginEntryConfig = mixinEntry && isRecord(mixinEntry.config) ? mixinEntry.config : undefined;
44
+ if (pluginEntryConfig) {
45
+ return pluginEntryConfig as MixinConfigRoot;
46
+ }
47
+
48
+ return isRecord(root) ? (root as MixinConfigRoot) : {};
49
+ }
50
+
51
+ function updateMixinRoot(cfg: OpenClawConfig, patch: Partial<MixinConfigRoot>): OpenClawConfig {
52
+ const current = getMixinRoot(cfg);
53
+ return {
54
+ ...cfg,
55
+ channels: {
56
+ ...(cfg.channels ?? {}),
57
+ mixin: {
58
+ ...current,
59
+ ...patch,
60
+ },
61
+ },
62
+ } as OpenClawConfig;
63
+ }
64
+
65
+ function updateMixinAccountConfig(
66
+ cfg: OpenClawConfig,
67
+ accountId: string,
68
+ patch: Partial<MixinAccountConfig>,
69
+ ): OpenClawConfig {
70
+ if (accountId === DEFAULT_ACCOUNT_ID) {
71
+ return updateMixinRoot(cfg, patch);
72
+ }
73
+
74
+ const current = getMixinRoot(cfg);
75
+ const accounts = current.accounts ?? {};
76
+
77
+ return updateMixinRoot(cfg, {
78
+ accounts: {
79
+ ...accounts,
80
+ [accountId]: {
81
+ ...(accounts[accountId] ?? {}),
82
+ ...patch,
83
+ },
84
+ },
85
+ });
86
+ }
87
+
88
+ function mergeAllowFrom(values: string[] | undefined, nextValue: string): string[] {
89
+ const parts = nextValue
90
+ .split(/[\n,;]+/g)
91
+ .map((entry) => entry.trim())
92
+ .filter(Boolean);
93
+ return [...new Set([...(values ?? []), ...parts])];
94
+ }
95
+
96
+
97
+ async function promptAccountGuide(prompter: WizardPrompter): Promise<void> {
98
+ await prompter.note(
99
+ [
100
+ "Mixin uses Blaze WebSocket and needs one account block per bot account.",
101
+ "Required fields: appId, sessionId, serverPublicKey, sessionPrivateKey.",
102
+ "Single-account setup lives directly under channels.mixin.",
103
+ "Multi-account setup lives under channels.mixin.accounts.<accountId>.",
104
+ "Optional fields: dmPolicy, allowFrom, groupPolicy, mixpay, proxy.",
105
+ "Docs: README.md / README.zh-CN.md",
106
+ ].join("\n"),
107
+ "Mixin setup guide",
108
+ );
109
+ }
110
+
111
+ function setRootDmPolicy(cfg: OpenClawConfig, policy: DmPolicy): OpenClawConfig {
112
+ return updateMixinRoot(cfg, {
113
+ dmPolicy: policy as MixinDmPolicy,
114
+ });
115
+ }
116
+
117
+ function normalizeDmPolicy(value: unknown): MixinDmPolicy {
118
+ return value === "allowlist" || value === "open" || value === "disabled" ? value : "pairing";
119
+ }
120
+
121
+ function normalizeGroupPolicy(value: unknown): MixinGroupPolicy {
122
+ return value === "allowlist" || value === "open" || value === "disabled" ? value : "open";
123
+ }
124
+
125
+ function defaultMixpayConfig(existing?: MixinAccountConfig["mixpay"]): NonNullable<MixinAccountConfig["mixpay"]> {
126
+ return {
127
+ enabled: existing?.enabled ?? false,
128
+ apiBaseUrl: existing?.apiBaseUrl,
129
+ payeeId: existing?.payeeId,
130
+ defaultQuoteAssetId: existing?.defaultQuoteAssetId,
131
+ defaultSettlementAssetId: existing?.defaultSettlementAssetId,
132
+ expireMinutes: existing?.expireMinutes ?? 15,
133
+ pollIntervalSec: existing?.pollIntervalSec ?? 30,
134
+ allowedCreators: existing?.allowedCreators ?? [],
135
+ notifyOnPending: existing?.notifyOnPending ?? false,
136
+ notifyOnPaidLess: existing?.notifyOnPaidLess ?? true,
137
+ };
138
+ }
139
+
140
+ async function promptAllowFrom(params: {
141
+ cfg: OpenClawConfig;
142
+ prompter: WizardPrompter;
143
+ accountId?: string;
144
+ }): Promise<OpenClawConfig> {
145
+ const accountId = params.accountId ?? DEFAULT_ACCOUNT_ID;
146
+ const current = getAccountConfig(params.cfg, accountId).allowFrom ?? [];
147
+ await params.prompter.note(
148
+ [
149
+ "Enter Mixin UUID values separated by commas or new lines.",
150
+ "Example: 12345678-1234-1234-1234-123456789abc",
151
+ "Leave blank if you want to keep pairing-only access.",
152
+ ].join("\n"),
153
+ "Mixin allowFrom",
154
+ );
155
+ const raw = await params.prompter.text({
156
+ message: "Allowed Mixin UUIDs",
157
+ placeholder: "uuid-one, uuid-two",
158
+ initialValue: current.join(", ") || undefined,
159
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
160
+ });
161
+ const allowFrom = mergeAllowFrom(current, String(raw));
162
+ return updateMixinAccountConfig(params.cfg, accountId, { allowFrom });
163
+ }
164
+
165
+ async function promptAccountConfig(params: {
166
+ cfg: OpenClawConfig;
167
+ prompter: WizardPrompter;
168
+ accountId: string;
169
+ }): Promise<OpenClawConfig> {
170
+ const resolved = resolveAccount(params.cfg, params.accountId);
171
+ const current = getAccountConfig(params.cfg, params.accountId);
172
+ let next = params.cfg;
173
+
174
+ const appId = String(
175
+ await params.prompter.text({
176
+ message: "Mixin appId",
177
+ initialValue: resolved.appId ?? undefined,
178
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
179
+ }),
180
+ ).trim();
181
+
182
+ const sessionId = String(
183
+ await params.prompter.text({
184
+ message: "Mixin sessionId",
185
+ initialValue: resolved.sessionId ?? undefined,
186
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
187
+ }),
188
+ ).trim();
189
+
190
+ const serverPublicKey = String(
191
+ await params.prompter.text({
192
+ message: "Mixin serverPublicKey",
193
+ initialValue: resolved.serverPublicKey ?? undefined,
194
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
195
+ }),
196
+ ).trim();
197
+
198
+ const sessionPrivateKey = String(
199
+ await params.prompter.text({
200
+ message: "Mixin sessionPrivateKey",
201
+ initialValue: resolved.sessionPrivateKey ?? undefined,
202
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
203
+ }),
204
+ ).trim();
205
+
206
+ next = updateMixinAccountConfig(next, params.accountId, {
207
+ enabled: true,
208
+ appId,
209
+ sessionId,
210
+ serverPublicKey,
211
+ sessionPrivateKey,
212
+ });
213
+
214
+ const dmPolicy = await params.prompter.select<MixinDmPolicy>({
215
+ message: "DM policy",
216
+ initialValue: normalizeDmPolicy(current.dmPolicy),
217
+ options: [
218
+ { value: "pairing", label: "Pairing", hint: "Accept DMs after pairing approval" },
219
+ { value: "allowlist", label: "Allowlist", hint: "Only accept DMs from allowFrom" },
220
+ { value: "open", label: "Open", hint: "Accept DMs without an allowlist" },
221
+ { value: "disabled", label: "Disabled", hint: "Disable DMs for this account" },
222
+ ],
223
+ });
224
+ next = setRootDmPolicy(next, dmPolicy);
225
+ next = updateMixinAccountConfig(next, params.accountId, {
226
+ dmPolicy,
227
+ });
228
+
229
+ if (dmPolicy === "allowlist") {
230
+ next = await promptAllowFrom({ cfg: next, prompter: params.prompter, accountId: params.accountId });
231
+ }
232
+
233
+ const groupPolicy = await params.prompter.select<MixinGroupPolicy>({
234
+ message: "Group policy",
235
+ initialValue: normalizeGroupPolicy(current.groupPolicy),
236
+ options: [
237
+ { value: "open", label: "Open", hint: "Allow all configured group chats" },
238
+ { value: "allowlist", label: "Allowlist", hint: "Only accept listed group chats" },
239
+ { value: "disabled", label: "Disabled", hint: "Disable group access" },
240
+ ],
241
+ });
242
+ next = updateMixinAccountConfig(next, params.accountId, {
243
+ groupPolicy,
244
+ });
245
+
246
+ const addGroupAllowFrom = groupPolicy === "allowlist"
247
+ ? await params.prompter.confirm({
248
+ message: "Add initial group allowFrom entries now?",
249
+ initialValue: true,
250
+ })
251
+ : false;
252
+ if (addGroupAllowFrom) {
253
+ const groupAllowFrom = String(
254
+ await params.prompter.text({
255
+ message: "Group allowFrom",
256
+ placeholder: "conversation-id-one, conversation-id-two",
257
+ validate: (value) => (String(value ?? "").trim() ? undefined : "Required"),
258
+ }),
259
+ )
260
+ .split(/[\n,;]+/g)
261
+ .map((entry) => entry.trim())
262
+ .filter(Boolean);
263
+ next = updateMixinAccountConfig(next, params.accountId, {
264
+ groupAllowFrom,
265
+ });
266
+ }
267
+
268
+ const mixpayEnabled = await params.prompter.confirm({
269
+ message: "Enable MixPay for this account?",
270
+ initialValue: Boolean(current.mixpay?.enabled),
271
+ });
272
+ if (mixpayEnabled) {
273
+ next = updateMixinAccountConfig(next, params.accountId, {
274
+ mixpay: {
275
+ ...defaultMixpayConfig(current.mixpay),
276
+ enabled: true,
277
+ },
278
+ });
279
+ }
280
+
281
+ return next;
282
+ }
283
+
284
+ const dmPolicy: ChannelOnboardingDmPolicy = {
285
+ label: "Mixin",
286
+ channel,
287
+ policyKey: "channels.mixin.dmPolicy",
288
+ allowFromKey: "channels.mixin.allowFrom",
289
+ getCurrent: (cfg) => normalizeDmPolicy((cfg.channels?.mixin as { dmPolicy?: unknown } | undefined)?.dmPolicy),
290
+ setPolicy: (cfg, policy) => setRootDmPolicy(cfg, policy as DmPolicy),
291
+ promptAllowFrom,
292
+ };
293
+
294
+ export const mixinOnboardingAdapter: ChannelOnboardingAdapter = {
295
+ channel,
296
+ getStatus: async ({ cfg }) => {
297
+ const accountIds = listAccountIds(cfg);
298
+ const configured = accountIds.some((accountId) => resolveAccount(cfg, accountId).configured);
299
+ return {
300
+ channel,
301
+ configured,
302
+ statusLines: [
303
+ `Mixin: ${configured ? "configured" : "needs credentials"}`,
304
+ `Accounts: ${accountIds.length}`,
305
+ ],
306
+ selectionHint: configured ? "configured" : "Blaze WebSocket Mixin bridge",
307
+ quickstartScore: configured ? 1 : 4,
308
+ };
309
+ },
310
+ configure: async ({ cfg, prompter, accountOverrides, shouldPromptAccountIds }) => {
311
+ await promptAccountGuide(prompter);
312
+
313
+ const mixinOverride = accountOverrides.mixin?.trim();
314
+ const defaultAccountId = resolveDefaultAccountId(cfg);
315
+ let accountId = mixinOverride || defaultAccountId;
316
+ if (shouldPromptAccountIds && !mixinOverride) {
317
+ accountId = await promptAccountId({
318
+ cfg,
319
+ prompter,
320
+ label: "Mixin",
321
+ currentId: accountId,
322
+ listAccountIds,
323
+ defaultAccountId,
324
+ });
325
+ }
326
+
327
+ const next = await promptAccountConfig({ cfg, prompter, accountId });
328
+ await prompter.outro(
329
+ [
330
+ `Configured account: ${accountId}`,
331
+ "Restart the Gateway after saving the config.",
332
+ "Use /mixin-status to verify the connection.",
333
+ ].join("\n"),
334
+ );
335
+
336
+ return {
337
+ cfg: next,
338
+ accountId,
339
+ };
340
+ },
341
+ dmPolicy,
342
+ };